diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..c0b199b --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "stage-0", "react"], + "plugins": ["transform-runtime", "transform-decorators-legacy"] +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6cb772b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +tmp diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6051e92 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 4 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.json] +indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..bf61c02 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,67 @@ +/* +# Airbnb is putting everything on error which makes it hard to spot JavaScript errors: https://github.com/airbnb/javascript/issues/853 +# Work-around, move everything from error to warn: +find node_modules/eslint-config-airbnb -name '*.js'|xargs sed -i 's/\[2,/\[1,/' +find node_modules/eslint-config-airbnb -name '*.js'|xargs sed -i "s/': 2/': 1/" +*/ +{ + "extends": "airbnb", + "parser": "babel-eslint", + "env": { + "browser": true, + "node": true, + "es6": true + }, + "rules": { + "max-len": 0, /*[1, 160, 4],*/ + "comma-dangle": 0, + "semi": [0], + "curly": [0], + "one-var": [0], + "eqeqeq": 0, + "no-cond-assign": 0, + "react/wrap-multilines": [0], + "one-var-declaration-per-line": [0], + "no-param-reassign": [0], + "no-nested-ternary": [0], + "no-undef": 2, + "camelcase": [0], + "no-console": [0], + "padded-blocks": 0, + "object-curly-spacing": [0], + "react/jsx-indent": [1,4], + "react/jsx-indent-props": [1,4], + "react/jsx-closing-bracket-location": [0], + "no-use-before-define": [0, {"functions": false, "classes": false}], + "spaced-comment": [0], + "prefer-template": [0], + "new-cap": [0], + "arrow-body-style": [0], + "func-names": [0], + "no-return-assign": 0, + "no-redeclare": 1, + "eol-last": 0, + "no-loop-func": 0, + "no-unneeded-ternary": 0, // false trigger const b = b ? b : a + + /* Nice to haves */ + /*"quotes": [1, "single", "avoid-escape"],*/ "quotes": 0, + /*"indent", [1,4], */ "indent": 0, + "brace-style": 0, + "space-infix-ops": 0, + "keyword-spacing": 0, + "no-confusing-arrow": 0, + "space-in-parens": 0, + "no-throw-literal": 0, + "react/sort-comp": [1, { "order": [ "lifecycle" ] }], + "react/prefer-stateless-function": 0, + "react/prop-types": 0, + "radix": 0, + + "jsx-a11y/href-no-hash": "off", + "jsx-a11y/anchor-is-valid": ["warn", { "aspects": ["invalidHref"] }], + "import/no-extraneous-dependencies": 0, + "import/no-unresolved": 0, + "import/extensions": 0 + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0391319 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +.idea +coverage +node_modules +*.log +dist +vendor/* +tmp/* +.vagrant +lib + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4d0ed73 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Please Read + +Please read these instructions before submitting issues to the Steemit.com GitHub repository. The issue tracker is for bugs and specific implementation discussion **only**. It should not be used for other purposes, as described below. + +## Bug Reports + +If there is an existing feature that is not working correctly, or a glitch in the website that is impacting user behaviour - please open an issue to report variance. Include as much relevant information as you can, including screen shots and steps to reproduce the issue. + +## Technical Support and Signup/Login Issues + +If you are having trouble using the website but it is not an error with the website (this includes signup/login issues), do **not** open a GitHub issue. Please request help from the users in the [steemit.chat help](https://steemit.chat/channel/help) channel. + +## Enhancement Suggestions + +Do **not** use the issue tracker to suggest enhancements or improvements to the platform. The best place for these discussions is on Steemit.com. If there is a well vetted idea that has the support of the community that you feel should be considered by the development team, please email it to [sneak@steemit.com](mailto:sneak@steemit.com) for review. + +## Implementation Discussion + +The developers frequently open issues to discuss changes that are being worked on. This is to inform the community of the changes being worked on, and to get input from the community and other developers on the implementation. + +Issues opened that devolve into lengthy discussion of minor site features will be closed or locked. The issue tracker is not a general purpose discussion forum. + +This is not the place to make suggestions for product improvement (please see the Enhancement Suggestions section above for this). If you are not planning to work on the change yourself - do not open an issue for it. + +## Duplicate Issues + +Please do a keyword search to see if there is already an existing issue before opening a new one. + +## Steemit.com vs. Steem Blockchain + +This issue tracker is only intended to track issues for the Steemit.com website. If the issue is with the Steem blockchain, please open an issue in the [Steem Repository](https://github.com/steemit/steem). + +## Pull Requests + +Anybody in the community is welcome and encouraged to submit pull requests with any desired changes to the platform! + +Requests to make changes to steemit.com that include working, tested pull requests jump to the top of the queue. There is not a guarantee that all functionality submitted as a PR will be accepted and merged, however. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4cc9e00 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM node:7.5 + +# yarn > npm +#RUN npm install --global yarn + +RUN npm install -g yarn + +WORKDIR /var/app +RUN mkdir -p /var/app +ADD package.json /var/app/package.json +RUN yarn + +COPY . /var/app + +# FIXME TODO: fix eslint warnings + +#RUN mkdir tmp && \ +# npm test && \ +# ./node_modules/.bin/eslint . && \ +# npm run build + +RUN mkdir tmp && \ + npm test && \ + npm run-script build + +ENV PORT 8080 +ENV NODE_ENV production + +EXPOSE 8080 + +CMD [ "yarn", "run", "production" ] + +# uncomment the lines below to run it in development mode +# ENV NODE_ENV development +# CMD [ "yarn", "run", "start" ] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1afba09 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +default: test + +test: node_modules + npm test + +node_modules: + yarn install + +build: + docker build -t steemit/steemit.com . + +clean: + rm -rf node_modules *.log tmp npm-debug.log.* + +vagrant: + vagrant destroy -f + vagrant up diff --git a/README.md b/README.md new file mode 100644 index 0000000..aca00ee --- /dev/null +++ b/README.md @@ -0,0 +1,226 @@ + +# Condenser + + +Condenser is the react.js web interface to the world's first and best blockchain-based social media platform, steemit.com. It uses [STEEM](https://github.com/steemit/steem), a blockchain powered by Graphene 2.0 technology to store JSON-based content for a plethora of web applications. + +## Why would I want to use Condenser (steemit.com front-end)? +* Learning how to build blockchain-based web applications using STEEM as a content storage mechanism in react.js +* Reviewing the inner workings of the steemit.com social media platform +* Assisting with software development for steemit.com + +## Installation + +#### Docker + +We highly recommend using docker to run condenser. This is how we run the live steemit.com site and it is the most supported (and fastest) method of both building and running condenser. We will always have the latest version of condenser (master branch) available on dockerhub. Configuration settings can be set using environment variables (see configuration section below for more information). If you need to install docker, you can get it at https://get.docker.com + +To bring up a running container it's as simple as this: + +```bash +docker run -it -p 8080:8080 steemit/condenser +``` + +Environment variables can be added like this: + +```bash +docker run -it --env SDC_DATABASE_URL="mysql://user:pass@hostname/databasename" -p 8080:8080 steemit/condenser +``` + +If you would like to modify, build, and run condenser using docker, it's as simple as pulling in the github repo and issuing one command to build it, like this: + +```bash +git clone https://github.com/steemit/condenser +cd condenser +docker build -t="myname/condenser:mybranch" . +docker run -it -p 8080:8080 myname/condenser:mybranch +``` + +## Building from source without docker (the 'traditional' way): + +#### Clone the repository and make a tmp folder +```bash +git clone https://github.com/steemit/condenser +cd condenser +mkdir tmp +``` + +#### Install dependencies + +Install at least Node v7.5 if you don't already have it. We recommend using `nvm` to do this as it's both the simplest way to install and manage installed version(s) of node. If you need `nvm`, you can get it at [https://github.com/creationix/nvm](https://github.com/creationix/nvm). + +Condenser is known to successfully build using node 7.5, npm 4.1.2, and yarn 1.1.0. + +Using nvm, you would install like this: +```bash +nvm install v7.5 +``` + +We use the yarn package manager instead of the default `npm`. There are multiple reasons for this, one being that we have `steem-js` built from source pulling the github repo as part of the build process and yarn supports this. This way the library that handles keys can be loaded by commit hash instead of a version name and cryptographically verified to be exactly what we expect it to be. Yarn can be installed with `npm`, but afterwards you will not need to use `npm` further. + +```bash +npm install -g yarn +yarn global add babel-cli +yarn install +yarn run build +``` +To run condenser in production mode, run: + +```bash +yarn run production +``` + +When launching condenser in production mode it will automatically use 1 process per available core. You will be able to access the front-end at http://localhost:8080 by default. + +To run condenser in development mode, run: + +```bash +yarn run start +``` + +It will take quite a bit longer to start in this mode (~60s) as it needs to build and start the webpack-dev-server. + +By default you will be connected to steemit.com's public steem node at `wss://steemd.steeemit.com`. This is actually on the real blockchain and you would use your regular account name and credentials to login - there is not an official separate testnet at this time. If you intend to run a full-fledged site relying on your own, we recommend looking into running a copy of `steemd` locally instead [https://github.com/steemit/steem](https://github.com/steemit/steem). + +#### Configuration + +The intention is to configure condenser using environment variables. You can see the names of all of the available configuration environment variables in `config/custom-environment-variables.json`. Default values are stored in `config/defaults.json`. + +Environment variables using an example like this: + +```bash +export SDC_CLIENT_STEEMD_URL="wss://steemd.steemit.com" +export SDC_SERVER_STEEMD_URL="wss://steemd.steemit.com" +``` +Keep in mind environment variables only exist in your active session, so if you wish to save them for later use you can put them all in a file and `source` them in. + +If you'd like to statically configure condenser without variables you can edit the settings directly in `config/production.json`. If you're running in development mode, copy `config/production.json` to `config/dev.json` with `cp config/production.json config/dev.json` and adjust settings in `dev.json`. + +If you're intending to run condenser in a production environment one configuration option that you will definitely want to edit is `server_session_secret` which can be set by the environment variable `SDC_SESSION_SECRETKEY`. To generate a new value for this setting, you can do this: + +```bash +node +> crypto.randomBytes(32).toString('base64') +> .exit +``` + +#### Install mysql server + +If you've followed the instructions up until this point you will already have a running condenser installation which is entirely acceptable for development purposes. It is *not required to run a SQL server for development*. If you're running a full-fledged site however, you will want to set one up. + +Once set up, you can set the mysql server configuration option for condenser using the environment variable `SDC_DATABASE_URL`, or alternatively by editing it in `config/production.json`. You will use the format `mysql://user:pass@hostname/databasename`. + +Example: + +```bash +export SDC_DATABASE_URL="mysql://root:password@127.0.0.1/steemit_dev" +``` + +Here are instructions for setting up a mysql server and running the necessary migrations by operating system: + +OS X: + +```bash +brew update +brew doctor +brew upgrade +brew install mysql +mysql.server restart +``` + +Debian based Linux: + +```bash +sudo apt-get update +sudo apt-get install mysql-server +``` + +On Ubuntu 16.04+ you may be unable to connect to mysql without root access, if +so update the mysql root user as follows: + +``` +sudo mysql -u root +DROP USER 'root'@'localhost'; +CREATE USER 'root'@'%' IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'; +FLUSH PRIVILEGES; +``` + +Now launch mysql client and create steemit_dev database: +```bash +mysql -u root +> create database steemit_dev; +> quit +``` + +#### Database migrations + +This is a required step in order for the database to be 'ready' for condenser's use. + + +Edit the file `src/db/config/config.json` using your favorite command line text editor being sure that the username, password, host, and database name are set correctly and match your newly configured mysql setup. + +Run `sequelize db:migrate` in `src/db` directory, like this: + +```bash +cd src/db +yarn exec sequelize db:migrate +``` + +#### Install Tarantool - Production Only + +Tarantool similarly to mysql is not required for development but if you're running a full-fledged site with condenser you will want to run one. + +OS X: + +```bash +brew install tarantool +``` + +Debian based Linux: + +```bash +sudo apt-get install tarantool +``` + +Test the interactive console: + +```bash +user@example:~$ tarantool +``` + +#### Style Guides For Submitting Pull Requests + +##### File naming and location + +- Prefer CamelCase js and jsx file names +- Prefer lower case one word directory names +- Keep stylesheet files close to components +- Component's stylesheet file name should match component name + +##### Js & Jsx +We are using _[Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript)_ with some modifications (see .eslintrc). +Please run _eslint_ in the working directory before committing your changes and make sure you didn't introduce any new styling issues. + +##### CSS & SCSS +If a component requires a css rule, please use its uppercase name for the class, e.g. "Header" class for the header's root div. +We adhere to BEM methodology with exception for Foundation classes, here is an example for the Header component: + +```html + + +``` + +## Issues + +To report a non-critical issue, please file an issue on this GitHub project. + +If you find a security issue please report details to: security@steemit.com + +We will evaluate the risk and make a patch available before filing the issue. diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..841f805 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,92 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# All Vagrant configuration is done below. The "2" in Vagrant.configure +# configures the configuration version (we support older styles for +# backwards compatibility). Please don't change it unless you know what +# you're doing. +Vagrant.configure("2") do |config| + # The most common configuration options are documented and commented below. + # For a complete reference, please see the online documentation at + # https://docs.vagrantup.com. + + # Every Vagrant development environment requires a box. You can search for + # boxes at https://atlas.hashicorp.com/search. + config.vm.box = "ubuntu/xenial64" + + # Disable automatic box update checking. If you disable this, then + # boxes will only be checked for updates when the user runs + # `vagrant box outdated`. This is not recommended. + # config.vm.box_check_update = false + + # Create a forwarded port mapping which allows access to a specific port + # within the machine from a port on the host machine. In the example below, + # accessing "localhost:8080" will access port 80 on the guest machine. + # config.vm.network "forwarded_port", guest: 80, host: 8080 + + # Create a private network, which allows host-only access to the machine + # using a specific IP. + # config.vm.network "private_network", ip: "192.168.33.10" + + # Create a public network, which generally matched to bridged network. + # Bridged networks make the machine appear as another physical device on + # your network. + # config.vm.network "public_network" + + # Share an additional folder to the guest VM. The first argument is + # the path on the host to the actual folder. The second argument is + # the path on the guest to mount the folder. And the optional third + # argument is a set of non-required options. + # config.vm.synced_folder "../data", "/vagrant_data" + + # Provider-specific configuration so you can fine-tune various + # backing providers for Vagrant. These expose provider-specific options. + # Example for VirtualBox: + # + config.vm.provider "virtualbox" do |vb| + # # Display the VirtualBox GUI when booting the machine + # vb.gui = true + # + # # Customize the amount of memory on the VM: + vb.memory = "2048" + end + # + # View the documentation for the provider you are using for more + # information on available options. + + # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies + # such as FTP and Heroku are also available. See the documentation at + # https://docs.vagrantup.com/v2/push/atlas.html for more information. + # config.push.define "atlas" do |push| + # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" + # end + + # Enable provisioning with a shell script. Additional provisioners such as + # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the + # documentation for more information about their specific syntax and use. + + + config.vm.provision "shell", inline: <<-SHELL + apt-get update + apt-get install -y build-essential + export NVM_DIR="/usr/local/nvm" && ( + git clone https://github.com/creationix/nvm.git "$NVM_DIR" + cd "$NVM_DIR" + git checkout `git describe --abbrev=0 --tags --match "v[0-9]*" origin` + ) + source $NVM_DIR/nvm.sh + nvm install 7.5 + nvm alias default 7.5 + nvm use default + nvm exec npm install -g yarn + SHELL + + config.vm.provision "shell", privileged: false, inline: <<-SHELL + source /usr/local/nvm/nvm.sh + cd /vagrant + nvm use default + yarn install + npm start + SHELL + +end diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..dd9c89c --- /dev/null +++ b/circle.yml @@ -0,0 +1,11 @@ +machine: + services: + - docker + +dependencies: + override: + - echo "Ignore CircleCI detected dependencies" + +test: + override: + - docker build -t steemit/steemit.com . diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json new file mode 100644 index 0000000..c21eb08 --- /dev/null +++ b/config/custom-environment-variables.json @@ -0,0 +1,85 @@ +{ + "database_url": "SDC_DATABASE_URL", + "disable_signups": "SDC_DISABLE_SIGNUPS", + "facebook_app_id": "SDC_FACEBOOK_APP_ID", + "grant": { + "facebook": { + "key": "SDC_FB_API_KEY", + "secret": "SDC_FB_API_SECRET" + }, + "reddit": { + "key": "SDC_REDDIT_API_KEY", + "secret": "SDC_REDDIT_API_SECRET" + }, + "server": { + "host": "SDC_GRANT_SERVER", + "protocol": "SDC_GRANT_PROTOCOL" + } + }, + "google_analytics_id": "SDC_GOOGLE_ANALYTICS_ID", + "helmet": { + "directives": { + "childSrc": "SDC_HELMET_CHILDSRC", + "connectSrc": "SDC_HELMET_CONNECTSRC", + "defaultSrc": "SDC_HELMET_DEFAULTSRC", + "fontSrc": "SDC_HELMET_FONTSRC", + "frameAncestors": "SDC_HELMET_FRAMEANCESTORS", + "imgSrc": "SDC_HELMET_IMGSRC", + "objectSrc": "SDC_HELMET_OBJECTSRC", + "scriptSrc": "SDC_HELMET_SCRIPTSRC", + "styleSrc": "SDC_HELMET_STYLESRC", + "reportUri": "SDC_HELMET_REPORTURI" + } + }, + "img_proxy_prefix": "SDC_IMAGE_PROXY_PREFIX", + "mixpanel": "SDC_MIXPANEL", + "newrelic": "SDC_NEWRELIC_LICENSE_KEY", + "notify": { + "gcm_key": "SDC_NOTIFY_GCM_KEY" + }, + "read_only_mode": "SDC_READONLY_MODE", + "recaptcha": { + "secret_key": "SDC_RECAPTCHA_SECRET_KEY", + "site_key": "SDC_RECAPTCHA_SITE_KEY" + }, + "registrar": { + "account": "SDC_REGISTRAR_ACCOUNT", + "fee": "SDC_REGISTRAR_AMOUNT", + "signing_key": "SDC_REGISTRAR_SIGNINGKEY", + "delegation": "SDC_REGISTRAR_DELEGATION" + }, + "requestAccountRecovery": { + "recovery_account": "SDC_RECOVERY_ACCOUNT", + "signing_key": "SDC_RECOVERY_SIGNINGKEY" + }, + "sendgrid": { + "from": "SDC_SENDGRID_FROM", + "key": "SDC_SENDGRID_API_KEY", + "templates": { + "confirm_email": "SDC_SENDGRID_CONFIRMTEMPLATE", + "waiting_list_invite": "SDC_SENDGRID_WAITINGTEMPLATE" + } + }, + "server_session_secret": "SDC_SESSION_SECRETKEY", + "site_domain": "SDC_SITE_DOMAIN", + "tarantool": { + "host": "SDC_TARANTOOL_HOSTNAME", + "password": "SDC_TARANTOOL_PASSWORD", + "port": "SDC_TARANTOOL_PORT", + "username": "SDC_TARANTOOL_USERNAME" + }, + "telesign": { + "customer_id": "SDC_TELESIGN_CUSTOMER_ID", + "rest_api_key": "SDC_TELESIGN_API_KEY" + }, + "twilio": { + "account_sid": "SDC_TWILIO_ACCOUNT_SID", + "auth_token": "SDC_TWILIO_AUTH_TOKEN" + }, + "upload_image": "SDC_UPLOAD_IMAGE_URL", + "session_cookie_key": "SDC_SESSION_COOKIE_KEY", + "steemd_connection_client": "SDC_CLIENT_STEEMD_URL", + "steemd_connection_server": "SDC_SERVER_STEEMD_URL", + "chain_id": "SDC_CHAIN_ID", + "address_prefix": "SDC_ADDRESS_PREFIX" +} diff --git a/config/default.json b/config/default.json new file mode 100644 index 0000000..82b8d73 --- /dev/null +++ b/config/default.json @@ -0,0 +1,103 @@ +{ + "database_url": "mysql://root:password@127.0.0.1/steemit_dev", + "disable_signups": false, + "facebook_app_id": false, + "grant": { + "facebook": { + "callback": "/handle_facebook_callback", + "key": "", + "scope": [ + "email", + "user_location" + ], + "secret": "" + }, + "reddit": { + "callback": "/handle_reddit_callback", + "custom_params": { + "duration": "temporary", + "state": "void" + }, + "key": "", + "scope": [ + "identity" + ], + "secret": "" + }, + "server": { + "host": "localhost:3002", + "protocol": "http" + } + }, + "google_analytics_id": false, + "helmet": { + "directives": { + "childSrc": "'self' www.youtube.com staticxx.facebook.com w.soundcloud.com player.vimeo.com", + "connectSrc": "'self' steemit.com wss://steemd.steemit.com api.blocktrades.us", + "defaultSrc": "'self' www.youtube.com staticxx.facebook.com player.vimeo.com", + "fontSrc": "data: fonts.gstatic.com", + "frameAncestors": "'none'", + "imgSrc": "* data:", + "objectSrc": "'none'", + "pluginTypes": "application/pdf", + "scriptSrc": "'self' www.google-analytics.com connect.facebook.net", + "styleSrc": "'self' 'unsafe-inline' fonts.googleapis.com", + "reportUri": "/api/v1/csp_violation" + }, + "reportOnly": false, + "setAllHeaders": true + }, + "img_proxy_prefix": "https://steemitdevimages.com/", + "ipfs_prefix": false, + "mixpanel": false, + "newrelic": false, + "notify": { + "gcm_key": "google secret key" + }, + "read_only_mode": false, + "recaptcha": { + "secret_key": false, + "site_key": false + }, + "registrar": { + "account": "-", + "fee": "0.5 STEEM", + "delegation": "150250.000000 VESTS", + "signing_key": "5J..." + }, + "requestAccountRecovery": { + "recovery_account": "steem", + "signing_key": "5J..." + }, + "sendgrid": { + "from": "noreply@example.com", + "key": "SG.xxx_yyyy", + "templates": { + "confirm_email": false, + "waiting_list_invite": false + } + }, + "server_session_secret": "exiKdyF+IwRIXJDmtGIl4vWUz4i3eVSISpfZoeYc0s4=", + "session_cookie_key": "stm-dev", + "session_key": "steemses", + "site_domain": "steemitdev.com", + "tarantool": { + "host": "localhost", + "password": "", + "port": "3301", + "username": "guest" + }, + "telesign": { + "customer_id": false, + "rest_api_key": false + }, + "twilio": { + "account_sid": false, + "auth_token": false + }, + "upload_image": false, + "steemd_connection_client": "wss://steemd.steemit.com", + "steemd_connection_server": "wss://steemd.steemit.com", + "chain_id": "0000000000000000000000000000000000000000000000000000000000000000", + "address_prefix": "STM" +} diff --git a/config/production.json b/config/production.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/config/production.json @@ -0,0 +1 @@ +{} diff --git a/doc/CONTRIBUTORS.md b/doc/CONTRIBUTORS.md new file mode 100644 index 0000000..430f2ab --- /dev/null +++ b/doc/CONTRIBUTORS.md @@ -0,0 +1,18 @@ +This file contains a list of people who have made +large contributions to the Steemit.com codebase. + + + +(not complete) + + + +## Internationalization + - @Undeadlol1 (Mihail Paley) + - @ekitcho + - @heimindanger + - @fernando-sanz + - @jza + - @cheftony + - @jumpeiyamane + * Japanese diff --git a/doc/DEPLOYMENT.md b/doc/DEPLOYMENT.md new file mode 100644 index 0000000..c03b346 --- /dev/null +++ b/doc/DEPLOYMENT.md @@ -0,0 +1,19 @@ +# How To Deploy In Production + +## Front End + +* recommend fronting with a reverse proxy such as nginx + +## Rate Limiting + +* You will want to rate limit certain paths, likely to ~1r/s in your proxy + +``` +/api/v1/initiate_account_recovery +/api/v1/account_recovery_confirmation/:code +/api/v1/request_account_recovery +/api/v1/account_identity_providers +/api/v1/accounts +/api/v1/update_email +/api/v1/login_account +``` diff --git a/doc/FAQ.md b/doc/FAQ.md new file mode 100644 index 0000000..5e98e2e --- /dev/null +++ b/doc/FAQ.md @@ -0,0 +1,29 @@ +# steemit.com Repository FAQ + +# Golden Rules + +* The issue tracker is for bugs and specific implementation discussion. It + is not appropriate for product metadiscussion - it is a workroom, not a + cocktail party. + +* Not every feature that spawns a drama thread on steemit.com necessarily + warrants a commit/PR. Frequently, such threads occur even when nothing is + broken or when things are working as intended. Not all problems + encountered warrant technical solutions. + +* Issues opened that devolve into lengthy discussion of minor site features + will be closed or locked. The issue tracker is not a general purpose + discussion forum. + +* Product improvement suggestions are encouraged, however they should be + sent to [sneak@steemit.com](mailto:sneak@steemit.com) for review. + +* Working code (usually) trumps all. Requests to make changes to + steemit.com that include working, tested Pull Requests jump to the top of + the queue. (This is not a guarantee that all functionality submitted as a + PR will be merged, however.) + +* For many small changes, it may be easiest to send a Pull Request *instead* of + opening an issue requesting a change be made. Anyone with a GitHub + account can search the repo, edit a file in the browser, and send in a PR. + It's easier than you might think! diff --git a/doc/LICENSE.md b/doc/LICENSE.md new file mode 100644 index 0000000..c78b719 --- /dev/null +++ b/doc/LICENSE.md @@ -0,0 +1,7 @@ +Copyright (c) 2016 Steemit, Inc., and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/doc/release-notes.txt b/doc/release-notes.txt new file mode 100644 index 0000000..1f0aa3a --- /dev/null +++ b/doc/release-notes.txt @@ -0,0 +1,336 @@ +--------------------------------------------------------------------- +0.1.170324 +--------------------------------------------------------------------- + +New features +-------- + - use twilio to verify phone numbers #1232 + - show order ops in wallet #1144 + - switch to invisible recaptcha #1244 + - votes icon refresh #1110 + - add busy to external links #1117 + - simplified/refreshed post hero #1207 + - add post json routes #1122 + - markdown headings in sans #1184 + - add payout sorts #1137 + +Bug fixes +-------- + - replace usage of mode prop #1260 + - fix account recovery issue #1257 + - update login dialog titles, copy #1251 + - handle double-encoded json bug #1249 + - disable spellcheck on transfer #1248 + - use blue circle stroke on upvote #1242 + - show follow mute button for guests #1217 + - prevent anchor tags breaking #1218 + - support ctrl clicking on authors #1220 + - show apr in saving balance #1228 + - update README.md #1208 + - add TOC to FAQ Page #1106 + - conv tracking for banner signup #1183 + - no signup link authenticate #1176 + - post quote update #1198 + - add Banner & TOC to Welcome page #1130 + - fix depth check on Comments #1204 + - add commas to view count #1199 + - update CONTRIBUTING.md #1193 + - critical telesign bugfix #1191 + - add fb and ga config env #1185 + - update readme for tarantool #1142 + - hide signup prompt on active key login #1265 + - add app manifest #1266 + + +--------------------------------------------------------------------- +0.1.170216 +--------------------------------------------------------------------- + +New features +-------- + - markdown image upload #1026 + - create CONTRIBUTING.md #1141 + - dockerization, config module, node 7.5 #1108 + - add phist to alternative post viewers #1150 + - do not linkify exe and zip urls #1087 + +Bug fixes +-------- + - fetch more followers in batch #1097 + - fall back to rshares sort for comments #1099 + - adjust author heading #1101 + - move the "Steemit Chat" link higher in the main menu #1102 + - adjust graying and post collapsing rules #1124 + - move witness proxy to bottom of page #1104 + - restore page title when closing modal with ESC #1123 + - cleanup unused import, eslint #1125 + - improve byline handling in mobile view #1112 + - FollowSaga: fix ternary op, eslint #1111 + - update json profile #1120 + - rollback to loading fb plugin on page load #1151 + - disable autocorrect on wallet page #1143 + - address large svgs problem #1152 + - fix HtmlReady regression #1157 + - wrap transfer memo text properly #1156 + - fix invalid vote state bug #1154 + + +--------------------------------------------------------------------- +0.1.170201 +--------------------------------------------------------------------- + +New features +-------- + - show author's userpic below the title on post page #1051 + - smaller server response size & flag refactor #1054 + - remove flag count on posts #1039 + - post performance #1016 + - lighten post link color #1044 + - setting page consistency #1056 + - load fb sdk only when needed #1049 + - link to block explorer from post #1063, #1077 + - Parse metadata when returning user JSON #1037 + - use large twitter card, fallback to profile image #1053 + +Bug fixes +-------- + - fix web push notifications undefined url issue on Android #1079 + - fix scrollable close sign on post modal #1078 + - cannot read property bug #1060 + - fix Comments tab truncation #1085 + - use on chain account reg fee instead of config if larger #1069 + - fix upvote slider positioning on mobile & state #1068 + - capitalized steem verbiage for consistency #1048 + + +--------------------------------------------------------------------- +0.1.170121 +--------------------------------------------------------------------- + +New features +-------- + - Add plain description meta tag #982 + - Remove filtering of trending options for time being #983 + - Do not confirm clearing of reply form when body is empty #985 + - Meta description should be name, not property #989 + - Show Author dropdown for non-logged in users #991 + - Adjust rephide -- add threshold #1010 + - Update profile header for semantic upgrade, adjust styling to match old #1001 + - #844 protect for invalid user pages allow for short post urls #1011 + - 958 web push notifications #1007 + - Add profile pics support to notifications #1017 + - Support blacklisted phone prefixes #1015 + +Bug fixes +-------- + - Save json_metadata youtube preview image url for iframe embedds. fix #995 + - Clarify wording of wallet actions, fix #990 + - Usernames element for reblogged_by array compat #994 + + +--------------------------------------------------------------------- +0.1.161261 +--------------------------------------------------------------------- + +New features +-------- + - Strict checking of profile url #979 + - TagsIndex revamp #806 + - Parse block quote from text if preview text is rendered as comment #955 + - Prepend nested comments with dots #942 + - Welcome Page Copyedit Changes for #923 + - Update default wallet estimated balance tip message, update en and ru… #921 + - Add a last-minute check to ensure one phone per account #917 + - Add 3x support for thumbnail previews for post previews to remove fuz… #940 + - 914 user thumbnails #934 + - 915 resteem button feeds mobile #937 + - Add metadata to nsfw post summaries #956 + - Update verbiage for flag tooltip for distribution/rewards, update loc… #957 + +Bug fixes +-------- + - Fix phone verification issue #922 + - Fix translation issue on account recovery page #908 + - Patch route user #883 + - Fix account page 500 error #897 + - Comment edit bug 895 #954 + - Confirm clearing of post form. fix #970 + + +--------------------------------------------------------------------- +0.1.161221 +--------------------------------------------------------------------- + +New features +-------- + - Mixpanel Social Sharing Tracking #881 + - Json route user #869 + - nsfw handling #861 + - ability to sort comments by upvotes #808 + - username routes 404 #851 + - utilize new get follow count #845 + - update react & babel #843 + - never hide flags #858 + - allow refetching of pages #593 + - make cookie name configurable #852 + +Bug fixes +-------- + - Patch route user #883 + - Increase nodejs framesize #873 + - should fix some inconsistent post overlay scrolling behavior #863 + - mark fsevents as optional dep (fixes linux instal issue with npm ~4.0.3) #850 + - phone is considered to be used only if there is account created with it #841 + - username should appear in header prefixed with #855 + - typesafe json metadata #868 + - Refactor class name for best practice conventional naming #871 + + +--------------------------------------------------------------------- +0.1.161214 +--------------------------------------------------------------------- + + - ability to set witness proxy voter #797 + - add muted user list on Settings page #834 + - add zebra stripes to wallet page #818 + - allow votes dropdown to expand past post modal #836 + - always show comment collapser #835 + - fix phone verification issue #839 + - clarify power down error message #838 + - disable follow buttons while pending #832 + - translation of markets #604 + - mixpanel - track more events #828 + - add contributors.md #825 + - support for secure server sessions #823 + - fix post footer promo text #822 + - translation of blocktrades deposit #821 + - display pending conversions & add them to account value #804 + - fix follow counts #802 + - fix unknown account flashing #801 + - login/transfer updates, autofill username #798 + - prevent post footer wrapping #781 + + +--------------------------------------------------------------------- +0.1.161205 +--------------------------------------------------------------------- + + - proper inflection for vote count #794 + - update econ rules copy #793 + - remove high security key in overlay #791 + - @author/permlink redirect #786 + - normalize profile url #785 + - enforce display names not starting with `@` #780 + - show 'since date' for view counts on old posts #779 + - handle off screen click on resteem confirm #778 + - revert youtube previews to lower resolution #777 + - remove 0.15.0 compat layer, re-enable Promoted posts #776 + - refactor follow data structure #774 + - fix prop warnings - npm cleanup #773 + - fix potential firefox bug - not able to scroll a post #767 + - refactoring of market state #758 + + +--------------------------------------------------------------------- +0.1.161202 +--------------------------------------------------------------------- + +New features +-------- + - views counter #744 + - profile customization #737 + - full power badge #748 + - add current open orders to wallet balances #740 + +Bug fixes +-------- + - various market bug fixes and price warning #728 + - performance tweaks: minimize rendering and API calls #738 + - fix witness votes not appearing for logged in user #741 + - add support for vimeo auto embed #731 + - fix obscure bug which causes certain keys to trigger back event #754 + - fix follow mute button alignment for mobile display #753 + - do not show dropdown for comments with 0 votes #747 + - fix bug preventing declined payout post from being edited #743 + - handle malformed categories in url #742 + - fix share menu scrolling behavior #739 + - adjust password data-entry error wording #736 + - clarify dangerous-html flag usage #733 + - remove fastclick for JS dropdown conflicts #727 + - allow links to open in new tab without closing menu #726 + - add padding for avatar on collapsed state #717 + - display previous title when closing post modal #709 + - remove negative top margin on comment footer #714 + + +--------------------------------------------------------------------- +0.1.161123 +--------------------------------------------------------------------- + + - Add welcome page #585 (@timcliff, @bitcoiner) + - Fix joined date on mobile #629 (@bitcoiner) + - Add a "settings" link to the drop-down menu #618 (@bitcoiner) + - Fix wallet UI glitches on mobile #633 (@bitcoiner) + - Hide follow counts until loaded #632 + - Hamburger menu clarifications #635 (@bitcoiner) + - Add support for renamed API key #637 + - Do not hide dropdown menu on ctrl-click #641 + - Strikethrough payout amount for declined payout posts #644 + - Fix reputation (float) bug #643 + - i18n: fix reply count var and singular counts #649 + - Better support for non-lowercase tags and mentions #659 + - Fix showing of category error #659 + - Fix auto-vote + decline payout bug #650 + - Remove dup exports #657 + - Update follows api logic with backwards compat for share-db upgrade #669 + - Fix react-addons-perf and update shrinkwrap #670 + - Ensure lowercase user names for page titles #661 + - Fix comment sort order label #671 + - Properly handle relative links #603 + - Support for new tags and tag_idx state #689 + - Fix multiple account creation per verification issue #692 + - Cleanup & i18n for awards pages #658 (@bitcoiner) + - Replace showSignUp with redirect to sign up's first step #694 + - Fix follow loading status, invert shared-db follows api fix #695 + - Replace showSignUp with redirect to sign up's first step #694 + - Allow email verification resend if expired #691 + - Fix sign up issue that could allow attacker to create up to 8 accounts per single verification #625 + + +--------------------------------------------------------------------- +0.1.161109 +--------------------------------------------------------------------- + +New features and improvements +-------- +- custom user profile images +- show dynamic sbd interest in user's wallet +- new submenu on the wallet page + +Bug fixes +-------- +- remove estimates, just show 7-day summary #600 +- own reply notifications appear on other users' account pages #595 + + +--------------------------------------------------------------------- +0.1.161104 +--------------------------------------------------------------------- + +New features and improvements +-------- +- In app notifications #584 +- New confirmation dialog for resteeming action #572 +- New Profile display user join steem date #582 +- New hyperlink on post timestamp for content +- Inclusive of Steemit API docs sub menu +- Youtube preview improvements #588 + +Bug fixes +-------- +- fixes for user wallet views/actions #528 +- fixes rewards balances #528 +- TypeError: Cannot read property 'get' of undefined #238 +- remove ReplyEditor__title padding #570 +- Show message when no post results #571 +- do not display invalid cashout_time #532 diff --git a/mocha.setup.js b/mocha.setup.js new file mode 100644 index 0000000..86fce39 --- /dev/null +++ b/mocha.setup.js @@ -0,0 +1,30 @@ +require('babel-register')(); + +process.env.NODE_PATH = require('path').resolve(__dirname, './src'); +require('module').Module._initPaths(); + +const jsdom = require('jsdom').jsdom; + +const exposedProperties = ['window', 'navigator', 'document']; + +global.document = jsdom(''); +global.window = document.defaultView; +Object.keys(document.defaultView).forEach((property) => { + if (typeof global[property] === 'undefined') { + exposedProperties.push(property); + global[property] = document.defaultView[property]; + } +}); + +global.navigator = { + userAgent: 'node.js' +}; + +documentRef = document; + +function donothing() { + return null; +} +require.extensions['.svg'] = donothing; +require.extensions['.css'] = donothing; +require.extensions['.scss'] = donothing; diff --git a/package.json b/package.json new file mode 100644 index 0000000..d07e6ad --- /dev/null +++ b/package.json @@ -0,0 +1,193 @@ +{ + "name": "steemit.com", + "repository": { + "type": "git", + "url": "https://github.com/steemit/steemit.com.git" + }, + "version": "1.0.0", + "description": "steemit.com is the koa web server & middleware and react.js in-browser code for the world's first blockchain content + social media monetization platform!", + "main": "index.js", + "scripts": { + "build": "NODE_ENV=production ./node_modules/babel-cli/bin/babel-node.js ./node_modules/.bin/webpack --config ./webpack/prod.config.js; rm -rf ./lib; NODE_ENV=production babel --plugins transform-runtime,transform-inline-environment-variables src --out-dir lib -Dq", + "mocha": "NODE_ENV=test mocha ./mocha.setup.js", + "test": "npm run mocha -- src/app/**/*.test.js src/shared/**/*.test.js", + "test:watch:all": "npm test -- --watch --watch-extensions jsx", + "test:watch": "npm run mocha -- --watch --watch-extensions jsx", + "eslint": "LIST=`git diff-index --name-only HEAD | grep .*\\.js | grep -v json`; if [ \"$LIST\" ]; then eslint $LIST; fi", + "production": "NODE_ENV=production node lib/server/index.js", + "start": "NODE_ENV=development ./node_modules/babel-cli/bin/babel-node.js ./webpack/dev-server.js", + "webpush": "./node_modules/babel-cli/bin/babel-node.js ./scripts/webpush_notify.js", + "checktranslations": "node scripts/check_translations.js" + }, + "author": "Steemit, Inc.", + "license": "MIT", + "dependencies": { + "@steem/crypto-session": "git+https://github.com/steemit/crypto-session.git#83a90b319ce5bc6a70362d52a15a815de7e729bb", + "assert": "^1.3.0", + "autoprefixer-loader": "^3.2.0", + "babel-cli": "^6.22.2", + "babel-core": "^6.20.0", + "babel-eslint": "^6.0.4", + "babel-loader": "^7.1.2", + "babel-plugin-react-intl": "^2.2.0", + "babel-plugin-transform-decorators-legacy": "^1.3.4", + "babel-plugin-transform-inline-environment-variables": "^0.2.0", + "babel-plugin-transform-runtime": "^6.15.0", + "babel-preset-es2015": "^6.18.0", + "babel-preset-react": "^6.16.0", + "babel-preset-stage-0": "^6.16.0", + "bigi": "^1.4.1", + "blocked": "^1.1.0", + "bs58": "^3.0.0", + "bytebuffer": "^5.0.0", + "bytes": "^2.4.0", + "classnames": "^2.2.5", + "cluster": "^0.7.7", + "co-body": "^4.2.0", + "config": "^1.25.1", + "counterpart": "^0.17.6", + "cpu-stat": "^2.0.1", + "css-loader": "^0.28.5", + "currency-symbol-map": "^3.1.0", + "diff-match-patch": "^1.0.0", + "disk-stat": "^1.0.4", + "ecurve": "^1.0.2", + "estraverse-fb": "^1.3.1", + "file-loader": "^0.11.2", + "foundation-sites": "git+https://github.com/steemit/foundation-sites.git#e8e32c715bbc4c822b80b555345f61337269ca78", + "git-rev-sync": "^1.9.1", + "grant-koa": "^3.6.0", + "highcharts": "^4.2.5", + "humanize-number": "0.0.2", + "imports-loader": "^0.7.1", + "intl": "^1.2.5", + "iso": "^5.1.0", + "json-loader": "^0.5.7", + "koa": "^1.1.2", + "koa-body": "^1.4.0", + "koa-compressor": "^1.0.3", + "koa-conditional-get": "^1.0.3", + "koa-csrf": "^2.5.0", + "koa-etag": "^2.0.0", + "koa-favicon": "^1.2.0", + "koa-flash": "^1.0.0", + "koa-helmet": "^1.0.0", + "koa-isbot": "^0.1.1", + "koa-locale": "^1.3.0", + "koa-logger": "^1.3.0", + "koa-mount": "^1.3.0", + "koa-proxy": "^0.5.0", + "koa-route": "^2.4.2", + "koa-router": "^5.4.0", + "koa-session": "^3.3.1", + "koa-static-cache": "^3.1.2", + "lodash.debounce": "^4.0.7", + "medium-editor-insert-plugin": "^2.3.2", + "mem-stat": "^1.0.5", + "minimist": "^1.2.0", + "mixpanel": "^0.5.0", + "mysql": "^2.10.2", + "net": "^1.0.2", + "node-sass": "^4.5.3", + "os": "^0.1.1", + "picturefill": "^3.0.2", + "purest": "^2.0.1", + "raw-loader": "^0.5.1", + "react": "15.4.2", + "react-ab-test": "^1.7.0", + "react-addons-pure-render-mixin": "15.4.2", + "react-copy-to-clipboard": "^4.2.3", + "react-dom": "15.4.2", + "react-dropzone": "^3.7.3", + "react-foundation-components": "git+https://github.com/valzav/react-foundation-components.git#d14362c7c8eee946a4acc3b18d70271d5a82813e", + "react-highcharts": "^8.3.3", + "react-intl": "^2.1.3", + "react-medium-editor": "^1.8.0", + "react-notification": "^5.0.7", + "react-overlays": "^0.7.0", + "react-portal": "^2.2.1", + "react-prop-types": "^0.3.0", + "react-qr": "0.0.2", + "react-rangeslider": "1.0.3", + "react-redux": "^5.0.6", + "react-router": "^3.0.5", + "react-router-redux": "^4.0.0", + "react-router-scroll": "^0.4.2", + "react-rte-image": "^0.3.1", + "react-timeago": "^3.1.2", + "redux": "^3.3.1", + "redux-form": "5.3.4", + "redux-modules": "0.0.5", + "redux-saga": "^0.9.5", + "remarkable": "^1.7.1", + "sanitize-html": "^1.11.4", + "sass-loader": "^6.0.6", + "secure-random": "^1.1.1", + "sendgrid": "^4.0.1", + "sequelize": "^3.21.0", + "sequelize-cli": "^2.3.1", + "speakingurl": "^9.0.0", + "sqlite3": "^3.1.8", + "steem": "github:steemit/steem-js#72a36c835a74f446da47e36feea5d5f1f4dde55b", + "store": "^1.3.20", + "style-loader": "^0.18.2", + "svg-inline-loader": "^0.8.0", + "svg-inline-react": "^1.0.2", + "svgo-loader": "^1.2.1", + "tarantool-driver": "^2.0.1", + "twilio": "^2.11.1", + "uncontrollable": "^3.2.1", + "underscore.string": "^3.2.3", + "url-loader": "^0.5.9", + "web-push": "^3.2.1", + "webpack": "^3.5.5", + "webpack-dev-middleware": "^1.12.0", + "webpack-isomorphic-tools": "^3.0.3", + "websocket": "^1.0.22", + "whatwg-fetch": "^0.11.1", + "xmldom": "^0.1.22" + }, + "devDependencies": { + "babel-plugin-transform-decorators-legacy": "^1.3.4", + "chai": "^3.5.0", + "chai-immutable": "^1.5.3", + "co-mocha": "^1.1.2", + "co-supertest": "0.0.10", + "dev-ip": "^1.0.1", + "dirty-chai": "^1.2.2", + "enzyme": "^2.1.0", + "escope": "^3.6.0", + "eslint": "^4.7.0", + "eslint-config-airbnb": "^15.1.0", + "eslint-plugin-babel": "^4.1.2", + "eslint-plugin-import": "^2.7.0", + "eslint-plugin-jsx-a11y": "^6.0.2", + "eslint-plugin-react": "^7.4.0", + "extract-text-webpack-plugin": "^3.0.0", + "jsdom": "^9.8.0", + "koa-webpack-dev-middleware": "^1.1.0", + "koa-webpack-hot-middleware": "^1.0.3", + "mocha": "^2.4.5", + "node-watch": "^0.5.5", + "pre-commit": "^1.2.2", + "react-addons-perf": "15.4.2", + "react-addons-test-utils": "15.4.2", + "react-transform-catch-errors": "^1.0.1", + "react-transform-hmr": "^1.0.4", + "sinon": "^1.17.3", + "sinon-chai": "^2.8.0", + "supertest": "^1.2.0", + "webpack-bundle-analyzer": "^2.9.0" + }, + "optionalDependencies": { + "fsevents": "*" + }, + "engines": { + "node": ">=7.0.0", + "npm": ">=5.4.2" + }, + "pre-commit": [ + "eslint", + "checktranslations" + ] +} diff --git a/scripts/check_translations.js b/scripts/check_translations.js new file mode 100644 index 0000000..15b7a47 --- /dev/null +++ b/scripts/check_translations.js @@ -0,0 +1,101 @@ +/* eslint guard-for-in: 0 */ +/* eslint no-restricted-syntax: 0 */ + +const fs = require('fs'); + +function jsonToKeys(keys, prefix, json) { + if (typeof json === 'object') { + if (json.one && json.other) { + keys[prefix] = true; + return; + } + for (const k in json) { + const new_prefix = prefix ? prefix + '.' + k : k; + jsonToKeys(keys, new_prefix, json[k]); + } + return; + } + if (keys[prefix]) throw new Error('Duplicate translation: ' + prefix); + keys[prefix] = true; +} + +function readTranslationKeys(path) { + const data = fs.readFileSync(path, 'utf8'); + const json = JSON.parse(data); + const keys = {}; + jsonToKeys(keys, null, json); + return keys; +} + +function loadTranslationFiles(path) { + const args = process.argv.slice(2); + const translations = {}; + const files = fs.readdirSync(path); + for (const filename of files) { + if (args.length <= 0 || filename === args[0]) { + const m = filename.match(/([\w-]+)\.json$/); + if (m) { + const lang = m[1]; + translations[lang] = readTranslationKeys(path + '/' + filename); + } + } + } + return translations; +} + +function processFile(used_keys, path) { + const lines = fs.readFileSync(path, 'utf8').split(/\r?\n/); + for (const l of lines) { + const tts = l.match(/(tt\(["'.\-_\w]+)/g) || l.match(/(FormattedHTMLMessage.+id=["'.\-_\w]+)/g); + if (tts) { + // if(tts.length > 1) console.log('-- tt -->', path, l, tts.length, JSON.stringify(tts, null, 4)); + for (const t of tts) { + if (t !== 'tt(id') { + const m = t.match(/tt\(['"]([.\-_\w]+)/) || t.match(/id=['"]([.\-_\w]+)['"]/); + if (!m) throw new Error('Wrong format: "' + t + '" in "' + l + '"'); + const key = m[1]; + if (used_keys[key]) used_keys[key] += 1; + else used_keys[key] = 1; + } + } + } + } +} + +function processDir(path, used_keys = {}) { + const files = fs.readdirSync(path); + for (const filename of files) { + const newpath = path + '/' + filename; + const stat = fs.statSync(newpath); + if (stat.isDirectory()) processDir(newpath, used_keys); + else if (filename.match(/\.jsx?$/)) { + processFile(used_keys, newpath); + } + } + return used_keys; +} + +function checkKeys(translations, used_keys) { + let errors_counter = 0; + for (const lang in translations) { + const lang_keys = translations[lang]; + for (const key in used_keys) { + if (!lang_keys[key]) { + console.warn('Translation key not found: ', lang, key); + errors_counter += 1; + } + } + for (const key in lang_keys) { + if (!used_keys[key]) { + console.warn('Unused translation: ', lang, key); + errors_counter += 1; + } + } + } + return errors_counter; +} + +const translations = loadTranslationFiles('src/app/locales'); +const used_keys = processDir('src'); +const errors_counter = checkKeys(translations, used_keys); +process.exit(errors_counter > 0 ? 1 : 0); diff --git a/scripts/send_waiting_list_invites.js b/scripts/send_waiting_list_invites.js new file mode 100644 index 0000000..75d586b --- /dev/null +++ b/scripts/send_waiting_list_invites.js @@ -0,0 +1,36 @@ +import models from '../db/models'; +import sendEmail from '../server/sendEmail'; +import secureRandom from 'secure-random' + +function inviteUser(u, email, number) { + const confirmation_code = secureRandom.randomBuffer(13).toString('hex'); + console.log(`\n***** invite #${number} ***** `, u.id, email, confirmation_code); + const i_attrs = { + provider: 'email', + user_id: u.id, + email, + verified: false, + confirmation_code + }; + models.Identity.create(i_attrs).then(() => { + sendEmail('waiting_list_invite', 'to@example.com', {confirmation_code}, 'from@example.com'); + }); +} + +models.User.findAll({ + attributes: ['id', 'email'], + where: {waiting_list: true, email: {$ne: null}, id: {$gt: 0}}, + order: 'id', + limit: 1000 +}).then(users => { + let counter = 1; + for(let u of users) { + const email = u.email.toLowerCase(); + if (email.match(/\@qq\.com$/)) continue; + const m = email.match(/\.(\w+)$/); + if (!m || m[1] === 'ru') continue; + const number = counter; + setTimeout(() => inviteUser(u, email, number), counter * 1000); + counter += 1; + } +}); diff --git a/scripts/webpush_notify.js b/scripts/webpush_notify.js new file mode 100644 index 0000000..7d459fb --- /dev/null +++ b/scripts/webpush_notify.js @@ -0,0 +1,54 @@ +import config from 'config'; +import webPush from 'web-push'; +import Tarantool from '../src/db/tarantool'; + +webPush.setGCMAPIKey(config.get('notify.gcm_key')); + +function notify(account, nparams, title, body, url, pic) { + if (!nparams.keys || !nparams.keys.auth) return Promise.resolve(false); + var payload = JSON.stringify({ + title, + body, + url, + icon: pic || 'https://steemit.com/favicon.ico' //FIXME domain name from config + }); + return new Promise((resolve, reject) => { + webPush.sendNotification(nparams, payload).then(function() { + resolve(account); + }, function(err) { + reject(err); + }); + }); +} + +async function process_queue() { + try { + const queue = await Tarantool.instance().call('webpush_get_delivery_queue'); + console.log('processing web push notifications queue, length: ', queue.length); + for (const n of queue) { + if (n.length === 0) return; + const [account, nparams_array, title, body, url, pic] = n; + console.log('notification: ', account, body, url, pic); + for (const nparams of nparams_array) { + try { + await notify(account, nparams, title, body, url, pic); + } catch (err) { + console.error('-- error in notify -->', account, nparams, err); + if (err.statusCode && err.statusCode == 410) { + await Tarantool.instance().call('webpush_unsubscribe', account, nparams.keys.auth); + } + } + } + } + } catch (error) { + console.error('-- process_queue error -->', error); + } +} + +function run() { + process_queue().then(() => { + setTimeout(run, 30000); + }); +} + +run(); diff --git a/src/app/Main.js b/src/app/Main.js new file mode 100644 index 0000000..7f312f3 --- /dev/null +++ b/src/app/Main.js @@ -0,0 +1,112 @@ +import 'babel-core/register'; +import 'babel-polyfill'; +import 'whatwg-fetch'; +import './assets/stylesheets/app.scss'; +import plugins from 'app/utils/JsPlugins'; +import Iso from 'iso'; +import universalRender from 'shared/UniversalRender'; +import ConsoleExports from './utils/ConsoleExports'; +import {serverApiRecordEvent} from 'app/utils/ServerApiClient'; +import * as steem from 'steem'; + +window.onerror = error => { + if (window.$STM_csrf) serverApiRecordEvent('client_error', error); +}; + +const CMD_LOG_T = 'log-t' +const CMD_LOG_TOGGLE = 'log-toggle' +const CMD_LOG_O = 'log-on' + +try { + if(process.env.NODE_ENV === 'development') { + // Adds some object refs to the global window object + ConsoleExports.init(window) + } +} catch (e) { + console.error(e) +} + +function runApp(initial_state) { + console.log('Initial state', initial_state); + const konami = { + code: 'xyzzy', + enabled: false + }; + const buff = konami.code.split(''); + const cmd = (command) => { + console.log('got command:' + command); + switch (command) { + case CMD_LOG_O : + konami.enabled = false; + case CMD_LOG_TOGGLE : + case CMD_LOG_T : + konami.enabled = !konami.enabled; + if(konami.enabled) { + steem.api.setOptions({logger: console}); + } else { + steem.api.setOptions({logger: false}); + } + return 'api logging ' + konami.enabled; + default : + return 'That command is not supported.'; + } + //return 'done'; + } + + const enableKonami = () => { + if(!window.s) { + console.log('The cupie doll is yours.'); + window.s = (command) => { return cmd.call(this, command) }; + } + } + + window.document.body.onkeypress = (e) => { + buff.shift() + buff.push(e.key) + if(buff.join('') === konami.code) { + enableKonami(); + cmd(CMD_LOG_T) + } + }; + + if(window.location.hash.indexOf('#'+konami.code) === 0) { + enableKonami() + cmd(CMD_LOG_O) + } + + const config = initial_state.offchain.config + steem.api.setOptions({ url: config.steemd_connection_client }); + steem.config.set('address_prefix', config.address_prefix); + steem.config.set('chain_id', config.chain_id); + window.$STM_Config = config; + plugins(config); + if (initial_state.offchain.serverBusy) { + window.$STM_ServerBusy = true; + } + if (initial_state.offchain.csrf) { + window.$STM_csrf = initial_state.offchain.csrf; + delete initial_state.offchain.csrf; + } + + const location = `${window.location.pathname}${window.location.search}${window.location.hash}`; + universalRender({history, location, initial_state}) + .catch(error => { + console.error(error); + serverApiRecordEvent('client_error', error); + }); +} + +if (!window.Intl) { + require.ensure(['intl/dist/Intl'], (require) => { + window.IntlPolyfill = window.Intl = require('intl/dist/Intl'); + require('intl/locale-data/jsonp/en-US.js'); + require('intl/locale-data/jsonp/es.js'); + require('intl/locale-data/jsonp/ru.js'); + require('intl/locale-data/jsonp/fr.js'); + require('intl/locale-data/jsonp/it.js'); + Iso.bootstrap(runApp); + }, "IntlBundle"); +} +else { + Iso.bootstrap(runApp); +} diff --git a/src/app/ResolveRoute.js b/src/app/ResolveRoute.js new file mode 100644 index 0000000..0168962 --- /dev/null +++ b/src/app/ResolveRoute.js @@ -0,0 +1,101 @@ +export const routeRegex = { + PostsIndex: /^\/(@[\w\.\d-]+)\/feed\/?$/, + UserProfile1: /^\/(@[\w\.\d-]+)\/?$/, + UserProfile2: /^\/(@[\w\.\d-]+)\/(blog|posts|comments|recommended|transfers|curation-rewards|author-rewards|permissions|created|recent-replies|feed|password|followed|followers|settings)\/?$/, + UserProfile3: /^\/(@[\w\.\d-]+)\/[\w\.\d-]+/, + UserEndPoints: /^(blog|posts|comments|recommended|transfers|curation-rewards|author-rewards|permissions|created|recent-replies|feed|password|followed|followers|settings)$/, + CategoryFilters: /^\/(hot|votes|responses|trending|trending30|promoted|cashout|payout|payout_comments|created|active)\/?$/ig, + PostNoCategory: /^\/(@[\w\.\d-]+)\/([\w\d-]+)/, + Post: /^\/([\w\d\-\/]+)\/(\@[\w\d\.-]+)\/([\w\d-]+)\/?($|\?)/, + PostJson: /^\/([\w\d\-\/]+)\/(\@[\w\d\.-]+)\/([\w\d-]+)(\.json)$/, + UserJson: /^\/(@[\w\.\d-]+)(\.json)$/, + UserNameJson: /^.*(?=(\.json))/, +}; + +export default function resolveRoute(path) +{ + if (path === '/') { + return {page: 'PostsIndex', params: ['trending']}; + } + if (path === '/about.html') { + return {page: 'About'}; + } + if (path === '/welcome') { + return {page: 'Welcome'}; + } + if (path === '/faq.html') { + return {page: 'Faq'}; + } + if (path === '/login.html') { + return {page: 'Login'}; + } + if (path === '/privacy.html') { + return {page: 'Privacy'}; + } + if (path === '/support.html') { + return {page: 'Support'}; + } + if (path === '/xss/test' && process.env.NODE_ENV === 'development') { + return {page: 'XSSTest'}; + } + if (path.match(/^\/tags\/?/)) { + return {page: 'Tags'}; + } + if (path === '/tos.html') { + return {page: 'Tos'}; + } + if (path === '/change_password') { + return {page: 'ChangePassword'}; + } + if (path === '/create_account') { + return {page: 'CreateAccount'}; + } + if (path === '/approval') { + return {page: 'Approval'}; + } + if (path === '/pick_account') { + return {page: 'PickAccount'}; + } + if (path === '/recover_account_step_1') { + return {page: 'RecoverAccountStep1'}; + } + if (path === '/recover_account_step_2') { + return {page: 'RecoverAccountStep2'}; + } + if (path === '/waiting_list.html') { + return {page: 'WaitingList'}; + } + if (path === '/market') { + return {page: 'Market'}; + } + if (path === '/~witnesses') { + return {page: 'Witnesses'}; + } + if (path === '/submit.html') { + return {page: 'SubmitPost'}; + } + let match = path.match(routeRegex.PostsIndex); + if (match) { + return {page: 'PostsIndex', params: ['home', match[1]]}; + } + match = path.match(routeRegex.UserProfile1) || + // @user/"posts" is deprecated in favor of "comments" as of oct-2016 (#443) + path.match(routeRegex.UserProfile2); + if (match) { + return {page: 'UserProfile', params: match.slice(1)}; + } + match = path.match(routeRegex.PostNoCategory); + if (match) { + return {page: 'PostNoCategory', params: match.slice(1)}; + } + match = path.match(routeRegex.Post); + if (match) { + return {page: 'Post', params: match.slice(1)}; + } + match = path.match(/^\/(hot|votes|responses|trending|trending30|promoted|cashout|payout|payout_comments|created|active)\/?$/) + || path.match(/^\/(hot|votes|responses|trending|trending30|promoted|cashout|payout|payout_comments|created|active)\/([\w\d-]+)\/?$/); + if (match) { + return {page: 'PostsIndex', params: match.slice(1)}; + } + return {page: 'NotFound'}; +} diff --git a/src/app/RootRoute.js b/src/app/RootRoute.js new file mode 100644 index 0000000..2fc6380 --- /dev/null +++ b/src/app/RootRoute.js @@ -0,0 +1,117 @@ +import App from 'app/components/App'; +import PostsIndex from 'app/components/pages/PostsIndex'; +import resolveRoute from './ResolveRoute'; + +// polyfill webpack require.ensure +if (typeof require.ensure !== 'function') require.ensure = (d, c) => c(require); + +export default { + path: '/', + component: App, + getChildRoutes(nextState, cb) { + const route = resolveRoute(nextState.location.pathname); + if (route.page === 'About') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/About')]); + //}); + } else if (route.page === 'Welcome') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/Welcome')]); + //}); + } else if (route.page === 'Faq') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/Faq')]); + //}); + } else if (route.page === 'Login') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/Login')]); + //}); + } else if (route.page === 'Privacy') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/Privacy')]); + //}); + } else if (route.page === 'Support') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/Support')]); + //}); + } else if (route.page === 'XSSTest' && process.env.NODE_ENV === 'development') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/XSS')]); + //}); + } else if (route.page === 'Tags') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/TagsIndex')]); + //}); + } else if (route.page === 'Tos') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/Tos')]); + //}); + } else if (route.page === 'ChangePassword') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/ChangePasswordPage')]); + //}); + } else if (route.page === 'PickAccount') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/PickAccount')]); + //}); + } else if (route.page === 'CreateAccount') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/CreateAccount')]); + //}); + } else if (route.page === 'Approval') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/Approval')]); + //}); + } else if (route.page === 'RecoverAccountStep1') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/RecoverAccountStep1')]); + //}); + } else if (route.page === 'RecoverAccountStep2') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/RecoverAccountStep2')]); + //}); + } else if (route.page === 'WaitingList') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/WaitingList')]); + //}); + } else if (route.page === 'Witnesses') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/Witnesses')]); + //}); + } else if (route.page === 'SubmitPost') { + if (process.env.BROWSER) { + // require.ensure([], (require) => { + cb(null, [require('app/components/pages/SubmitPost')]); + // }); + } else { + cb(null, [require('app/components/pages/SubmitPostServerRender')]); + } + } else if (route.page === 'UserProfile') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/UserProfile')]); + //}); + } else if (route.page === 'Market') { + require.ensure([], (require) => { + cb(null, [require('app/components/pages/Market')]); + }); + } else if (route.page === 'Post') { + //require.ensure([], (require) => { + cb(null, [require('app/components/pages/PostPage')]); + //}); + } else if (route.page === 'PostNoCategory') { + cb(null, [require('app/components/pages/PostPageNoCategory')]); + } else if (route.page === 'PostsIndex') { + //require.ensure([], (require) => { + //cb(null, [require('app/components/pages/PostsIndex')]); + cb(null, [PostsIndex]); + //}); + } else { + //require.ensure([], (require) => { + cb(process.env.BROWSER ? null : Error(404), [require('app/components/pages/NotFound')]); + //}); + } + }, + indexRoute: { + component: PostsIndex.component + } +}; diff --git a/src/app/Translator.js b/src/app/Translator.js new file mode 100644 index 0000000..61ca4eb --- /dev/null +++ b/src/app/Translator.js @@ -0,0 +1,58 @@ +import React from 'react'; +import {connect} from 'react-redux' +import {IntlProvider, addLocaleData} from 'react-intl'; +import en from 'react-intl/locale-data/en'; +import es from 'react-intl/locale-data/es'; +import ru from 'react-intl/locale-data/ru'; +import fr from 'react-intl/locale-data/fr'; +import it from 'react-intl/locale-data/it'; +import {DEFAULT_LANGUAGE} from 'app/client_config'; +import tt from 'counterpart'; + +addLocaleData([...en, ...es, ...ru, ...fr, ...it]); + +tt.registerTranslations('en', require('counterpart/locales/en')); +tt.registerTranslations('en', require('app/locales/en.json')); + +tt.registerTranslations('es', require('app/locales/counterpart/es')); +tt.registerTranslations('es', require('app/locales/es.json')); + +tt.registerTranslations('ru', require('counterpart/locales/ru')); +tt.registerTranslations('ru', require('app/locales/ru.json')); + +tt.registerTranslations('fr', require('app/locales/counterpart/fr')); +tt.registerTranslations('fr', require('app/locales/fr.json')); + +tt.registerTranslations('it', require('app/locales/counterpart/it')); +tt.registerTranslations('it', require('app/locales/it.json')); + +if (process.env.NODE_ENV === 'production') { +tt.setFallbackLocale('en'); +} + +class Translator extends React.Component { + render() { + const language = this.props.locale; + tt.setLocale(language); + return + {this.props.children} + + } +} + +export default connect( + (state, ownProps) => { + const locale = state.app.getIn(['user_preferences', 'locale']); + return {...ownProps, locale}; + } +)(Translator); + +export const FormattedHTMLMessage = ({id, params, className}) => ( +
+); diff --git a/src/app/assets/icons/100.svg b/src/app/assets/icons/100.svg new file mode 100644 index 0000000..15a0cfb --- /dev/null +++ b/src/app/assets/icons/100.svg @@ -0,0 +1,6 @@ + + 100% powered up post icon + + + + \ No newline at end of file diff --git a/src/app/assets/icons/bitcoin.svg b/src/app/assets/icons/bitcoin.svg new file mode 100644 index 0000000..c9b8cbd --- /dev/null +++ b/src/app/assets/icons/bitcoin.svg @@ -0,0 +1,23 @@ + + + + bitcoin + Created with Sketch. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/assets/icons/bitshares.svg b/src/app/assets/icons/bitshares.svg new file mode 100644 index 0000000..1192e5f --- /dev/null +++ b/src/app/assets/icons/bitshares.svg @@ -0,0 +1,33 @@ + + + + bts-logo-v2 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/assets/icons/calendar.svg b/src/app/assets/icons/calendar.svg new file mode 100644 index 0000000..60ec762 --- /dev/null +++ b/src/app/assets/icons/calendar.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/app/assets/icons/chain.svg b/src/app/assets/icons/chain.svg new file mode 100644 index 0000000..5430757 --- /dev/null +++ b/src/app/assets/icons/chain.svg @@ -0,0 +1,529 @@ + + + + + + diff --git a/src/app/assets/icons/chatbox.svg b/src/app/assets/icons/chatbox.svg new file mode 100644 index 0000000..2eda85c --- /dev/null +++ b/src/app/assets/icons/chatbox.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/app/assets/icons/chatboxes.svg b/src/app/assets/icons/chatboxes.svg new file mode 100644 index 0000000..8e0847e --- /dev/null +++ b/src/app/assets/icons/chatboxes.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/src/app/assets/icons/chevron-down-circle.svg b/src/app/assets/icons/chevron-down-circle.svg new file mode 100644 index 0000000..5596b7b --- /dev/null +++ b/src/app/assets/icons/chevron-down-circle.svg @@ -0,0 +1,2 @@ + + diff --git a/src/app/assets/icons/chevron-left.svg b/src/app/assets/icons/chevron-left.svg new file mode 100644 index 0000000..2a5847d --- /dev/null +++ b/src/app/assets/icons/chevron-left.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/src/app/assets/icons/chevron-up-circle.svg b/src/app/assets/icons/chevron-up-circle.svg new file mode 100644 index 0000000..c02e4b7 --- /dev/null +++ b/src/app/assets/icons/chevron-up-circle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/app/assets/icons/clock.svg b/src/app/assets/icons/clock.svg new file mode 100644 index 0000000..717c318 --- /dev/null +++ b/src/app/assets/icons/clock.svg @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/src/app/assets/icons/cog.svg b/src/app/assets/icons/cog.svg new file mode 100644 index 0000000..1a9b165 --- /dev/null +++ b/src/app/assets/icons/cog.svg @@ -0,0 +1,5 @@ + + +cog + + diff --git a/src/app/assets/icons/dropdown-arrow.svg b/src/app/assets/icons/dropdown-arrow.svg new file mode 100644 index 0000000..e08cda1 --- /dev/null +++ b/src/app/assets/icons/dropdown-arrow.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/src/app/assets/icons/empty.svg b/src/app/assets/icons/empty.svg new file mode 100644 index 0000000..aeb7cab --- /dev/null +++ b/src/app/assets/icons/empty.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/app/assets/icons/enter.svg b/src/app/assets/icons/enter.svg new file mode 100644 index 0000000..37674e9 --- /dev/null +++ b/src/app/assets/icons/enter.svg @@ -0,0 +1,5 @@ + + +enter + + diff --git a/src/app/assets/icons/ether.svg b/src/app/assets/icons/ether.svg new file mode 100644 index 0000000..1af38d9 --- /dev/null +++ b/src/app/assets/icons/ether.svg @@ -0,0 +1,26 @@ + + + + ether + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/assets/icons/extlink.svg b/src/app/assets/icons/extlink.svg new file mode 100644 index 0000000..a9ea94b --- /dev/null +++ b/src/app/assets/icons/extlink.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/src/app/assets/icons/eye.svg b/src/app/assets/icons/eye.svg new file mode 100644 index 0000000..4b83a78 --- /dev/null +++ b/src/app/assets/icons/eye.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/src/app/assets/icons/facebook.svg b/src/app/assets/icons/facebook.svg new file mode 100644 index 0000000..28bd6f9 --- /dev/null +++ b/src/app/assets/icons/facebook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/assets/icons/flag1.svg b/src/app/assets/icons/flag1.svg new file mode 100644 index 0000000..8a9f14a --- /dev/null +++ b/src/app/assets/icons/flag1.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/src/app/assets/icons/flag2.svg b/src/app/assets/icons/flag2.svg new file mode 100644 index 0000000..bef152b --- /dev/null +++ b/src/app/assets/icons/flag2.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/src/app/assets/icons/home.svg b/src/app/assets/icons/home.svg new file mode 100644 index 0000000..5762b7b --- /dev/null +++ b/src/app/assets/icons/home.svg @@ -0,0 +1,5 @@ + + +home + + diff --git a/src/app/assets/icons/key.svg b/src/app/assets/icons/key.svg new file mode 100644 index 0000000..b79c465 --- /dev/null +++ b/src/app/assets/icons/key.svg @@ -0,0 +1,5 @@ + + +key + + diff --git a/src/app/assets/icons/line.svg b/src/app/assets/icons/line.svg new file mode 100644 index 0000000..1b9c80d --- /dev/null +++ b/src/app/assets/icons/line.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/app/assets/icons/link.svg b/src/app/assets/icons/link.svg new file mode 100644 index 0000000..3558ade --- /dev/null +++ b/src/app/assets/icons/link.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/src/app/assets/icons/linkedin.svg b/src/app/assets/icons/linkedin.svg new file mode 100644 index 0000000..18f92c7 --- /dev/null +++ b/src/app/assets/icons/linkedin.svg @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/src/app/assets/icons/location.svg b/src/app/assets/icons/location.svg new file mode 100644 index 0000000..d7844b2 --- /dev/null +++ b/src/app/assets/icons/location.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/app/assets/icons/logo.svg b/src/app/assets/icons/logo.svg new file mode 100644 index 0000000..646570d --- /dev/null +++ b/src/app/assets/icons/logo.svg @@ -0,0 +1,8 @@ + +Steemit Logo + + + + + + \ No newline at end of file diff --git a/src/app/assets/icons/logotype.svg b/src/app/assets/icons/logotype.svg new file mode 100644 index 0000000..421e7d5 --- /dev/null +++ b/src/app/assets/icons/logotype.svg @@ -0,0 +1,9 @@ + +Steemit Logo + + + + + + + \ No newline at end of file diff --git a/src/app/assets/icons/menu.svg b/src/app/assets/icons/menu.svg new file mode 100644 index 0000000..5d75ce4 --- /dev/null +++ b/src/app/assets/icons/menu.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/app/assets/icons/pencil2.svg b/src/app/assets/icons/pencil2.svg new file mode 100644 index 0000000..831818d --- /dev/null +++ b/src/app/assets/icons/pencil2.svg @@ -0,0 +1,5 @@ + + +pencil2 + + diff --git a/src/app/assets/icons/person.svg b/src/app/assets/icons/person.svg new file mode 100644 index 0000000..36f3272 --- /dev/null +++ b/src/app/assets/icons/person.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/app/assets/icons/photo.svg b/src/app/assets/icons/photo.svg new file mode 100644 index 0000000..cac4910 --- /dev/null +++ b/src/app/assets/icons/photo.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/app/assets/icons/printer.svg b/src/app/assets/icons/printer.svg new file mode 100644 index 0000000..7ed4f67 --- /dev/null +++ b/src/app/assets/icons/printer.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/app/assets/icons/profile.svg b/src/app/assets/icons/profile.svg new file mode 100644 index 0000000..a0ed7f4 --- /dev/null +++ b/src/app/assets/icons/profile.svg @@ -0,0 +1,5 @@ + + +profile + + diff --git a/src/app/assets/icons/quill.svg b/src/app/assets/icons/quill.svg new file mode 100644 index 0000000..ba79750 --- /dev/null +++ b/src/app/assets/icons/quill.svg @@ -0,0 +1,5 @@ + + +quill + + diff --git a/src/app/assets/icons/reblog.svg b/src/app/assets/icons/reblog.svg new file mode 100644 index 0000000..79deb8b --- /dev/null +++ b/src/app/assets/icons/reblog.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/app/assets/icons/replies.svg b/src/app/assets/icons/replies.svg new file mode 100644 index 0000000..9833065 --- /dev/null +++ b/src/app/assets/icons/replies.svg @@ -0,0 +1,5 @@ + + +indent-increase + + diff --git a/src/app/assets/icons/reply.svg b/src/app/assets/icons/reply.svg new file mode 100644 index 0000000..c81abcf --- /dev/null +++ b/src/app/assets/icons/reply.svg @@ -0,0 +1,5 @@ + + +reply + + diff --git a/src/app/assets/icons/search.svg b/src/app/assets/icons/search.svg new file mode 100644 index 0000000..4d007ad --- /dev/null +++ b/src/app/assets/icons/search.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/app/assets/icons/share.svg b/src/app/assets/icons/share.svg new file mode 100644 index 0000000..821b4e2 --- /dev/null +++ b/src/app/assets/icons/share.svg @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/src/app/assets/icons/steem.svg b/src/app/assets/icons/steem.svg new file mode 100644 index 0000000..c76eb79 --- /dev/null +++ b/src/app/assets/icons/steem.svg @@ -0,0 +1,16 @@ + + + + steem + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/src/app/assets/icons/steemd.svg b/src/app/assets/icons/steemd.svg new file mode 100644 index 0000000..26b99d3 --- /dev/null +++ b/src/app/assets/icons/steemd.svg @@ -0,0 +1,17 @@ + + + + + + + +SD + diff --git a/src/app/assets/icons/steemdb.svg b/src/app/assets/icons/steemdb.svg new file mode 100644 index 0000000..74aa721 --- /dev/null +++ b/src/app/assets/icons/steemdb.svg @@ -0,0 +1,17 @@ + + + + + + + +SDb + diff --git a/src/app/assets/icons/steempower.svg b/src/app/assets/icons/steempower.svg new file mode 100644 index 0000000..2d93593 --- /dev/null +++ b/src/app/assets/icons/steempower.svg @@ -0,0 +1,14 @@ + + + + steem + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/src/app/assets/icons/twitter.svg b/src/app/assets/icons/twitter.svg new file mode 100644 index 0000000..2b1346c --- /dev/null +++ b/src/app/assets/icons/twitter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/assets/icons/user.svg b/src/app/assets/icons/user.svg new file mode 100644 index 0000000..36f3272 --- /dev/null +++ b/src/app/assets/icons/user.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/app/assets/icons/video.svg b/src/app/assets/icons/video.svg new file mode 100644 index 0000000..4acd2bb --- /dev/null +++ b/src/app/assets/icons/video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/assets/icons/voter.svg b/src/app/assets/icons/voter.svg new file mode 100644 index 0000000..20cc7b7 --- /dev/null +++ b/src/app/assets/icons/voter.svg @@ -0,0 +1 @@ + diff --git a/src/app/assets/icons/voters.svg b/src/app/assets/icons/voters.svg new file mode 100644 index 0000000..94fe541 --- /dev/null +++ b/src/app/assets/icons/voters.svg @@ -0,0 +1,2 @@ + + diff --git a/src/app/assets/icons/wallet.svg b/src/app/assets/icons/wallet.svg new file mode 100644 index 0000000..3c4c3d8 --- /dev/null +++ b/src/app/assets/icons/wallet.svg @@ -0,0 +1,5 @@ + + +wallet + + diff --git a/src/app/assets/images/404.svg b/src/app/assets/images/404.svg new file mode 100644 index 0000000..5ea2e7f --- /dev/null +++ b/src/app/assets/images/404.svg @@ -0,0 +1,116 @@ + + + + 404-1 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/assets/images/500.jpg b/src/app/assets/images/500.jpg new file mode 100644 index 0000000..a0c40c6 Binary files /dev/null and b/src/app/assets/images/500.jpg differ diff --git a/src/app/assets/images/facebook.svg b/src/app/assets/images/facebook.svg new file mode 100644 index 0000000..5540720 --- /dev/null +++ b/src/app/assets/images/facebook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/assets/images/favicon.ico b/src/app/assets/images/favicon.ico new file mode 100644 index 0000000..eba89fa Binary files /dev/null and b/src/app/assets/images/favicon.ico differ diff --git a/src/app/assets/images/favicons/android-chrome-144x144.png b/src/app/assets/images/favicons/android-chrome-144x144.png new file mode 100644 index 0000000..ec20b11 Binary files /dev/null and b/src/app/assets/images/favicons/android-chrome-144x144.png differ diff --git a/src/app/assets/images/favicons/android-chrome-192x192.png b/src/app/assets/images/favicons/android-chrome-192x192.png new file mode 100644 index 0000000..fc288a8 Binary files /dev/null and b/src/app/assets/images/favicons/android-chrome-192x192.png differ diff --git a/src/app/assets/images/favicons/android-chrome-36x36.png b/src/app/assets/images/favicons/android-chrome-36x36.png new file mode 100644 index 0000000..9b3b66d Binary files /dev/null and b/src/app/assets/images/favicons/android-chrome-36x36.png differ diff --git a/src/app/assets/images/favicons/android-chrome-48x48.png b/src/app/assets/images/favicons/android-chrome-48x48.png new file mode 100644 index 0000000..ae7a4ea Binary files /dev/null and b/src/app/assets/images/favicons/android-chrome-48x48.png differ diff --git a/src/app/assets/images/favicons/android-chrome-72x72.png b/src/app/assets/images/favicons/android-chrome-72x72.png new file mode 100644 index 0000000..35cf1fe Binary files /dev/null and b/src/app/assets/images/favicons/android-chrome-72x72.png differ diff --git a/src/app/assets/images/favicons/android-chrome-96x96.png b/src/app/assets/images/favicons/android-chrome-96x96.png new file mode 100644 index 0000000..7c15f34 Binary files /dev/null and b/src/app/assets/images/favicons/android-chrome-96x96.png differ diff --git a/src/app/assets/images/favicons/apple-touch-icon-114x114.png b/src/app/assets/images/favicons/apple-touch-icon-114x114.png new file mode 100644 index 0000000..f540067 Binary files /dev/null and b/src/app/assets/images/favicons/apple-touch-icon-114x114.png differ diff --git a/src/app/assets/images/favicons/apple-touch-icon-120x120.png b/src/app/assets/images/favicons/apple-touch-icon-120x120.png new file mode 100644 index 0000000..a5cc02f Binary files /dev/null and b/src/app/assets/images/favicons/apple-touch-icon-120x120.png differ diff --git a/src/app/assets/images/favicons/apple-touch-icon-144x144.png b/src/app/assets/images/favicons/apple-touch-icon-144x144.png new file mode 100644 index 0000000..4a3cd9b Binary files /dev/null and b/src/app/assets/images/favicons/apple-touch-icon-144x144.png differ diff --git a/src/app/assets/images/favicons/apple-touch-icon-152x152.png b/src/app/assets/images/favicons/apple-touch-icon-152x152.png new file mode 100644 index 0000000..233c578 Binary files /dev/null and b/src/app/assets/images/favicons/apple-touch-icon-152x152.png differ diff --git a/src/app/assets/images/favicons/apple-touch-icon-180x180.png b/src/app/assets/images/favicons/apple-touch-icon-180x180.png new file mode 100644 index 0000000..88a1669 Binary files /dev/null and b/src/app/assets/images/favicons/apple-touch-icon-180x180.png differ diff --git a/src/app/assets/images/favicons/apple-touch-icon-57x57.png b/src/app/assets/images/favicons/apple-touch-icon-57x57.png new file mode 100644 index 0000000..10c60f4 Binary files /dev/null and b/src/app/assets/images/favicons/apple-touch-icon-57x57.png differ diff --git a/src/app/assets/images/favicons/apple-touch-icon-60x60.png b/src/app/assets/images/favicons/apple-touch-icon-60x60.png new file mode 100644 index 0000000..8538ef7 Binary files /dev/null and b/src/app/assets/images/favicons/apple-touch-icon-60x60.png differ diff --git a/src/app/assets/images/favicons/apple-touch-icon-72x72.png b/src/app/assets/images/favicons/apple-touch-icon-72x72.png new file mode 100644 index 0000000..35cf1fe Binary files /dev/null and b/src/app/assets/images/favicons/apple-touch-icon-72x72.png differ diff --git a/src/app/assets/images/favicons/apple-touch-icon-76x76.png b/src/app/assets/images/favicons/apple-touch-icon-76x76.png new file mode 100644 index 0000000..ffd7657 Binary files /dev/null and b/src/app/assets/images/favicons/apple-touch-icon-76x76.png differ diff --git a/src/app/assets/images/favicons/apple-touch-icon.png b/src/app/assets/images/favicons/apple-touch-icon.png new file mode 100644 index 0000000..88a1669 Binary files /dev/null and b/src/app/assets/images/favicons/apple-touch-icon.png differ diff --git a/src/app/assets/images/favicons/browserconfig.xml b/src/app/assets/images/favicons/browserconfig.xml new file mode 100644 index 0000000..628c382 --- /dev/null +++ b/src/app/assets/images/favicons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #ffffff + + + diff --git a/src/app/assets/images/favicons/chrome-web-store-128x128.png b/src/app/assets/images/favicons/chrome-web-store-128x128.png new file mode 100644 index 0000000..6e1b9d3 Binary files /dev/null and b/src/app/assets/images/favicons/chrome-web-store-128x128.png differ diff --git a/src/app/assets/images/favicons/favicon-128.png b/src/app/assets/images/favicons/favicon-128.png new file mode 100644 index 0000000..36599f8 Binary files /dev/null and b/src/app/assets/images/favicons/favicon-128.png differ diff --git a/src/app/assets/images/favicons/favicon-16x16.png b/src/app/assets/images/favicons/favicon-16x16.png new file mode 100644 index 0000000..8f00a50 Binary files /dev/null and b/src/app/assets/images/favicons/favicon-16x16.png differ diff --git a/src/app/assets/images/favicons/favicon-196x196.png b/src/app/assets/images/favicons/favicon-196x196.png new file mode 100644 index 0000000..be48eb0 Binary files /dev/null and b/src/app/assets/images/favicons/favicon-196x196.png differ diff --git a/src/app/assets/images/favicons/favicon-24x24.png b/src/app/assets/images/favicons/favicon-24x24.png new file mode 100644 index 0000000..9e3f1a5 Binary files /dev/null and b/src/app/assets/images/favicons/favicon-24x24.png differ diff --git a/src/app/assets/images/favicons/favicon-32x32.png b/src/app/assets/images/favicons/favicon-32x32.png new file mode 100644 index 0000000..a58d70e Binary files /dev/null and b/src/app/assets/images/favicons/favicon-32x32.png differ diff --git a/src/app/assets/images/favicons/favicon-64x64.png b/src/app/assets/images/favicons/favicon-64x64.png new file mode 100644 index 0000000..eba89fa Binary files /dev/null and b/src/app/assets/images/favicons/favicon-64x64.png differ diff --git a/src/app/assets/images/favicons/favicon-96x96.png b/src/app/assets/images/favicons/favicon-96x96.png new file mode 100644 index 0000000..388539b Binary files /dev/null and b/src/app/assets/images/favicons/favicon-96x96.png differ diff --git a/src/app/assets/images/favicons/favicon.ico b/src/app/assets/images/favicons/favicon.ico new file mode 100644 index 0000000..eba89fa Binary files /dev/null and b/src/app/assets/images/favicons/favicon.ico differ diff --git a/src/app/assets/images/favicons/manifest.json b/src/app/assets/images/favicons/manifest.json new file mode 100644 index 0000000..2d70e25 --- /dev/null +++ b/src/app/assets/images/favicons/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "Golos", + "icons": [ + { + "src": "\/images\/favicons\/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image\/png" + }, + { + "src": "\/images\/favicons\/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image\/png" + } + ], + "theme_color": "#ffffff", + "display": "standalone" +} diff --git a/src/app/assets/images/favicons/mstile-144x144.png b/src/app/assets/images/favicons/mstile-144x144.png new file mode 100644 index 0000000..ec20b11 Binary files /dev/null and b/src/app/assets/images/favicons/mstile-144x144.png differ diff --git a/src/app/assets/images/favicons/mstile-150x150.png b/src/app/assets/images/favicons/mstile-150x150.png new file mode 100644 index 0000000..e0b4f0f Binary files /dev/null and b/src/app/assets/images/favicons/mstile-150x150.png differ diff --git a/src/app/assets/images/favicons/mstile-310x150.png b/src/app/assets/images/favicons/mstile-310x150.png new file mode 100644 index 0000000..cffd4bb Binary files /dev/null and b/src/app/assets/images/favicons/mstile-310x150.png differ diff --git a/src/app/assets/images/favicons/mstile-310x310.png b/src/app/assets/images/favicons/mstile-310x310.png new file mode 100644 index 0000000..b244e48 Binary files /dev/null and b/src/app/assets/images/favicons/mstile-310x310.png differ diff --git a/src/app/assets/images/favicons/mstile-70x70.png b/src/app/assets/images/favicons/mstile-70x70.png new file mode 100644 index 0000000..6e1b9d3 Binary files /dev/null and b/src/app/assets/images/favicons/mstile-70x70.png differ diff --git a/src/app/assets/images/favicons/opera-speed-dial-195x195.png b/src/app/assets/images/favicons/opera-speed-dial-195x195.png new file mode 100644 index 0000000..0649691 Binary files /dev/null and b/src/app/assets/images/favicons/opera-speed-dial-195x195.png differ diff --git a/src/app/assets/images/lp-bottom.jpg b/src/app/assets/images/lp-bottom.jpg new file mode 100644 index 0000000..b5d2451 Binary files /dev/null and b/src/app/assets/images/lp-bottom.jpg differ diff --git a/src/app/assets/images/qrcode.png b/src/app/assets/images/qrcode.png new file mode 100644 index 0000000..b1e1015 Binary files /dev/null and b/src/app/assets/images/qrcode.png differ diff --git a/src/app/assets/images/reddit.svg b/src/app/assets/images/reddit.svg new file mode 100644 index 0000000..1c0ab03 --- /dev/null +++ b/src/app/assets/images/reddit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/assets/images/steemit-1024x1024.png b/src/app/assets/images/steemit-1024x1024.png new file mode 100644 index 0000000..bec145c Binary files /dev/null and b/src/app/assets/images/steemit-1024x1024.png differ diff --git a/src/app/assets/images/steemit-halloween.png b/src/app/assets/images/steemit-halloween.png new file mode 100644 index 0000000..423f9e8 Binary files /dev/null and b/src/app/assets/images/steemit-halloween.png differ diff --git a/src/app/assets/images/steemit-share.png b/src/app/assets/images/steemit-share.png new file mode 100644 index 0000000..1257c7d Binary files /dev/null and b/src/app/assets/images/steemit-share.png differ diff --git a/src/app/assets/images/steemit-twshare-2.png b/src/app/assets/images/steemit-twshare-2.png new file mode 100644 index 0000000..f569bdc Binary files /dev/null and b/src/app/assets/images/steemit-twshare-2.png differ diff --git a/src/app/assets/images/steemit-twshare.png b/src/app/assets/images/steemit-twshare.png new file mode 100644 index 0000000..f569bdc Binary files /dev/null and b/src/app/assets/images/steemit-twshare.png differ diff --git a/src/app/assets/images/steemit.png b/src/app/assets/images/steemit.png new file mode 100644 index 0000000..1257c7d Binary files /dev/null and b/src/app/assets/images/steemit.png differ diff --git a/src/app/assets/images/steemit.svg b/src/app/assets/images/steemit.svg new file mode 100644 index 0000000..9e862a7 --- /dev/null +++ b/src/app/assets/images/steemit.svg @@ -0,0 +1,19 @@ + + + + STEEMIT/steemit_logo_final Copy 2 + Created with Sketch. + + + + + + + + + + + + + + diff --git a/src/app/assets/images/user.png b/src/app/assets/images/user.png new file mode 100644 index 0000000..aa3db4b Binary files /dev/null and b/src/app/assets/images/user.png differ diff --git a/src/app/assets/images/welcome.jpg b/src/app/assets/images/welcome.jpg new file mode 100644 index 0000000..5c35613 Binary files /dev/null and b/src/app/assets/images/welcome.jpg differ diff --git a/src/app/assets/static/manifest.json b/src/app/assets/static/manifest.json new file mode 100644 index 0000000..52cf1fa --- /dev/null +++ b/src/app/assets/static/manifest.json @@ -0,0 +1,8 @@ +{ + "name": "Steemit", + "short_name": "Steemit", + "start_url": "/", + "display": "standalone", + "gcm_sender_id": "724829305784", + "gcm_user_visible_only": true +} diff --git a/src/app/assets/static/search.html b/src/app/assets/static/search.html new file mode 100644 index 0000000..3b7b270 --- /dev/null +++ b/src/app/assets/static/search.html @@ -0,0 +1,149 @@ + + + + + + Search - steemit.com + + + + + + +
+
+
+ +
+
+ + + diff --git a/src/app/assets/stylesheets/_animation.scss b/src/app/assets/stylesheets/_animation.scss new file mode 100755 index 0000000..1fd9d2e --- /dev/null +++ b/src/app/assets/stylesheets/_animation.scss @@ -0,0 +1,24 @@ +// Animation helpers + +$fade-in-animation-length: 1s; +$fade-in-animation-delay: 0.03s; + + +.fade-in { + @include opacity(0); + @for $i from 1 through 10 { + &--#{$i} { + animation: fade-in + $fade-in-animation-length ease-in-out $fade-in-animation-delay*$i both; + } + } +} + +@keyframes fade-in { + 0% { + @include opacity(0); + } + 100% { + @include opacity(1); + } +} \ No newline at end of file diff --git a/src/app/assets/stylesheets/_layout.scss b/src/app/assets/stylesheets/_layout.scss new file mode 100755 index 0000000..813d6a4 --- /dev/null +++ b/src/app/assets/stylesheets/_layout.scss @@ -0,0 +1,25 @@ +// breakpoints + +$S: 20em; // 320px / 16 +$M: 47.5em; // 760px / 16 +$L: 75em; // 1200px / 16 +$XL: 100em; // 1600px / 16 + +// media queries + +@mixin MQ($canvas) { + @if $canvas == S { + @media only screen and (min-width: $S) { @content; } + } + @else if $canvas == M { + @media only screen and (min-width: $M) { @content; } + } + @else if $canvas == L { + @media only screen and (min-width: $L) { @content; } + } + @else if $canvas == XL { + @media only screen and (min-width: $XL) { @content; } + } +} + + diff --git a/src/app/assets/stylesheets/_themes.scss b/src/app/assets/stylesheets/_themes.scss new file mode 100755 index 0000000..f0da826 --- /dev/null +++ b/src/app/assets/stylesheets/_themes.scss @@ -0,0 +1,317 @@ +$themes: ( + original: ( + colorAccent: $color-blue, + colorAccentHover: $color-blue-original-light, + colorAccentReverse: $color-blue-original-light, + colorWhite: $color-white, + backgroundColor: $color-background-off-white, + backgroundColorOpaque: $color-background-off-white, + backgroundTransparent: transparent, + moduleBackgroundColor: $color-white, + menuBackgroundColor: $color-background-dark, + moduleMediumBackgroundColor: $color-white, + navBackgroundColor: $color-white, + highlightBackgroundColor: #f3faf0, + tableRowEvenBackgroundColor: #f4f4f4, + border: 1px solid $color-border-light, + borderLight: 1px solid $color-border-light-lightest, + borderDark: 1px solid $color-text-gray, + borderAccent: 1px solid $color-blue, + borderTransparent: transparent, + iconColorSecondary: #cacaca, + textColorPrimary: $color-text-dark, + textColorSecondary: $color-text-gray, + textColorAccent: $color-text-blue, + textColorAccentHover: $color-blue-original-dark, + textColorError: $color-text-red, + contentBorderAccent: $color-transparent, + buttonBackground: $color-blue-original-dark, + buttonBackgroundHover: $color-blue-original-light, + buttonText: $color-text-white, + buttonTextShadow: 0 1px 0 rgba(0,0,0,0.20), + buttonTextHover: $color-text-white, + buttonBoxShadow: $color-transparent, + ), + light: ( + colorAccent: $color-teal, + colorAccentHover: $color-teal-dark, + colorAccentReverse: $color-blue-black, + colorWhite: $color-white, + backgroundColor: $color-background-off-white, + backgroundColorOpaque: $color-background-off-white, + backgroundTransparent: transparent, + moduleBackgroundColor: $color-white, + menuBackgroundColor: $color-background-dark, + moduleMediumBackgroundColor: $color-transparent, + navBackgroundColor: $color-white, + highlightBackgroundColor: #f3faf0, + tableRowEvenBackgroundColor: #f4f4f4, + border: 1px solid $color-border-light, + borderLight: 1px solid $color-border-light-lightest, + borderDark: 1px solid $color-text-gray, + borderAccent: 1px solid $color-teal, + borderTransparent: transparent, + iconColorSecondary: #cacaca, + textColorPrimary: $color-text-dark, + textColorSecondary: $color-text-gray, + textColorAccent: $color-text-teal, + textColorAccentHover: $color-teal, + textColorError: $color-text-red, + contentBorderAccent: $color-teal, + buttonBackground: $color-blue-black, + buttonBackgroundHover: $color-teal, + buttonText: $color-text-white, + buttonTextShadow: 0 1px 0 rgba(0,0,0,0.20), + buttonTextHover: $color-white, + buttonBoxShadow: $color-teal, + buttonBoxShadowHover: $color-blue-black, + ), + dark: ( + colorAccent: $color-teal, + colorAccentHover: $color-teal, + colorAccentReverse: $color-white, + colorWhite: $color-white, + backgroundColor: $color-background-dark, + backgroundColorOpaque: $color-blue-dark, + moduleBackgroundColor: $color-background-dark, + backgroundTransparent: transparent, + menuBackgroundColor: $color-blue-dark, + moduleMediumBackgroundColor: $color-background-dark, + navBackgroundColor: $color-background-dark, + highlightBackgroundColor: $color-blue-black-darkest, + tableRowEvenBackgroundColor: #212C33, + border: 1px solid $color-border-dark, + borderLight: 1px solid $color-border-dark-lightest, + borderDark: 1px solid $color-text-gray-light, + borderAccent: 1px solid $color-teal, + borderTransparent: transparent, + iconColorSecondary: $color-text-gray-light, + textColorPrimary: $color-text-white, + textColorSecondary: $color-text-gray-light, + textColorAccent: $color-teal, + textColorAccentHover: $color-teal-light, + textColorError: $color-text-red, + contentBorderAccent: $color-teal, + buttonBackground: $color-white, + buttonBackgroundHover: $color-teal, + buttonText: $color-blue-dark, + buttonTextShadow: 0 1px 0 rgba(0,0,0,0), + buttonTextHover: $color-white, + buttonBoxShadow: $color-teal, + buttonBoxShadowHover: $color-white, + ), +); + +/* + * Implementation of themes + */ +@mixin themify($themes) { + @each $theme, $map in $themes { + .theme-#{$theme} & { + $theme-map: () !global; + @each $key, $submap in $map { + $value: map-get(map-get($themes, $theme), '#{$key}'); + $theme-map: map-merge($theme-map, ($key: $value)) !global; + } + @content; + $theme-map: null !global; + } + } +} + +@function themed($key) { + @return map-get($theme-map, $key); +} + + + .theme-original { + background-color: $white; + color: $color-text-dark; + @include MQ(M) { + background-color: $color-background-off-white; + } + } + .theme-light { + background-color: $white; + color: $color-text-dark; + @include MQ(M) { + background-color: $color-background-off-white; + } + } + .theme-dark { + background-color: $color-background-dark; + color: $color-text-white; + } + + +// Utility classes to be used with @extend + +.link { + text-decoration: none; + transition: 0.2s all ease-in-out; + &--primary { + @include themify($themes) { + color: themed('textColorPrimary'); + } + &:visited, &:active { + @include themify($themes) { + color: themed('textColorPrimary'); + } + } + &:hover, &:focus { + @include themify($themes) { + color: themed('textColorAccent'); + } + } + } + &--secondary { + @include themify($themes) { + color: themed('textColorSecondary'); + } + &:visited, &:active { + @include themify($themes) { + color: themed('textColorSecondary'); + } + } + &:hover, &:focus { + @include themify($themes) { + color: themed('textColorAccent'); + } + } + } + &--accent { + @include themify($themes) { + color: themed('textColorAccent'); + } + &:visited, &:active { + @include themify($themes) { + color: themed('textColorAccent'); + } + } + &:hover, &:focus { + @include themify($themes) { + color: themed('textColorAccentHover'); + } + } + } +} + +.e-btn-hollow { + background-color: transparent; + transition: 0.2s all ease-in-out; + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0); + @include themify($themes) { + border: themed('borderAccent'); + color: themed('textColorAccent'); + } + &:hover { + @include themify($themes) { + border: themed('borderDark'); + color: themed('textColorPrimary'); + } + } +} + +.e-btn { + text-decoration: none; + font-weight: bold; + transition: 0.2s all ease-in-out; + text-transform: capitalize; + border-radius: 0; + text-decoration: none; + text-transform: capitalize; + @include font-size(18px); + @include themify($themes) { + background-color: themed('buttonBackground'); + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0), 5px 5px 0 0 themed('buttonBoxShadow'); + color: themed('buttonText'); + } + &:hover, &:focus { + @include themify($themes) { + background-color: themed('buttonBackgroundHover'); + box-shadow: 2px 2px 2px 0 rgba(0,0,0,0.1), 7px 7px 0 0 themed('buttonBoxShadowHover'); + color: themed('buttonTextHover'); + text-shadow: themed('buttonTextShadow'); + } + } + &:visited, &:active { + @include themify($themes) { + color: themed('buttonText'); + } + &:hover, &:focus { + @include themify($themes) { + color: themed('buttonTextHover'); + } + } + } +} + +.button.disabled, .button[disabled] { + opacity: 0.25; + cursor: not-allowed; + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0); + @include themify($themes) { + background-color: themed('buttonBackground'); + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0); + color: themed('buttonText'); + } + &:hover { + @include themify($themes) { + background-color: themed('buttonBackground'); + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0); + color: themed('buttonText'); + } + } +} + +// This button class doesn't applying theming (just straight styles). To be used when there are no theming classes available (e.g. in modals and static server pages in signup) + +.e-btn { + &--black { + background-color: $color-blue-black; + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0), 5px 5px 0 0 $color-teal; + color: $color-white; + &:hover, &:focus { + background-color: $color-teal; + box-shadow: 2px 2px 2px 0 rgba(0,0,0,0.1), 7px 7px 0 0 $color-blue-black; + color: $color-white; + text-shadow: 0 1px 0 rgba(0,0,0,0.20); + } + &:visited, &:active { + background-color: $color-blue-black; + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0), 5px 5px 0 0 $color-teal; + color: $color-white; + } + &.disabled, &[disabled] { + opacity: 0.25; + cursor: not-allowed; + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0); + &:hover, &:focus { + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0); + background-color: $color-blue-black; + color: $color-white; + } + } + &.hollow { + background-color: transparent; + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0); + color: $color-text-gray; + font-weight: normal; + transition: 0.2s all ease-in-out; + border: transparent; + &:hover, &:focus { + background-color: transparent; + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0); + color: $color-blue-dark; + font-weight: normal; + text-shadow: 0 1px 0 rgba(0,0,0,0.0); + } + &:visited, &:active { + background-color: transparent; + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0); + color: $color-text-gray; + font-weight: normal; + } + } + } +} diff --git a/src/app/assets/stylesheets/_variables.scss b/src/app/assets/stylesheets/_variables.scss new file mode 100755 index 0000000..852bceb --- /dev/null +++ b/src/app/assets/stylesheets/_variables.scss @@ -0,0 +1,37 @@ + + +$color-white: #fff; +$color-black: #000; +$color-red: #ff0264; +$color-blue: #004EFF; +$color-blue-original-dark: #1A5099; +$color-blue-original-light: #4BA2F2; +$color-blue-black: #171F24; +$color-blue-black-darkest: #11161A; +$color-blue-dark: #2C3A45; +$color-teal: #06D6A9; +$color-teal-light:#00FFC8; +$color-teal-dark:#049173; + +$color-yellow: #fce76c; +$color-transparent: transparent; + +$color-background-off-white: #fcfcfc; +$color-background-dark: #1C252B; + +$color-text-dark: #333; +$color-text-white: #fcfcfc; +$color-text-blue: #004EFF; +$color-text-gray: #788187; +$color-text-gray-light: #A6B2BA; +$color-text-teal: #1FBF8F; +$color-text-red: $color-red; + +$color-border-light: #eee; +$color-border-light-lightest: #f6f6f6; +$color-border-dark: #232F38; +$color-border-dark-lightest: #2B3A45; + +$font-primary: helvetica, sans-serif; + +$alert-color: $color-red; \ No newline at end of file diff --git a/src/app/assets/stylesheets/app.scss b/src/app/assets/stylesheets/app.scss new file mode 100644 index 0000000..a1906ee --- /dev/null +++ b/src/app/assets/stylesheets/app.scss @@ -0,0 +1,194 @@ + +@import "./foundation-settings"; +@import "~foundation-sites/scss/foundation"; + +@include foundation-everything(true); + +@import "./variables"; +@import "./mixins"; +@import "./layout"; +@import "./themes"; + +@import "./foundation-overrides"; + +@import './animation'; + +@import "./fonts"; +@import "./forms"; + +@import "./markdown"; +@import "./notifications"; +@import "src/app/components/all"; + +/* Small only */ +@media screen and (max-width: 39.9375em) { + body { + font-size: 86%; + } +} + +/* Medium only */ +@media screen and (min-width: 40em) and (max-width: 63.9375em) { + body { + font-size: 92%; + } +} + +a, path, circle { + transition: opacity, fill, stroke .3s ease 0s; +} + +.space-right { + margin-right: 0.4rem; +} + +.clear-right { + clear: right; +} + +.clear-left { + clear: left; +} + +.clear-both { + clear: both; +} + +.strikethrough { + text-decoration: line-through; +} + +.uppercase, label { + text-transform: uppercase; +} + + + +.secondary { + @include themify($themes) { + color: themed('textColorSecondary'); + } + font-size: 90%; + a { + transition: 0.2s all ease-in-out; + color: $dark-gray; + @include themify($themes) { + color: themed('textColorSecondary'); + } + :hover { + @include themify($themes) { + color: themed('textColorAccent'); + } + } + + } +} + +.ptc, +a.ptc { + text-decoration: none; + transition: 0.2s all ease-in-out; + @include themify($themes) { + color: themed('textColorPrimary'); + } + + &:hover, &:focus { + color: $dark-gray; + text-decoration: none; + @include themify($themes) { + color: themed('textColorAccent'); + } + } +} + +.button.hollow.no-border { + border: none; + text-decoration: underline; +} + +.button.slim { + padding: 5px; + margin: 5px; +} + +@keyframes loading { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.marginLeft1rem { + margin-left: 1rem; +} + +.NotFound { + width: 640px; + margin-top: 2em; + text-align: center; + // small only + @media screen and (max-width: 39.9375em) { + width: 340px; + } +} + +.NotFound__menu { + text-align: center; + li { + float: none; + display: inline-block; + text-align: center; + margin-right: 1%; + list-style: none; + font-weight: 400; + } + li:after { + content: " |"; + } + li:last-child:after { + content: ""; + } +} + +.NotFound__header { + margin-top: 1em; +} + +@media print { + .noPrint { + display: none; + } +} + +.anchor { + padding-top: 68px; + margin-top: -68px; + position: relative; + display: block; +} + +h1, h2, h3, h4, h5, h6 { + line-height: 1.2 !important; +} + +.c-sidebar { + width: 100%; + max-width: 240px; + font-family: helvetica, sans-serif; + &__module { + padding: 1.5em 2em; + @include themify($themes) { + background-color: themed('moduleBackgroundColor'); + border: themed('border'); + } + } +} + +.phishy { + display: inline; + color: red; +} + + diff --git a/src/app/assets/stylesheets/fonts.scss b/src/app/assets/stylesheets/fonts.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/assets/stylesheets/forms.scss b/src/app/assets/stylesheets/forms.scss new file mode 100644 index 0000000..0bee239 --- /dev/null +++ b/src/app/assets/stylesheets/forms.scss @@ -0,0 +1,75 @@ +form { + .error { + input, input:active, input:focus { + border-color: $alert-color; + } + label, .help-text { + color: $alert-color; + } + .help-text { + margin-top: 0.4rem; + } + } + .success { + color: $success-color; + transition: 0.3s all ease-in-out; + } +} +button { + margin-right: 15px !important; +} +input, textarea, select { + // Overwrite 16px margin-bottom, it was pusing error messages down away from the form element + margin-bottom: 0px !important; +} +.error { + color: $alert-color; + margin-bottom: 10px; + transition: 0.3s all ease-in-out; +} +p.error { + padding-top: 6px; + line-height: 1.1; +} +.warning { + color: darkgoldenrod; + margin-bottom: 10px; + transition: 0.3s all ease-in-out; +} +.darkred { + color: darkred; + margin-bottom: 10px; +} +.info { + color: dimgrey; + margin-bottom: 10px; +} +.hoverBackground:hover { + background-color: antiquewhite; +} +.de-emphasize { + color: $dark-gray; +} +.overflow-ellipsis { + overflow: hidden; + text-overflow: ellipsis; +} +.darkbtn { + padding: 12px; + background-color: #eeeeee; +} + +// // what about touch-screen only? +// .hoverShow { +// visibility: hidden; +// } +// // what about touch-screen only? +// .hoverShowTrigger:hover > .hoverShow { +// visibility: visible; +// } + +label { + @include themify($themes) { + color: themed('textColorPrimary'); + } +} \ No newline at end of file diff --git a/src/app/assets/stylesheets/foundation-overrides.scss b/src/app/assets/stylesheets/foundation-overrides.scss new file mode 100644 index 0000000..f6e7cb2 --- /dev/null +++ b/src/app/assets/stylesheets/foundation-overrides.scss @@ -0,0 +1,121 @@ +.menu > li > a { + line-height: 1.5rem; +} + +.tooltip { + margin-top: -1rem; +} + +.dropdown-pane { + width: auto; + padding: 0; + box-shadow: 1px 1px 5px 0px rgba(50, 50, 50, 0.75); + z-index: 1000; + font-size: inherit; + background-color: $color-white; + + .VerticalMenu { + a:hover { + background-color: #f6f6f6; + color: $color-teal-dark; + } + } +} + +a { + transition: 0.2s all ease-in-out; + @include themify($themes) { + color: themed('textColorAccent'); + } + &:hover, &:focus { + @include themify($themes) { + color: themed('textColorAccentHover'); + } + } +} + +div[role=dialog] { + z-index: 500; +} + +input[type=submit].disabled, input[type=submit].disabled:focus { + opacity: 1; + cursor: inherit; + background-color: $medium-gray; +} + +button, .button { + text-transform: uppercase; +} + +.column, .columns { + min-width: 0; +} + +.callout { + margin-top: 1rem; + @include themify($themes) { + color: themed('textColorPrimary'); + background-color: themed('highlightBackgroundColor'); + border: themed('border'); + } +} + +.close-button { + transition: color 0.2s ease-in-out; + @include themify($themes) { + color: themed('textColorSecondary'); + } + &:hover, &:focus { + @include themify($themes) { + color: themed('textColorAccent'); + } + } +} + +hr { + @include themify($themes) { + border-bottom: themed('border'); + } +} + +table { + box-shadow: inset 0 0 0 -1px red; +} + + +thead, tbody, tfoot { + @include themify($themes) { + background-color: themed('backgroundColor'); + } +} + +thead { + @include themify($themes) { + background-color: themed('tableRowEvenBackgroundColor'); + color: themed('textColorPrimary'); + } +} + +tbody tr:nth-child(even) { + @include themify($themes) { + background-color: themed('tableRowEvenBackgroundColor'); + } +} + +.reveal-overlay { + background-color: rgba(0, 0, 0, 0.88); + transition: 0.2s all ease-in-out; +} + +.reveal { + box-shadow: 2px 2px 2px 0 rgba(0,0,0,0.1), 7px 7px 0 0 $color-teal; + border-radius: 0; + border: transparent; + transition: 0.2s all ease-in-out; + .button { + @extend .e-btn; + @extend .e-btn--black; + min-width: 100px; + } +} diff --git a/src/app/assets/stylesheets/foundation-settings.scss b/src/app/assets/stylesheets/foundation-settings.scss new file mode 100644 index 0000000..c072c60 --- /dev/null +++ b/src/app/assets/stylesheets/foundation-settings.scss @@ -0,0 +1,57 @@ +/// Font size attribute applied to `` and ``. We use 100% by default so the value is inherited from the user's browser settings. +/// @type Number +$global-font-size: 100% !default; + +/// Default line height for all type. `$global-lineheight` is 24px while `$global-font-size` is 16px +/// @type Number +$global-lineheight: 1.6 !default; + +/// Colors used for buttons, callouts, links, etc. There must always be a color called `primary`. +/// @type Map +$foundation-palette: ( + primary: #1A5099, + secondary: #777, + success: #3adb76, + warning: #ffae00, + alert: #ec5840, +) !default; + +/// Color used for light gray UI items. +/// @type Color +$light-gray: #e6e6e6 !default; + +/// Color used for medium gray UI items. +/// @type Color +$medium-gray: #cacaca !default; + +/// Color used for dark gray UI items. +/// @type Color +$dark-gray: #8a8a8a !default; + +//$dark-blue: #1A5099 !default; //not used yet + +$light-blue: #4ba2f2 !default; + +/// Color used for black ui items. +/// @type Color +$black: #333333 !default; + +/// Color used for white ui items. +/// @type Color +$white: #fefefe !default; + +/// Background color of the body. +/// @type Color +$body-background: $white !default; + +/// Text color of the body. +/// @type Color +$body-font-color: $black !default; + +/// Font stack of the body. +/// @type List +$body-font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif !default; + +/// Global value used for all elements that have a border radius. +/// @type Number +$global-radius: 3px !default; diff --git a/src/app/assets/stylesheets/markdown.scss b/src/app/assets/stylesheets/markdown.scss new file mode 100644 index 0000000..415a43c --- /dev/null +++ b/src/app/assets/stylesheets/markdown.scss @@ -0,0 +1,161 @@ +.Markdown { + font-family: 'Source Serif Pro', serif; + font-size: 120%; + + line-height: 150%; +} + +// used for comments +.Markdown.MarkdownViewer--small { + font-family: inherit; + font-size: 110%; + + img { + max-width: 400px; + max-height: 400px; + } + + div.videoWrapper { + max-width: 480px; + padding-bottom: 270px; + } +} + +.Markdown, .ReplyEditor__body.rte { + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; + hyphens: none; + + h1, h2, h3, h4, h5, h6 { + font-family: $body-font-family; + font-weight: 600; + } + + h1 { + margin: 2.5rem 0 .3rem; + font-size: 160% + } + h2 { + margin: 2.5rem 0 .3rem; + font-size: 140%; + } + h3 { + margin: 2rem 0 0.3rem; + font-size: 120%; + } + h4 { + margin: 1.5rem 0 0.2rem; + font-size: 110%; + } + h5 { + margin: 1rem 0 0.2rem; + font-size: 100%; + } + h6 { + margin: 1rem 0 0.2rem; + font-size: 90%; + } + + code { + padding: 0.2rem; + font-size: 85%; + border-radius: 3px; + border: none; + background-color: #F4F4F4; + font-weight: inherit; + } + + pre > code { + display: block; + } + + strong { + font-weight: 600; + } + + ol, ul { + margin-left: 2rem; + } + + table td { + word-break: normal; // issue #146 + } + + p { + font-size: 100%; + line-height: 150%; + margin: 0 0 1.5rem 0; + } + a { + @extend .link; + @extend .link--accent; + } + + img { + width: auto; + max-width: 100%; + height: auto; + max-height: none; + } + + iframe { + max-width: 100%; + max-height: 75vw; + } + + div.pull-right { + float: right; + padding-left: 1rem; + max-width: 50%; + } + + div.pull-left { + float: left; + padding-right: 1rem; + max-width: 50%; + } + + div.text-justify { + text-align: justify; + } + + div.text-right { + text-align: right; + } + + div.text-center { + text-align: center; + } + + div.text-rtl { + direction: rtl; + } + + div.videoWrapper { + width: 100%; + height: 0; + padding-bottom: 56.2%; + position: relative; + + iframe { + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + } + } + blockquote { + @include themify($themes) { + border-left: themed('borderDark'); + } + } + blockquote > p { + @include themify($themes) { + color: themed('textColorSecondary'); + } + } +} + + diff --git a/src/app/assets/stylesheets/mixins.scss b/src/app/assets/stylesheets/mixins.scss new file mode 100644 index 0000000..0b3c0b0 --- /dev/null +++ b/src/app/assets/stylesheets/mixins.scss @@ -0,0 +1,26 @@ +@mixin hoverUnderline() { + &:hover { + text-decoration: underline; + } +} + +// rem fallback - credits: http://zerosixthree.se/ + +@function calculateRem($size) { + $remSize: $size / 16px; + @return $remSize * 1rem; +} + +@mixin font-size($size) { + font-size: $size; + font-size: calculateRem($size); +} + +// Enables opacity to be safely used with older browsers + +@mixin opacity($opacity) { + opacity: $opacity; + $opacity-ie: $opacity * 100; + filter: alpha(opacity=$opacity-ie); //IE8 +} + diff --git a/src/app/assets/stylesheets/notifications.scss b/src/app/assets/stylesheets/notifications.scss new file mode 100644 index 0000000..abf9495 --- /dev/null +++ b/src/app/assets/stylesheets/notifications.scss @@ -0,0 +1,5 @@ +.notification-bar { + max-width: 40rem !important; + background-color: rgba(70, 70, 70, .7) !important; + margin-bottom: 2rem !important; +} diff --git a/src/app/client_config.js b/src/app/client_config.js new file mode 100644 index 0000000..bcea252 --- /dev/null +++ b/src/app/client_config.js @@ -0,0 +1,52 @@ +// sometimes it's impossible to use html tags to style coin name, hence usage of _UPPERCASE modifier +export const APP_NAME = 'Steemit'; +// sometimes APP_NAME is written in non-latin characters, but they are needed for technical purposes +// ie. "Голос" > "Golos" +export const APP_NAME_LATIN = 'Steemit'; +export const APP_NAME_UPPERCASE = 'STEEMIT'; +export const APP_ICON = 'steem'; +// FIXME figure out best way to do this on both client and server from env +// vars. client should read $STM_Config, server should read config package. +export const APP_DOMAIN = 'steemit.com'; +export const LIQUID_TOKEN = 'Steem'; +// sometimes it's impossible to use html tags to style coin name, hence usage of _UPPERCASE modifier +export const LIQUID_TOKEN_UPPERCASE = 'STEEM'; +export const VESTING_TOKEN = 'STEEM POWER'; +export const INVEST_TOKEN_UPPERCASE = 'STEEM POWER'; +export const INVEST_TOKEN_SHORT = 'SP'; +export const DEBT_TOKEN = 'STEEM DOLLAR'; +export const DEBT_TOKENS = 'STEEM DOLLARS'; +export const CURRENCY_SIGN = '$'; +export const WIKI_URL = ''; // https://wiki.golos.io/ +export const LANDING_PAGE_URL = 'https://steem.io/'; +export const TERMS_OF_SERVICE_URL = 'https://' + APP_DOMAIN + '/tos.html'; +export const PRIVACY_POLICY_URL = 'https://' + APP_DOMAIN + '/privacy.html'; +export const WHITEPAPER_URL = 'https://steem.io/SteemWhitePaper.pdf'; + +// these are dealing with asset types, not displaying to client, rather sending data over websocket +export const LIQUID_TICKER = 'STEEM'; +export const VEST_TICKER = 'VESTS'; +export const DEBT_TICKER = 'SBD'; +export const DEBT_TOKEN_SHORT = 'SBD'; + +// application settings +export const DEFAULT_LANGUAGE = 'en'; // used on application internationalization bootstrap +export const DEFAULT_CURRENCY = 'USD'; +export const ALLOWED_CURRENCIES = ['USD']; +export const FRACTION_DIGITS = 2; // default amount of decimal digits +export const FRACTION_DIGITS_MARKET = 3; // accurate amount of deciaml digits (example: used in market) + +// meta info +export const TWITTER_HANDLE = '@steemit'; +export const SHARE_IMAGE = 'https://' + + APP_DOMAIN + + '/images/steemit-share.png'; +export const TWITTER_SHARE_IMAGE = 'https://' + + APP_DOMAIN + + '/images/steemit-twshare.png'; +export const SITE_DESCRIPTION = 'Steemit is a social media platform where everyone gets paid for ' + + 'creating and curating content. It leverages a robust digital points system, called Steem, that ' + + 'supports real value for digital rewards through market price discovery and liquidity'; + +// various +export const SUPPORT_EMAIL = 'support@' + APP_DOMAIN; diff --git a/src/app/components/App.jsx b/src/app/components/App.jsx new file mode 100644 index 0000000..78741a1 --- /dev/null +++ b/src/app/components/App.jsx @@ -0,0 +1,341 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import AppPropTypes from 'app/utils/AppPropTypes'; +import Header from 'app/components/modules/Header'; +import LpFooter from 'app/components/modules/lp/LpFooter'; +import user from 'app/redux/User'; +import g from 'app/redux/GlobalReducer'; +import TopRightMenu from 'app/components/modules/TopRightMenu'; +import { browserHistory } from 'react-router'; +import classNames from 'classnames'; +import SidePanel from 'app/components/modules/SidePanel'; +import CloseButton from 'react-foundation-components/lib/global/close-button'; +import Dialogs from 'app/components/modules/Dialogs'; +import Modals from 'app/components/modules/Modals'; +import Icon from 'app/components/elements/Icon'; +import MiniHeader from 'app/components/modules/MiniHeader'; +import tt from 'counterpart'; +import PageViewsCounter from 'app/components/elements/PageViewsCounter'; +import {serverApiRecordEvent} from 'app/utils/ServerApiClient'; +import { APP_NAME, VESTING_TOKEN, LIQUID_TOKEN } from 'app/client_config'; +import {key_utils} from 'steem/lib/auth/ecc'; +import resolveRoute from 'app/ResolveRoute'; + +const pageRequiresEntropy = (path) => { + const {page} = resolveRoute(path); + const entropyPages = [ + "ChangePassword", "RecoverAccountStep1", "RecoverAccountStep2", + "UserProfile", "CreateAccount" + ]; + /* Returns true if that page requires the entropy collection listener */ + return entropyPages.indexOf(page) !== -1 +} + +class App extends React.Component { + constructor(props) { + super(props); + this.state = {open: null, showCallout: true, showBanner: true, expandCallout: false}; + this.toggleOffCanvasMenu = this.toggleOffCanvasMenu.bind(this); + this.signUp = this.signUp.bind(this); + this.learnMore = this.learnMore.bind(this); + this.listenerActive = null; + this.onEntropyEvent = this.onEntropyEvent.bind(this); + // this.shouldComponentUpdate = shouldComponentUpdate(this, 'App') + } + + componentWillMount() { + if (process.env.BROWSER) localStorage.removeItem('autopost') // July 14 '16 compromise, renamed to autopost2 + this.props.loginUser(); + } + + componentDidMount() { + // setTimeout(() => this.setState({showCallout: false}), 15000); + if (pageRequiresEntropy(this.props.location.pathname)) { + this._addEntropyCollector(); + } + } + + componentWillReceiveProps(np) { + /* Add listener if the next page requires entropy and the current page didn't */ + if (pageRequiresEntropy(np.location.pathname) && !pageRequiresEntropy(this.props.location.pathname)) { + this._addEntropyCollector(); + } else if (!pageRequiresEntropy(np.location.pathname)) { // Remove if next page does not require entropy + this._removeEntropyCollector(); + } + } + + _addEntropyCollector() { + if (!this.listenerActive && this.refs.App_root) { + this.refs.App_root.addEventListener("mousemove", this.onEntropyEvent, {capture: false, passive: true}); + this.listenerActive = true; + } + } + + _removeEntropyCollector() { + if (this.listenerActive && this.refs.App_root) { + this.refs.App_root.removeEventListener("mousemove", this.onEntropyEvent); + this.listenerActive = null; + } + } + + shouldComponentUpdate(nextProps, nextState) { + const p = this.props; + const n = nextProps; + return ( + p.location.pathname !== n.location.pathname || + p.new_visitor !== n.new_visitor || + p.flash !== n.flash || + this.state.open !== nextState.open || + this.state.showBanner !== nextState.showBanner || + this.state.showCallout !== nextState.showCallout || + p.nightmodeEnabled !== n.nightmodeEnabled + ); + } + + toggleOffCanvasMenu(e) { + e.preventDefault(); + // this.setState({open: this.state.open ? null : 'left'}); + this.refs.side_panel.show(); + } + + handleClose = () => this.setState({open: null}); + + navigate = (e) => { + const a = e.target.nodeName.toLowerCase() === 'a' ? e.target : e.target.parentNode; + // this.setState({open: null}); + if (a.host !== window.location.host) return; + e.preventDefault(); + browserHistory.push(a.pathname + a.search + a.hash); + }; + + onEntropyEvent(e) { + if(e.type === 'mousemove') + key_utils.addEntropy(e.pageX, e.pageY, e.screenX, e.screenY) + else + console.log('onEntropyEvent Unknown', e.type, e) + } + + signUp() { + serverApiRecordEvent('Sign up', 'Hero banner'); + } + + learnMore() { + serverApiRecordEvent('Learn more', 'Hero banner'); + } + + render() { + const {location, params, children, flash, new_visitor, + depositSteem, signup_bonus, username, nightmodeEnabled} = this.props; + const lp = false; //location.pathname === '/'; + const miniHeader = location.pathname === '/create_account' || location.pathname === '/pick_account'; + const headerHidden = miniHeader && location.search === '?whistle_signup' + const params_keys = Object.keys(params); + const ip = location.pathname === '/' || (params_keys.length === 2 && params_keys[0] === 'order' && params_keys[1] === 'category'); + const alert = this.props.error || flash.get('alert') || flash.get('error'); + const warning = flash.get('warning'); + const success = flash.get('success'); + let callout = null; + if (this.state.showCallout && (alert || warning || success)) { + callout =
+
+
+ this.setState({showCallout: false})} /> +

{alert || warning || success}

+
+
+
; + } + else if (false && ip && this.state.showCallout) { + callout =
+
+
+ this.setState({showCallout: false})} /> + +
+
+
+ } + if ($STM_Config.read_only_mode && this.state.showCallout) { + callout =
+
+
+ this.setState({showCallout: false})} /> +

{tt('g.read_only_mode')}

+
+
+
; + } + + let welcome_screen = null; + if (ip && new_visitor && this.state.showBanner) { + welcome_screen = ( +
+
+ this.setState({showBanner: false})} /> +
+

{tt('navigation.intro_tagline')}

+

{tt('navigation.intro_paragraph')}

+
+ {tt('navigation.sign_up')} + {/* JSX Comment       + {tt('navigation.learn_more')} */} +
+
+
+ ); + } + + const themeClass = nightmodeEnabled ? ' theme-dark' : ' theme-light'; + + return
+ + + + + + + {miniHeader ? headerHidden ? null : :
} +
+ {welcome_screen} + {callout} + {children} + {lp ? : null} +
+ + + +
+ } +} + +App.propTypes = { + error: React.PropTypes.string, + children: AppPropTypes.Children, + location: React.PropTypes.object, + signup_bonus: React.PropTypes.string, + loginUser: React.PropTypes.func.isRequired, + depositSteem: React.PropTypes.func.isRequired, + username: React.PropTypes.string, +}; + +export default connect( + state => { + return { + error: state.app.get('error'), + flash: state.offchain.get('flash'), + signup_bonus: state.offchain.get('signup_bonus'), + new_visitor: !state.user.get('current') && + !state.offchain.get('user') && + !state.offchain.get('account') && + state.offchain.get('new_visit'), + username: state.user.getIn(['current', 'username']) || state.offchain.get('account') || '', + nightmodeEnabled: state.app.getIn(['user_preferences', 'nightmode']), + }; + }, + dispatch => ({ + loginUser: () => + dispatch(user.actions.usernamePasswordLogin()), + depositSteem: (username) => { + const new_window = window.open(); + new_window.opener = null; + new_window.location = 'https://blocktrades.us/?input_coin_type=btc&output_coin_type=steem&receive_address=' + username; + //dispatch(g.actions.showDialog({name: 'blocktrades_deposit', params: {outputCoinType: 'VESTS'}})); + }, + }) +)(App); \ No newline at end of file diff --git a/src/app/components/App.scss b/src/app/components/App.scss new file mode 100644 index 0000000..3beda2a --- /dev/null +++ b/src/app/components/App.scss @@ -0,0 +1,154 @@ +.App { + min-height: 100vh; + padding-top: 50px; +} + +.App__content { + margin-top: 1rem; + + @media print, screen and (min-width: 52.5em) { + margin-top: 2.5rem; + } + + // /* Small only */ + // @media screen and (max-width: 39.9375em) { + // margin-top: 2.25rem; + // } + // /* Medium only */ + // @media screen and (min-width: 40em) and (max-width: 63.9375em) { + // margin-top: 3rem; + // } + // /* Large and above */ + // @media screen and (min-width: 64.063em) { + // margin-top: 3rem; + +} + +.mini-header .App__content { + margin-top: 0; +} + +.welcomeWrapper { + margin-top: -1rem; + padding-bottom: 1rem; +} +.welcomeBanner { + padding: 4em 0 4em; + background-color: $color-blue-black; + color: $color-white; + // background-image: url(../images/lp-bottom.jpg); + // background-repeat: repeat-x; + // background-position:bottom; + //background-attachment:fixed; +} + +.RightMenu { + background-color: #555; + height: 100vh; + color: #fff; + padding-top: 3rem; + .close-button { + color: #fff; + } + .menu > li { + > a { + color: #fff; + border-top: 1px solid #777; + } + > a:hover { + background-color: #666; + } + } + .menu > li.last { + border-bottom: 1px solid #777; + } + .button.hollow { + color: #fff; + border: none; + } +} +.PlainLink { + @extend .link; + @extend .link--secondary; +} + +.welcomeBanner { + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); + h2 { + font-weight: bold; + margin-bottom: 16px; + @include font-size(48px); + font-family: helvetica, sans-serif; + } + h4 { + color: $color-white; + font-weight: 500; + margin-bottom: 1rem; + width: 84%; + max-width: 440px; + margin: 0 auto; + font-family: helvetica, sans-serif; + @include font-size(18px); + } + .button { + + min-width: 240px; + + text-decoration: none; + font-weight: bold; + transition: 0.2s all ease-in-out; + text-transform: capitalize; + border-radius: 0; + font-size: 18px; + background-color: $color-white; + color: $color-blue-black; + border: none; + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0), 5px 5px 0 0 $color-teal; + padding: 16px 22px; + @include font-size(22px); + cursor: pointer; + &:hover, &:focus { + background-color: $color-teal; + color: $color-white; + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0.1), 7px 7px 0 0 $color-white; + text-shadow: 0 1px 0 rgba(0,0,0,0.20); + } + } + .close-button { + top: 0.5rem; + right: 0; + } + .button.hollow { + background-color: rgba(255,255,255,0.5); + } +} +.downvoted { + opacity: 0.5; + -webkit-filter: grayscale(1); // image + filter: gray; // image grayscale + transition: 0.2s all ease-in-out; + color: #848282; + @include themify($themes) { + color: themed('textColorPrimary'); + } + .Comment__header-user { + color: #848282; + @include themify($themes) { + color: themed('textColorPrimary'); + } + } +} +.downvoted:hover { + opacity: 1; + filter: none; + -webkit-filter: none; +} + +.App__announcement { + padding-right: 40px; + padding-top: 40px; + .close-button { + right: 0; + } +} diff --git a/src/app/components/all.scss b/src/app/components/all.scss new file mode 100644 index 0000000..e5b33d8 --- /dev/null +++ b/src/app/components/all.scss @@ -0,0 +1,65 @@ +@import "./App"; + +// cards +@import "./cards/Comment"; +@import "./cards/MarkdownViewer"; +@import "./cards/PostSummary"; +@import "./cards/PostFull"; +@import "./cards/PostsList"; + +// elements +@import "./elements/Icon"; +@import "./elements/LoadingIndicator"; +@import "./elements/Userpic"; +@import "./elements/Voting"; +@import "./elements/FormattedAsset"; +@import "./elements/ReplyEditor"; +@import "./elements/NotifiCounter"; +@import "./elements/DropdownMenu"; +@import "./elements/FoundationDropdownMenu"; +@import "./elements/VerticalMenu"; +@import "./elements/HorizontalMenu"; +@import "./elements/VotesAndComments"; +@import "./elements/GeneratedPasswordInput"; +@import "./elements/TagList"; +@import "./elements/ChangePassword"; +@import "./elements/Reputation"; +@import "./elements/Reblog"; +@import "./elements/YoutubePreview"; +@import "./elements/SignupProgressBar"; +@import "./elements/ShareMenu"; +@import "./elements/Author"; +@import "./elements/AuthorDropdown"; +@import "./elements/UserNames"; +@import "./elements/UserKeys"; +@import "./elements/QrKeyView"; + +// modules +@import "./modules/Header"; +@import "./modules/Footer"; +@import "./modules/lp/LpHeader"; +@import "./modules/lp/LpFooter"; +@import "./modules/SignUp"; +@import "./modules/LoginForm"; +@import "./modules/MiniHeader"; +@import "./modules/SidePanel"; +@import "./modules/Settings"; +@import "./modules/BottomPanel"; +@import "./modules/UserWallet"; +@import "./modules/Powerdown"; +@import "./modules/TopRightMenu"; +@import "./modules/ConfirmTransactionForm"; + +// pages +@import "./pages/PostsIndex"; +@import "./pages/Topics"; +@import "./pages/CreateAccount"; +@import "./pages/Post"; +@import "./pages/Privacy"; +@import "./pages/Tos"; +@import "./pages/UserProfile"; +@import "./pages/Market"; +@import "./pages/TagsIndex"; +@import "./pages/Welcome"; +@import "./pages/RecoverAccountStep1"; +@import "./pages/Witnesses"; diff --git a/src/app/components/cards/CardView.js b/src/app/components/cards/CardView.js new file mode 100644 index 0000000..d05d187 --- /dev/null +++ b/src/app/components/cards/CardView.js @@ -0,0 +1,73 @@ +import React from 'react'; +import {connect} from 'react-redux' +import Link from 'app/components/elements/Link' +import g from 'app/redux/GlobalReducer' +import links from 'app/utils/Links' +import tt from 'counterpart'; + +/** @deprecated */ +class CardView extends React.Component { + static propTypes = { + // HTML properties + formId: React.PropTypes.string, + canEdit: React.PropTypes.bool, + + // redux or html + metaLinkData: React.PropTypes.object, + + // redux + clearMetaElement: React.PropTypes.func, + } + static defaultProps = { + canEdit: false + } + constructor() { + super() + this.onCloseImage = (e) => { + e.preventDefault() + const {formId, clearMetaElement} = this.props + clearMetaElement(formId, 'image') + } + this.onCloseDescription = (e) => { + e.preventDefault() + const {formId, clearMetaElement} = this.props + clearMetaElement(formId, 'description') + } + } + render() { + const {metaLinkData, canEdit} = this.props + if(!metaLinkData) return + const {link, image, description, alt} = metaLinkData + // http://postimg.org/image/kbefrpbe9/ + if(!image && !description) return + // youTubeImages have their own preview + const youTubeImage = links.youTube.test(link) + return ( + {image && !youTubeImage &&
+ {canEdit &&
({tt('g.remove')})
} + + {alt} + +
} + {description &&
+ {canEdit && ({tt('g.remove')})} + +
{description}
+ +
} +
) + } +} +export default connect( + (state, ownProps) => { + // const {text} = ownProps + const formId = ownProps.formId + const metaLinkData = state.global.getIn(['metaLinkData', formId]) + return {metaLinkData, ...ownProps}; + }, + dispatch => ({ + clearMetaElement: (formId, element) => { + dispatch(g.actions.clearMetaElement({formId, element})) + } + }) +)(CardView) diff --git a/src/app/components/cards/CategorySelector.jsx b/src/app/components/cards/CategorySelector.jsx new file mode 100644 index 0000000..e8d279e --- /dev/null +++ b/src/app/components/cards/CategorySelector.jsx @@ -0,0 +1,97 @@ +import React from 'react'; +import {connect} from 'react-redux' +import shouldComponentUpdate from 'app/utils/shouldComponentUpdate' +import {cleanReduxInput} from 'app/utils/ReduxForms' +import tt from 'counterpart'; + +class CategorySelector extends React.Component { + static propTypes = { + // HTML props + id: React.PropTypes.string, // DOM id for active component (focusing, etc...) + autoComplete: React.PropTypes.string, + placeholder: React.PropTypes.string, + onChange: React.PropTypes.func.isRequired, + onBlur: React.PropTypes.func.isRequired, + isEdit: React.PropTypes.bool, + disabled: React.PropTypes.bool, + value: React.PropTypes.string, + tabIndex: React.PropTypes.number, + + // redux connect (overwrite in HTML) + trending: React.PropTypes.object.isRequired, // Immutable.List + } + static defaultProps = { + autoComplete: 'on', + id: 'CategorySelectorId', + isEdit: false, + } + constructor() { + super() + this.state = {createCategory: true} + this.shouldComponentUpdate = shouldComponentUpdate(this, 'CategorySelector') + this.categoryCreateToggle = (e) => { + e.preventDefault() + this.props.onChange() + this.setState({ createCategory: !this.state.createCategory }) + setTimeout(() => this.refs.categoryRef.focus(), 300) + } + this.categorySelectOnChange = (e) => { + e.preventDefault() + const {value} = e.target + const {onBlur} = this.props // call onBlur to trigger validation immediately + if (value === 'new') { + this.setState({createCategory: true}) + setTimeout(() => { if(onBlur) onBlur(); this.refs.categoryRef.focus() }, 300) + } else + this.props.onChange(e) + } + } + render() { + const {trending, tabIndex, disabled} = this.props + const categories = trending.slice(0, 11).filterNot(c => validateCategory(c)) + const {createCategory} = this.state + + const categoryOptions = categories.map((c, idx) => + ) + + const impProps = {...this.props} + const categoryInput = + + + const categorySelect = ( + + ) + return ( + + {createCategory ? categoryInput : categorySelect} + + ) + } +} +export function validateCategory(category, required = true) { + if(!category || category.trim() === '') return required ? tt('g.required') : null + const cats = category.trim().split(' ') + return ( + // !category || category.trim() === '' ? 'Required' : + cats.length > 5 ? tt('category_selector_jsx.use_limited_amount_of_categories', {amount: 5}) : + cats.find(c => c.length > 24) ? tt('category_selector_jsx.maximum_tag_length_is_24_characters') : + cats.find(c => c.split('-').length > 2) ? tt('category_selector_jsx.use_one_dash') : + cats.find(c => c.indexOf(',') >= 0) ? tt('category_selector_jsx.use_spaces_to_separate_tags') : + cats.find(c => /[A-Z]/.test(c)) ? tt('category_selector_jsx.use_only_lowercase_letters') : + cats.find(c => !/^[a-z0-9-#]+$/.test(c)) ? tt('category_selector_jsx.use_only_allowed_characters') : + cats.find(c => !/^[a-z-#]/.test(c)) ? tt('category_selector_jsx.must_start_with_a_letter') : + cats.find(c => !/[a-z0-9]$/.test(c)) ? tt('category_selector_jsx.must_end_with_a_letter_or_number') : + null + ) +} +export default connect((state, ownProps) => { + const trending = state.global.getIn(['tag_idx', 'trending']) + // apply translations + // they are used here because default prop can't acces intl property + const placeholder = tt('category_selector_jsx.tag_your_story'); + return { trending, placeholder, ...ownProps, } +})(CategorySelector); diff --git a/src/app/components/cards/Comment.jsx b/src/app/components/cards/Comment.jsx new file mode 100644 index 0000000..85180bc --- /dev/null +++ b/src/app/components/cards/Comment.jsx @@ -0,0 +1,425 @@ +import React from 'react'; +import Author from 'app/components/elements/Author'; +import ReplyEditor from 'app/components/elements/ReplyEditor'; +import MarkdownViewer from 'app/components/cards/MarkdownViewer'; +import shouldComponentUpdate from 'app/utils/shouldComponentUpdate' +import Voting from 'app/components/elements/Voting'; +import { connect } from 'react-redux'; +import { Link } from 'react-router'; +import user from 'app/redux/User'; +import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper'; +import Userpic from 'app/components/elements/Userpic'; +import transaction from 'app/redux/Transaction' +import {List} from 'immutable' +import tt from 'counterpart'; +import {parsePayoutAmount} from 'app/utils/ParsersAndFormatters'; +import {Long} from 'bytebuffer'; +import ImageUserBlockList from 'app/utils/ImageUserBlockList'; + +// returns true if the comment has a 'hide' flag AND has no descendants w/ positive payout +function hideSubtree(cont, c) { + return cont.getIn([c, 'stats', 'hide']) && !hasPositivePayout(cont, c) +} + +function hasPositivePayout(cont, c) { + const post = cont.get(c) + if(post.getIn(['stats', 'hasPendingPayout'])) { + return true; + } + if(post.get('replies').find(reply => hasPositivePayout(cont, reply))) { + return true; + } + return false; +} + + +export function sortComments( cont, comments, sort_order ) { + function netNegative(a) { + return a.get("net_rshares") < 0; + } + function totalPayout(a) { + return parsePayoutAmount(a.get('pending_payout_value')) + + parsePayoutAmount(a.get('total_payout_value')) + + parsePayoutAmount(a.get('curator_payout_value')); + } + function netRshares(a) { + return Long.fromString(String(a.get('net_rshares'))) + } + function countUpvotes(a) { + return a.get('active_votes').filter(vote => vote.get('percent') > 0).size + } + + /** sorts replies by upvotes, age, or payout */ + const sort_orders = { + votes: (a, b) => { + const aactive = countUpvotes(cont.get(a)) + const bactive = countUpvotes(cont.get(b)) + return bactive - aactive; + }, + new: (a, b) => { + const acontent = cont.get(a); + const bcontent = cont.get(b); + if (netNegative(acontent)) { + return 1; + } else if (netNegative(bcontent)) { + return -1; + } + const aactive = Date.parse( acontent.get('created') ); + const bactive = Date.parse( bcontent.get('created') ); + return bactive - aactive; + }, + trending: (a, b) => { + const acontent = cont.get(a); + const bcontent = cont.get(b); + if (netNegative(acontent)) { + return 1; + } else if (netNegative(bcontent)) { + return -1; + } + const apayout = totalPayout(acontent) + const bpayout = totalPayout(bcontent) + if(apayout !== bpayout) { + return bpayout - apayout; + } + // If SBD payouts were equal, fall back to rshares sorting + return netRshares(bcontent).compare(netRshares(acontent)) + } + } + comments.sort( sort_orders[sort_order] ); +} + +class CommentImpl extends React.Component { + static propTypes = { + // html props + cont: React.PropTypes.object.isRequired, + content: React.PropTypes.string.isRequired, + sort_order: React.PropTypes.oneOf(['votes', 'new', 'trending']).isRequired, + root: React.PropTypes.bool, + showNegativeComments: React.PropTypes.bool, + onHide: React.PropTypes.func, + noImage: React.PropTypes.bool, + + // component props (for recursion) + depth: React.PropTypes.number, + + // redux props + username: React.PropTypes.string, + rootComment: React.PropTypes.string, + anchor_link: React.PropTypes.string.isRequired, + deletePost: React.PropTypes.func.isRequired, + }; + static defaultProps = { + depth: 1, + } + + constructor() { + super(); + this.state = {collapsed: false, hide_body: false, highlight: false}; + this.revealBody = this.revealBody.bind(this); + this.shouldComponentUpdate = shouldComponentUpdate(this, 'Comment') + this.onShowReply = () => { + const {showReply} = this.state + this.setState({showReply: !showReply, showEdit: false}) + this.saveOnShow(!showReply ? 'reply' : null) + } + this.onShowEdit = () => { + const {showEdit} = this.state + this.setState({showEdit: !showEdit, showReply: false}) + this.saveOnShow(!showEdit ? 'edit' : null) + } + this.saveOnShow = (type) => { + if(process.env.BROWSER) { + const {cont} = this.props; + const content = cont.get(this.props.content) + const formId = content.get('author') + '/' + content.get('permlink') + if(type) + localStorage.setItem('showEditor-' + formId, JSON.stringify({type}, null, 0)) + else { + localStorage.removeItem('showEditor-' + formId) + localStorage.removeItem('replyEditorData-' + formId + '-reply') + localStorage.removeItem('replyEditorData-' + formId + '-edit') + } + } + } + this.saveOnShow = this.saveOnShow.bind(this) + this.onDeletePost = () => { + const {props: {deletePost}} = this + const content = this.props.cont.get(this.props.content); + deletePost(content.get('author'), content.get('permlink')) + } + this.toggleCollapsed = this.toggleCollapsed.bind(this); + } + + componentWillMount() { + this.initEditor(this.props) + this._checkHide(this.props); + } + + componentDidMount() { + // Jump to comment via hash (note: comment element's id has a hash(#) in it) + if (window.location.hash == this.props.anchor_link) { + const comment_el = document.getElementById(this.props.anchor_link) + if (comment_el) { + comment_el.scrollIntoView(true) + const scrollingEl = document.scrollingElement || document.documentElement; + scrollingEl.scrollTop -= 100; + this.setState({highlight: true}); + } + } + } + + //componentWillReceiveProps(np) { + // this._checkHide(np); + //} + + /** + * - `hide` is based on author reputation, and will hide the entire post on initial render. + * - `hide_body` is true when comment rshares OR author rep is negative. + * it hides the comment body (but not the header) until the "reveal comment" link is clicked. + */ + _checkHide(props) { + const content = props.cont.get(props.content); + if (content) { + const hide = hideSubtree(props.cont, props.content) + const gray = content.getIn(['stats', 'gray']) + if(hide) { + const {onHide} = this.props + // console.log('Comment --> onHide') + if(onHide) onHide() + } + this.setState({hide, hide_body: hide || gray}) + } + } + + toggleCollapsed() { + this.setState({collapsed: !this.state.collapsed}); + } + revealBody() { + this.setState({hide_body: false}); + } + initEditor(props) { + if(this.state.PostReplyEditor) return + const {cont} = this.props; + const content = cont.get(props.content); + if (!content) return + const post = content.get('author') + '/' + content.get('permlink') + const PostReplyEditor = ReplyEditor(post + '-reply') + const PostEditEditor = ReplyEditor(post + '-edit') + if(process.env.BROWSER) { + const formId = post + let showEditor = localStorage.getItem('showEditor-' + formId) + if(showEditor) { + showEditor = JSON.parse(showEditor) + if(showEditor.type === 'reply') { + this.setState({showReply: true}) + } + if(showEditor.type === 'edit') { + this.setState({showEdit: true}) + } + } + } + this.setState({PostReplyEditor, PostEditEditor}) + } + render() { + const {cont} = this.props; + const dis = cont.get(this.props.content); + if (!dis) { + return
{tt('g.loading')}...
+ } + const comment = dis.toJS(); + if(!comment.stats) { + console.error('Comment -- missing stats object') + comment.stats = {} + } + const {allowDelete, authorRepLog10, gray} = comment.stats + const {author, json_metadata} = comment + const {username, depth, anchor_link, + showNegativeComments, ignore_list, noImage} = this.props + const {onShowReply, onShowEdit, onDeletePost} = this + const post = comment.author + '/' + comment.permlink + const {PostReplyEditor, PostEditEditor, showReply, showEdit, hide, hide_body} = this.state + const Editor = showReply ? PostReplyEditor : PostEditEditor + + let {rootComment} = this.props + if(!rootComment && depth === 1) { + rootComment = comment.parent_author + '/' + comment.parent_permlink; + } + const comment_link = `/${comment.category}/@${rootComment}#@${comment.author}/${comment.permlink}` + const ignore = ignore_list && ignore_list.has(comment.author) + + if(!showNegativeComments && (hide || ignore)) { + return null; + } + + let jsonMetadata = null + try { + if(!showReply) jsonMetadata = JSON.parse(json_metadata) + } catch(error) { + // console.error('Invalid json metadata string', json_metadata, 'in post', this.props.content); + } + // const get_asset_value = ( asset_str ) => { return parseFloat( asset_str.split(' ')[0] ); } + // const steem_supply = this.props.global.getIn(['props','current_supply']); + + // hide images if author is in blacklist + const hideImages = ImageUserBlockList.includes(author) + + const showDeleteOption = username === author && allowDelete + const showEditOption = username === author + const showReplyOption = comment.depth < 255 + const archived = comment.cashout_time === '1969-12-31T23:59:59' // TODO: audit after HF19. #1259 + const readonly = archived || $STM_Config.read_only_mode + + let body = null; + let controls = null; + + if (!this.state.collapsed && !hide_body) { + body = (); + controls = (
+ + + {showReplyOption && {tt('g.reply')}} + {' '}{!readonly && showEditOption && {tt('g.edit')}} + {' '}{!readonly && showDeleteOption && {tt('g.delete')}} + +
); + } + + let replies = null; + if(!this.state.collapsed && comment.children > 0) { + if(depth > 7) { + const comment_permlink = `/${comment.category}/@${comment.author}/${comment.permlink}` + replies = Show {comment.children} more {comment.children == 1 ? 'reply' : 'replies'} + } else { + replies = comment.replies; + sortComments( cont, replies, this.props.sort_order ); + // When a comment has hidden replies and is collapsed, the reply count is off + //console.log("replies:", replies.length, "num_visible:", replies.filter( reply => !cont.get(reply).getIn(['stats', 'hide'])).length) + replies = replies.map((reply, idx) => ( + ) + ); + } + } + + const commentClasses = ['hentry'] + commentClasses.push('Comment') + commentClasses.push(this.props.root ? 'root' : 'reply') + if(hide_body || this.state.collapsed) commentClasses.push('collapsed'); + + let innerCommentClass = ignore || gray ? 'downvoted' : '' + if(this.state.highlight) innerCommentClass += ' highlighted' + + //console.log(comment); + let renderedEditor = null; + if (showReply || showEdit) { + renderedEditor = (
+ { + this.setState({showReply: false, showEdit: false}) + this.saveOnShow(null) + }} + onCancel={() => { + this.setState({showReply: false, showEdit: false}) + this.saveOnShow(null) + }} + jsonMetadata={jsonMetadata} + /> +
) + } + + const depth_indicator = []; + if (depth > 1) { + for (let i = 1; i < depth; ++i) { + depth_indicator.push(
·
); + } + } + + return ( +
+ {depth_indicator} +
+
+ +
+
+ + +
+ +
+ +
+   ·   + + + + { (this.state.collapsed || hide_body) && + } + { this.state.collapsed && comment.children > 0 && + {tt('g.reply_count', {count: comment.children})}} + { !this.state.collapsed && hide_body && + {tt('g.reveal_comment')}} +
+
+ {showEdit ? renderedEditor : body} +
+
+ {controls} +
+
+
+ {showReply && renderedEditor} + {replies} +
+
+ ); + } +} + +const Comment = connect( + // mapStateToProps + (state, ownProps) => { + const {content} = ownProps + + const username = state.user.getIn(['current', 'username']) + const ignore_list = username ? state.global.getIn(['follow', 'getFollowingAsync', username, 'ignore_result']) : null + + return { + ...ownProps, + anchor_link: '#@' + content, // Using a hash here is not standard but intentional; see issue #124 for details + username, + ignore_list + } + }, + + // mapDispatchToProps + dispatch => ({ + unlock: () => { dispatch(user.actions.showLogin()) }, + deletePost: (author, permlink) => { + dispatch(transaction.actions.broadcastOperation({ + type: 'delete_comment', + operation: {author, permlink}, + confirm: tt('g.are_you_sure'), + })) + }, + }) +)(CommentImpl) +export default Comment; diff --git a/src/app/components/cards/Comment.scss b/src/app/components/cards/Comment.scss new file mode 100644 index 0000000..74120e7 --- /dev/null +++ b/src/app/components/cards/Comment.scss @@ -0,0 +1,218 @@ +.Comment { + position: relative; + clear: both; + margin-bottom: 2.4rem; + .Markdown { + p { + margin: 0.1rem 0 0.6rem 0; + } + + p:last-child { + margin-bottom: 0.2rem; + } + } +} + +.Comment__Userpic { + float: left; + position: relative; + top: 0.4rem; + padding-right: 0.6rem; + @include MQ(M) { + top: 0.2rem; + } +} + +.Comment__Userpic-small { + @include hide-for(medium); + .Userpic { + float: left; + position: relative; + top: 3px; + margin-right: 0.2rem; + width: 16px !important; + height: 16px !important; + } +} + +.Comment .highlighted { + padding: 14px; + @include themify($themes) { + border: themed('borderAccent'); + background-color: themed('highlightBackgroundColor'); + } +} + +.Comment.collapsed { + > .Comment__Userpic { + top: 0; + left: 26px; + .Userpic { + width: 24px !important; + height: 24px !important; + } + } + .Comment__header { + .Voting { + margin-left: 1rem; + border-right: none; + } + a { + @include themify($themes) { + color: themed('textColorSecondary'); + } + } + } +} + +.Comment__header { + margin-left: 62px; +} + +.Comment__header-user { + color: $black; + font-size: 100%; + font-weight: 600; + a { + @extend .link; + @extend .link--primary; + } +} + + + +.Comment__header_collapse { + float: right; + > a { + color: $medium-gray; + letter-spacing: 0.1rem; + padding: 0 0.5rem; + } + .Icon { + top: 5px; + } +} + +.Comment__body { + margin-left: 62px; + max-width: 50rem; +} + +.Comment__footer { + margin-left: 62px; + @include themify($themes) { + color: themed('textColorPrimary'); + } + a { + @extend .link; + @extend .link--primary; + } + .Voting__voters_list { + @include themify($themes) { + border-right: themed('border'); + } + padding-right: 1rem; + margin-right: 1rem; + } + .Comment__footer__controls { + a {margin: 0 0.2rem;} + } +} + +.Comment__replies { + margin-top: 1.4rem; + margin-left: 62px; + .Comment { + margin-bottom: 1.4rem; + } +} + +.Comment__negative_group { + color: $medium-gray; + border-top: 1px solid $light-gray; + padding-top: 1rem; + clear: none; + button { + opacity: 0.35; + &:hover {opacity: 0.5;} + } +} + +.depth { + position: absolute; + top: 20px; + left: 0; + color: $dark-gray; +} + +.Comment.collapsed > .depth { + top: 2px; +} + +.depth.di-1 { + left: -38px; +} + +.depth.di-2 { + left: -100px; +} + +.depth.di-3 { + left: -162px; +} + +.depth.di-4 { + left: -224px; +} + +.depth.di-5 { + left: -286px; +} + +.depth.di-6 { + left: -348px; +} + +@media screen and (max-width: 39.9375em) { + .root { + .Comment__header, .Comment__footer, .Comment__body, .Comment__replies { + margin-left: 0; + } + } + .reply { + .Comment__header, .Comment__footer, .Comment__body, .Comment__replies { + margin-left: 20px; + } + } + + .depth { + top: 2px; + } + + .depth.di-1 { + left: 0; + } + + .depth.di-2 { + left: -20px; + } + + .depth.di-3 { + left: -40px; + } + + .depth.di-4 { + left: -60px; + } + + .depth.di-5 { + left: -80px; + } + + .depth.di-6 { + left: -100px; + } + .Comment .highlighted { + padding-left: 0; + } +} diff --git a/src/app/components/cards/MarkdownViewer.jsx b/src/app/components/cards/MarkdownViewer.jsx new file mode 100644 index 0000000..044d021 --- /dev/null +++ b/src/app/components/cards/MarkdownViewer.jsx @@ -0,0 +1,165 @@ +import React from 'react'; +import {connect} from 'react-redux' +import {Component} from 'react' +import Remarkable from 'remarkable' +import YoutubePreview from 'app/components/elements/YoutubePreview' +import sanitizeConfig, {noImageText} from 'app/utils/SanitizeConfig' +import sanitize from 'sanitize-html' +import HtmlReady from 'shared/HtmlReady' +import tt from 'counterpart'; + +const remarkable = new Remarkable({ + html: true, // remarkable renders first then sanitize runs... + breaks: true, + linkify: false, // linkify is done locally + typographer: false, // https://github.com/jonschlinkert/remarkable/issues/142#issuecomment-221546793 + quotes: '“”‘’' +}) + +class MarkdownViewer extends Component { + static propTypes = { + // HTML properties + text: React.PropTypes.string, + className: React.PropTypes.string, + large: React.PropTypes.bool, + // formId: React.PropTypes.string, // This is unique for every editor of every post (including reply or edit) + canEdit: React.PropTypes.bool, + jsonMetadata: React.PropTypes.object, + highQualityPost: React.PropTypes.bool, + noImage: React.PropTypes.bool, + allowDangerousHTML: React.PropTypes.bool, + hideImages: React.PropTypes.bool, // whether to replace images with just a span containing the src url + // used for the ImageUserBlockList + } + + static defaultProps = { + className: '', + large: false, + allowDangerousHTML: false, + hideImages: false, + } + + constructor() { + super() + this.state = {allowNoImage: true} + } + + shouldComponentUpdate(np, ns) { + return np.text !== this.props.text || + np.large !== this.props.large || + // np.formId !== this.props.formId || + np.canEdit !== this.props.canEdit || + ns.allowNoImage !== this.state.allowNoImage + } + + onAllowNoImage = () => { + this.setState({allowNoImage: false}) + } + + render() { + const {noImage, hideImages} = this.props + const {allowNoImage} = this.state + let {text} = this.props + if (!text) text = '' // text can be empty, still view the link meta data + const {large, /*formId, canEdit, jsonMetadata,*/ highQualityPost} = this.props + + let html = false; + // See also ReplyEditor isHtmlTest + const m = text.match(/^([\S\s]*)<\/html>$/); + if (m && m.length === 2) { + html = true; + text = m[1]; + } else { + // See also ReplyEditor isHtmlTest + html = /^

[\S\s]*<\/p>/.test(text) + } + + // Strip out HTML comments. "JS-DOS" bug. + text = text.replace(/|$)/g, '(html comment removed: $1)') + + let renderedText = html ? text : remarkable.render(text) + + // Embed videos, link mentions and hashtags, etc... + if(renderedText) renderedText = HtmlReady(renderedText, {hideImages}).html + + // Complete removal of javascript and other dangerous tags.. + // The must remain as close as possible to dangerouslySetInnerHTML + let cleanText = renderedText + if (this.props.allowDangerousHTML === true) { + console.log('WARN\tMarkdownViewer rendering unsanitized content') + } else { + cleanText = sanitize(renderedText, sanitizeConfig({large, highQualityPost, noImage: noImage && allowNoImage})) + } + + if(/<\s*script/ig.test(cleanText)) { + // Not meant to be complete checking, just a secondary trap and red flag (code can change) + console.error('Refusing to render script tag in post text', cleanText) + return

+ } + + const noImageActive = cleanText.indexOf(noImageText) !== -1 + + // In addition to inserting the youtube compoennt, this allows react to compare separately preventing excessive re-rendering. + let idx = 0 + const sections = [] + + // HtmlReady inserts ~~~ embed:${id} type ~~~ + for(let section of cleanText.split('~~~ embed:')) { + const match = section.match(/^([A-Za-z0-9\_\-]+) (youtube|vimeo) ~~~/) + if(match && match.length >= 3) { + const id = match[1] + const type = match[2] + const w = large ? 640 : 480, + h = large ? 360 : 270 + if(type === 'youtube') { + sections.push( + + ) + } else if(type === 'vimeo') { + const url = `https://player.vimeo.com/video/${id}` + sections.push( +
+ +
+ } +} diff --git a/src/app/components/elements/YoutubePreview.scss b/src/app/components/elements/YoutubePreview.scss new file mode 100644 index 0000000..b9050b3 --- /dev/null +++ b/src/app/components/elements/YoutubePreview.scss @@ -0,0 +1,22 @@ +.youtube { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + transition: all 200ms ease-out; + cursor: pointer; + + .play { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAERklEQVR4nOWbTWhcVRTHb1IJVoxGtNCNdal2JYJReC6GWuO83PM/59yUS3FRFARdFlwYP1CfiojQWt36sRCUurRIdVFXIn41lAoVdRGrG1M01YpKrWjiYmaSl8ybZJL3cd+YA//NLObd3++eO8x79z5jSq5Gw+8kov0AP8vMR5l1BtBZQM4B8ks75wCdZdYZZj5qLZ4hov2Nht9Z9vhKKSIaB/gI4M4w62KeAO6Mte4lYOq20FxrlqqOibhHmeWbvNC9ZfDX1mLae391aN6limO/gwgvAPJbWeAZuSDingdwXTBw7/0IsyaA/Fkh+KqOkD+YNfHej1QKD+y7iVlOhgLvFqFfNJvNGyuBJ+KDAF8MDd0tgS8y64OlgSdJMsysL4cG7SOHkyQZLhTee7+d2R2rAVy/S+Jd7/32ouBHAP4gNNRGQyTHc/84NhqNywZp5rvjjnnvt21aABFeCQ+RLwAf2hQ8s7sv9OCLk6AHNgQvIrvbfzKCD76g/O6cu7lf/iER/aQGgy448pExZmhdegAPhR9sObFWH1gT3lp7DaA/5bkIgJhZPgsNmz02novj+KqeApj1ubwXWe4kdyeznAgNvTpE/HQmvKqOMeuFogTUVQSRno+iaLRLAJF7uIgL9O4ubgL8aWgB7S44mNX+35YpICUiAvS9sBLkq1WzT+NFffl6AuoiApi6NT37h6sWkBIRZGkQ8YtLgyji6e1mBYTqCEBPG2Naz+0BWQgtoGoRgCzEsd9hAN1X5BfnFZASUfrSAFQNsyZ1FJASUVpHiLinDJG8U2cBZYogkrcNs5waBAGdstbeU9zdqpw0gPwwSAI6VUxHyFlDpOcHUUBBIuYNs14aZAE5RVwyzPr3/0EAEY0TyfGNjBWQvwZ +CTSbehfAH29mrID8bET0+0EUkAd8WYDOmqJ3ecsG30yr9wqRfm6Y+a1BEFDEjHfHvWmY9ck6CygHvBVr8Xhtb4ZE5HZA3y8DvBNA1TjnrmXWf+sioMwZX5V/VHXMGGMMoKdDCxCRvRWBdzKzdHEO+EisilbPyopHYqp6S9UCAsz4iojI7hUDAtyXVQgIDd6KnOoaWNkbI6FaPSuZGyMArsi7MZoloB4zviI/Nhr3X95jltwTRQmoIfgisy5ai+me67OI7fE4nrqjrqfK1t0eby0FPRB6oGVlchL3rgnfrq19RKbVBdhV9IOSwJmfmJi4vi/4ThERitwyCxVAFqydshuCX5awhQ9KtmuIWd8IDZED/nXT77rvVVv6sHRKwjYi91poqP7Dr+Y6JJ1VSZIMA3wkPNy6bX+o8Bcm0sXMdwM8Fxo0A3xORPaWBp6uPXsmbxCRD0NDL0dOANhVCXy6iAjMcjbcrMt3RITKwdMVRdFo+y5yvkL4eWZ+zHt/ZVD4dEVRNGotpst+dZZZH8k86lqn2pIvT/eqrNfn2xuyqYPZ8mv7s8pfn/8Pybm4TIjanscAAAAASUVORK5CYII=") no-repeat center center; + background-size: 64px 64px; + position: absolute; + height: 100%; + width: 100%; + opacity: .8; + filter: alpha(opacity=80); + transition: all 0.2s ease-out; + &:hover { + opacity: 1; + filter: alpha(opacity=100); + } + } +} diff --git a/src/app/components/modules/AddToWaitingList.jsx b/src/app/components/modules/AddToWaitingList.jsx new file mode 100644 index 0000000..b86f94b --- /dev/null +++ b/src/app/components/modules/AddToWaitingList.jsx @@ -0,0 +1,65 @@ +import React from 'react'; +import tt from 'counterpart'; +import { APP_NAME } from 'app/client_config'; + +const email_regex = /^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/; + +export default class AddToWaitingList extends React.Component { + constructor() { + super(); + this.state = {email: '', submitted: false, email_error: ''}; + this.onEmailChange = this.onEmailChange.bind(this); + } + + onSubmit = (e) => { + e.preventDefault(); + const email = this.state.email; + if (!email) return; + fetch('/api/v1/update_email', { + method: 'post', + mode: 'no-cors', + credentials: 'same-origin', + headers: { + Accept: 'application/json', + 'Content-type': 'application/json' + }, + body: JSON.stringify({csrf: $STM_csrf, email}) + }).then(r => r.json()).then(res => { + if (res.error || res.status !== 'ok') { + console.error('CreateAccount server error', res.error); + } else { + // TODO: process errors + } + this.setState({submitted: true}); + }).catch(error => { + console.error('Caught CreateAccount server error', error); + this.setState({submitted: true}); + }); + }; + + onEmailChange(e) { + const email = e.target.value.trim().toLowerCase(); + let email_error = ''; + if (!email_regex.test(email.toLowerCase())) email_error = tt('g.not_valid_email'); + this.setState({email, email_error}); + } + + render() { + const {email, email_error, submitted} = this.state; + if (submitted) { + return
+ {tt('g.thank_you_for_being_an_early_visitor_to_APP_NAME', {APP_NAME})} +
+ } + return
+
+ +

{email_error}

+
+
+ +
; + } +} diff --git a/src/app/components/modules/ArticleLayoutSelector.jsx b/src/app/components/modules/ArticleLayoutSelector.jsx new file mode 100644 index 0000000..952a5f8 --- /dev/null +++ b/src/app/components/modules/ArticleLayoutSelector.jsx @@ -0,0 +1,32 @@ +/* eslint react/prop-types: 0 */ +import React, { PropTypes } from 'react'; +import { connect } from 'react-redux'; +import user from 'app/redux/User'; + +class ArticleLayoutSelector extends React.Component { + render() { + return ( +
+ + + + + + + + +
+ ); + } +} + +export default connect( + state => ({ + blogmode: state.app.getIn(['user_preferences', 'blogmode']), + }), + dispatch => ({ + toggleBlogmode: () => { + dispatch({ type: 'TOGGLE_BLOGMODE' }); + }, + }), +)(ArticleLayoutSelector); \ No newline at end of file diff --git a/src/app/components/modules/AuthorRewards.jsx b/src/app/components/modules/AuthorRewards.jsx new file mode 100644 index 0000000..0c44863 --- /dev/null +++ b/src/app/components/modules/AuthorRewards.jsx @@ -0,0 +1,158 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; +import {connect} from 'react-redux' +import TransferHistoryRow from 'app/components/cards/TransferHistoryRow'; +import {numberWithCommas, vestsToSp, assetFloat} from 'app/utils/StateFunctions' +import tt from 'counterpart'; +import { VESTING_TOKEN, LIQUID_TICKER, VEST_TICKER, DEBT_TICKER, DEBT_TOKEN_SHORT } from 'app/client_config'; + +class AuthorRewards extends React.Component { + constructor() { + super() + this.state = {historyIndex: 0} + this.onShowDeposit = () => {this.setState({showDeposit: !this.state.showDeposit})} + this.onShowDepositSteem = () => { + this.setState({showDeposit: !this.state.showDeposit, depositType: LIQUID_TICKER}) + } + this.onShowDepositPower = () => { + this.setState({showDeposit: !this.state.showDeposit, depositType: VEST_TICKER}) + } + // this.onShowDeposit = this.onShowDeposit.bind(this) + } + + shouldComponentUpdate(nextProps, nextState) { + return ( + nextProps.account.transfer_history.length !== this.props.account.transfer_history.length || + nextState.historyIndex !== this.state.historyIndex); + } + + _setHistoryPage(back) { + const newIndex = this.state.historyIndex + (back ? 10 : -10); + this.setState({historyIndex: Math.max(0, newIndex)}); + } + + render() { + const {state: {historyIndex}} = this + const account = this.props.account; + + /// transfer log + let rewards24Vests = 0, rewardsWeekVests = 0, totalRewardsVests = 0; + let rewards24Steem = 0, rewardsWeekSteem = 0, totalRewardsSteem = 0; + let rewards24SBD = 0, rewardsWeekSBD = 0, totalRewardsSBD = 0; + const today = new Date(); + const oneDay = 86400 * 1000; + const yesterday = new Date(today.getTime() - oneDay ).getTime(); + const lastWeek = new Date(today.getTime() - 7 * oneDay ).getTime(); + + let firstDate, finalDate; + let author_log = account.transfer_history.map((item, index) => { + // Filter out rewards + if (item[1].op[0] === "author_reward") { + if (!finalDate) { + finalDate = new Date(item[1].timestamp).getTime(); + } + firstDate = new Date(item[1].timestamp).getTime(); + + const vest = assetFloat(item[1].op[1].vesting_payout, VEST_TICKER); + const steem = assetFloat(item[1].op[1].steem_payout, LIQUID_TICKER); + const sbd = assetFloat(item[1].op[1].sbd_payout, DEBT_TICKER); + + if (new Date(item[1].timestamp).getTime() > lastWeek) { + if (new Date(item[1].timestamp).getTime() > yesterday) { + rewards24Vests += vest; + rewards24Steem += steem; + rewards24SBD += sbd; + } + rewardsWeekVests += vest; + rewardsWeekSteem += steem; + rewardsWeekSBD += sbd; + } + totalRewardsVests += vest; + totalRewardsSteem += steem; + totalRewardsSBD += sbd; + + return + } + return null; + }).filter(el => !!el); + + let currentIndex = -1; + const curationLength = author_log.length; + const daysOfCuration = (firstDate - finalDate) / oneDay || 1; + const averageCurationVests = !daysOfCuration ? 0 : totalRewardsVests / daysOfCuration; + const averageCurationSteem = !daysOfCuration ? 0 : totalRewardsSteem / daysOfCuration; + const averageCurationSBD = !daysOfCuration ? 0 : totalRewardsSBD / daysOfCuration; + const hasFullWeek = daysOfCuration >= 7; + const limitedIndex = Math.min(historyIndex, curationLength - 10); + author_log = author_log.reverse().filter(() => { + currentIndex++; + return currentIndex >= limitedIndex && currentIndex < limitedIndex + 10; + }); + + const navButtons = ( + + ); + return (
+
+
+

{tt('g.author_rewards')}

+
+
+
+
+ {tt('authorrewards_jsx.estimated_author_rewards_last_week')}: +
+
+ {numberWithCommas(vestsToSp(this.props.state, rewardsWeekVests + " " + VEST_TICKER)) + " " + VESTING_TOKEN} +
+ {rewardsWeekSteem.toFixed(3) + " " + LIQUID_TICKER} +
+ {rewardsWeekSBD.toFixed(3) + " " + DEBT_TOKEN_SHORT} +
+
+ +
+
+
+
+
+ +
+
+ {/** history */} +

{tt('authorrewards_jsx.author_rewards_history')}

+ {navButtons} + + + {author_log} + +
+ {navButtons} +
+
+
); + } +} + +export default connect( + // mapStateToProps + (state, ownProps) => { + return { + state, + ...ownProps + } + } +)(AuthorRewards) diff --git a/src/app/components/modules/BottomPanel.jsx b/src/app/components/modules/BottomPanel.jsx new file mode 100644 index 0000000..635dfe2 --- /dev/null +++ b/src/app/components/modules/BottomPanel.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import CloseButton from 'react-foundation-components/lib/global/close-button'; + +export default class BottomPanel extends React.Component { + static propTypes = { + children: React.PropTypes.object, + visible: React.PropTypes.bool, + hide: React.PropTypes.func.isRequired + }; + + componentWillReceiveProps(nextProps) { + if (nextProps.visible) { + document.addEventListener('click', this.props.hide); + } else { + document.removeEventListener('click', this.props.hide); + } + } + + componentWillUnmount() { + document.removeEventListener('click', this.props.hide); + } + + render() { + const {children, visible, hide} = this.props; + return
+
+ + {children} +
+
; + } +} diff --git a/src/app/components/modules/BottomPanel.scss b/src/app/components/modules/BottomPanel.scss new file mode 100644 index 0000000..e68c958 --- /dev/null +++ b/src/app/components/modules/BottomPanel.scss @@ -0,0 +1,41 @@ +$btm-panel-width: 400px; +$btm-panel-height: 90px; + +.BottomPanel { + > div { + background-color: rgba(220, 220, 220, .9); + color: #fff; + padding: 2rem 1rem 0.5rem 1rem; + border-top-left-radius: $global-radius; + border-top-right-radius: $global-radius; + .close-button { + color: #000; + } + + position: fixed; + z-index: 1000; + width: $btm-panel-width; + height: $btm-panel-height; + box-sizing: border-box; + transition: visibility 250ms, transform ease 250ms; + left: 50%; + margin-left: -$btm-panel-width/2; + bottom: -$btm-panel-height; + visibility: hidden; + + &.visible { + transform: translate3d(0, -$btm-panel-height, 0); + visibility: visible; + } + } +} + +/* Small only */ +@media screen and (max-width: 39.9375em) { + .BottomPanel > div { + width: 100%; + left: 0; + margin-left: 0; + border-radius: 0; + } +} diff --git a/src/app/components/modules/ConfirmTransactionForm.jsx b/src/app/components/modules/ConfirmTransactionForm.jsx new file mode 100644 index 0000000..c1ce794 --- /dev/null +++ b/src/app/components/modules/ConfirmTransactionForm.jsx @@ -0,0 +1,102 @@ +import React, { PropTypes, Component } from 'react'; +import {connect} from 'react-redux' +import transaction from 'app/redux/Transaction' +import {findParent} from 'app/utils/DomUtils'; +import tt from 'counterpart'; + +class ConfirmTransactionForm extends Component { + + static propTypes = { + //Steemit + onCancel: PropTypes.func, + warning: PropTypes.string, + checkbox: PropTypes.string, + // redux-form + confirm: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + confirmBroadcastOperation: PropTypes.object, + confirmErrorCallback: PropTypes.func, + okClick: PropTypes.func, + }; + constructor() { + super() + this.state = {checkboxChecked: false} + } + componentDidMount() { + document.body.addEventListener('click', this.closeOnOutsideClick); + } + componentWillUnmount() { + document.body.removeEventListener('click', this.closeOnOutsideClick); + } + closeOnOutsideClick = (e) => { + const inside_dialog = findParent(e.target, 'ConfirmTransactionForm'); + if (!inside_dialog) this.onCancel(); + } + onCancel = () => { + const {confirmErrorCallback, onCancel} = this.props; + if(confirmErrorCallback) confirmErrorCallback(); + if(onCancel) onCancel() + } + okClick = () => { + const {okClick, confirmBroadcastOperation} = this.props + okClick(confirmBroadcastOperation) + } + onCheckbox = (e) => { + const checkboxChecked = e.target.checked + this.setState({checkboxChecked}) + } + render() { + const {onCancel, okClick, onCheckbox} = this + const {confirm, confirmBroadcastOperation, warning, checkbox} = this.props + const {checkboxChecked} = this.state + const conf = typeof confirm === 'function' ? confirm() : confirm + return ( +
+

{typeName(confirmBroadcastOperation)}

+
+
{conf}
+ {warning ?
{warning}
: null} + {checkbox ? +
+ +
: null} +
+ + +
+ ) + } +} +const typeName = confirmBroadcastOperation => { + const title = confirmBroadcastOperation.getIn(['operation', '__config', 'title']) + if(title) return title + const type = confirmBroadcastOperation.get('type') + return tt('g.confirm') + ' ' + (type.split('_').map(n => n.charAt(0).toUpperCase() + n.substring(1))).join(' ') +} + +export default connect( + // mapStateToProps + (state) => { + const confirmBroadcastOperation = state.transaction.get('confirmBroadcastOperation') + const confirmErrorCallback = state.transaction.get('confirmErrorCallback') + const confirm = state.transaction.get('confirm') + const warning = state.transaction.get('warning') + const checkbox = state.transaction.get('checkbox') + return { + confirmBroadcastOperation, + confirmErrorCallback, + confirm, + warning, + checkbox + } + }, + // mapDispatchToProps + dispatch => ({ + okClick: (confirmBroadcastOperation) => { + dispatch(transaction.actions.hideConfirm()) + dispatch(transaction.actions.broadcastOperation({...(confirmBroadcastOperation.toJS())})) + } + }) +)(ConfirmTransactionForm) diff --git a/src/app/components/modules/ConfirmTransactionForm.scss b/src/app/components/modules/ConfirmTransactionForm.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/modules/CurationRewards.jsx b/src/app/components/modules/CurationRewards.jsx new file mode 100644 index 0000000..40a25e4 --- /dev/null +++ b/src/app/components/modules/CurationRewards.jsx @@ -0,0 +1,142 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; +import {connect} from 'react-redux' +import TransferHistoryRow from 'app/components/cards/TransferHistoryRow'; +import {numberWithCommas, vestsToSp, assetFloat} from 'app/utils/StateFunctions' +import tt from 'counterpart'; +import { APP_NAME, DEBT_TOKEN, DEBT_TOKEN_SHORT, LIQUID_TOKEN, CURRENCY_SIGN, +VESTING_TOKEN, LIQUID_TICKER, VEST_TICKER } from 'app/client_config'; + +class CurationRewards extends React.Component { + constructor() { + super() + this.state = {historyIndex: 0} + this.onShowDeposit = () => {this.setState({showDeposit: !this.state.showDeposit})} + this.onShowDepositSteem = () => { + this.setState({showDeposit: !this.state.showDeposit, depositType: LIQUID_TICKER}) + } + this.onShowDepositPower = () => { + this.setState({showDeposit: !this.state.showDeposit, depositType: VEST_TICKER}) + } + // this.onShowDeposit = this.onShowDeposit.bind(this) + } + + shouldComponentUpdate(nextProps, nextState) { + return ( + nextProps.account.transfer_history.length !== this.props.account.transfer_history.length || + nextState.historyIndex !== this.state.historyIndex); + } + + _setHistoryPage(back) { + const newIndex = this.state.historyIndex + (back ? 10 : -10); + this.setState({historyIndex: Math.max(0, newIndex)}); + } + + render() { + const {state: {historyIndex}} = this + const account = this.props.account; + + /// transfer log + let rewards24 = 0, rewardsWeek = 0, totalRewards = 0; + let today = new Date(); + let oneDay = 86400 * 1000; + let yesterday = new Date(today.getTime() - oneDay ).getTime(); + let lastWeek = new Date(today.getTime() - 7 * oneDay ).getTime(); + + let firstDate, finalDate; + let curation_log = account.transfer_history.map((item, index) => { + // Filter out rewards + if (item[1].op[0] === "curation_reward") { + if (!finalDate) { + finalDate = new Date(item[1].timestamp).getTime(); + } + firstDate = new Date(item[1].timestamp).getTime(); + const vest = assetFloat(item[1].op[1].reward, VEST_TICKER); + if (new Date(item[1].timestamp).getTime() > yesterday) { + rewards24 += vest; + rewardsWeek += vest; + } else if (new Date(item[1].timestamp).getTime() > lastWeek) { + rewardsWeek += vest; + } + totalRewards += vest; + return + } + return null; + }).filter(el => !!el); + let currentIndex = -1; + const curationLength = curation_log.length; + const daysOfCuration = (firstDate - finalDate) / oneDay || 1; + const averageCuration = !daysOfCuration ? 0 : totalRewards / daysOfCuration; + const hasFullWeek = daysOfCuration >= 7; + const limitedIndex = Math.min(historyIndex, curationLength - 10); + curation_log = curation_log.reverse().filter(() => { + currentIndex++; + return currentIndex >= limitedIndex && currentIndex < limitedIndex + 10; + }); + + const navButtons = ( + + ); + + + + + return (
+
+
+

{tt('g.curation_rewards')}

+
+
+
+
+ {tt('curationrewards_jsx.estimated_curation_rewards_last_week')}: +
+
+ {numberWithCommas(vestsToSp(this.props.state, rewardsWeek + " " + VEST_TICKER)) + " " + VESTING_TOKEN} +
+
+
+
+
+
+
+ +
+
+ {/** history */} +

{tt('curationrewards_jsx.curation_rewards_history')}

+ {navButtons} + + + {curation_log} + +
+ {navButtons} +
+
+
); + } +} + +export default connect( + // mapStateToProps + (state, ownProps) => { + return { + state, + ...ownProps + } + } +)(CurationRewards) diff --git a/src/app/components/modules/Dialogs.jsx b/src/app/components/modules/Dialogs.jsx new file mode 100644 index 0000000..ddfd440 --- /dev/null +++ b/src/app/components/modules/Dialogs.jsx @@ -0,0 +1,105 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import CloseButton from 'react-foundation-components/lib/global/close-button'; +import Reveal from 'react-foundation-components/lib/global/reveal'; +import g from 'app/redux/GlobalReducer' +import {Map, List} from 'immutable' +import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'; +import QrReader from 'app/components/elements/QrReader' +import ConvertToSteem from 'app/components/elements/ConvertToSteem' +import SuggestPassword from 'app/components/elements/SuggestPassword' +import ChangePassword from 'app/components/elements/ChangePassword' +import CheckLoginOwner from 'app/components/elements/CheckLoginOwner' +import QrKeyView from 'app/components/elements/QrKeyView' +import PromotePost from 'app/components/modules/PromotePost'; +import ExplorePost from 'app/components/modules/ExplorePost'; + +class Dialogs extends React.Component { + static propTypes = { + active_dialogs: React.PropTypes.object, + hide: React.PropTypes.func.isRequired, + } + constructor() { + super() + this.shouldComponentUpdate = shouldComponentUpdate(this, 'Dialogs') + this.hide = (name) => { + this.props.hide(name) + } + } + componentWillReceiveProps(nextProps) { + const {active_dialogs, hide} = nextProps + active_dialogs.forEach((v, k) => { + if(!this['hide_' + k]) + this['hide_' + k] = () => hide(k) + }) + } + render() { + const {active_dialogs} = this.props + let idx = 0 + const dialogs = active_dialogs.reduce((r, v, k) => { + const cmp = k === 'qr_reader' ? + + + + + : + k === 'convertToSteem' ? + + + + + : + k === 'suggestPassword' ? + + + + + : + k === 'changePassword' ? + + + + + : + k === 'promotePost' ? + + + + + : + k === 'explorePost' ? + + + + + : + k === 'qr_key' ? + + + + + : + null + return cmp ? r.push(cmp) : r + }, List()) + return
+ {dialogs.toJS()} + +
+ } +} + +const emptyMap = Map() + +export default connect( + state => { + return { + active_dialogs: state.global.get('active_dialogs') || emptyMap, + } + }, + dispatch => ({ + hide: name => { + dispatch(g.actions.hideDialog({name})) + }, + }) +)(Dialogs) diff --git a/src/app/components/modules/ExplorePost.jsx b/src/app/components/modules/ExplorePost.jsx new file mode 100644 index 0000000..5e9a556 --- /dev/null +++ b/src/app/components/modules/ExplorePost.jsx @@ -0,0 +1,79 @@ +import React, {PropTypes, Component} from 'react'; +import {connect} from 'react-redux'; +import {serverApiRecordEvent} from 'app/utils/ServerApiClient'; +import Icon from 'app/components/elements/Icon'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import tt from 'counterpart'; + +class ExplorePost extends Component { + + static propTypes = { + permlink: PropTypes.string.isRequired + }; + + constructor(props) { + super(props); + this.state = { + copied: false + }; + this.onCopy = this.onCopy.bind(this); + this.Steemd = this.Steemd.bind(this); + this.Steemdb = this.Steemdb.bind(this); + this.Busy = this.Busy.bind(this); + this.Phist = this.Phist.bind(this); + } + + Steemd() { + serverApiRecordEvent('SteemdView', this.props.permlink); + } + + Steemdb() { + serverApiRecordEvent('SteemdbView', this.props.permlink); + } + + Busy() { + serverApiRecordEvent('Busy view', this.props.permlink); + } + + Phist() { + serverApiRecordEvent('PhistView', this.props.permlink); + } + + onCopy() { + this.setState({ + copied: true + }); + } + + render() { + const link = this.props.permlink; + const steemd = 'https://steemd.com' + link; + const steemdb = 'https://steemdb.com' + link; + const busy = 'https://busy.org' + link; + const steemit = 'https://steemit.com' + link; + const phist = 'https://phist.steemdata.com/history?identifier=steemit.com' + link; + let text = this.state.copied == true ? tt('explorepost_jsx.copied') : tt('explorepost_jsx.copy'); + return ( + +

{tt('g.share_this_post')}

+
+
+ e.preventDefault()} /> + + {text} + +
+
{tt('explorepost_jsx.alternative_sources')}
+ +
+ ) + } +} + +export default connect( +)(ExplorePost) diff --git a/src/app/components/modules/Footer.jsx b/src/app/components/modules/Footer.jsx new file mode 100644 index 0000000..b3ee12b --- /dev/null +++ b/src/app/components/modules/Footer.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Link } from 'react-router'; +import {connect} from 'react-redux'; +import tt from 'counterpart'; + +const Footer = props => ( +
+
+
    +
  • {tt('navigation.about')}
  • +
  • {tt('navigation.privacy_policy')}
  • +
  • {tt('navigation.terms_of_service')}
  • +
  • {tt('navigation.witnesses')}
  • +
+
+
+
+
+
+
+) + +Footer.propTypes = { +} + +export default connect(state => { + return { + }; +})(Footer); diff --git a/src/app/components/modules/Footer.scss b/src/app/components/modules/Footer.scss new file mode 100644 index 0000000..5d2155d --- /dev/null +++ b/src/app/components/modules/Footer.scss @@ -0,0 +1,13 @@ +.Footer { + border: none; + margin-top: 2rem; + margin-bottom: 0; + border-right: none; + border-radius: 0; + background-color: $medium-gray; +} + +.Footer__section { + line-height: 1.5rem; + padding: 0.7rem 1rem; +} diff --git a/src/app/components/modules/Header.jsx b/src/app/components/modules/Header.jsx new file mode 100644 index 0000000..8dce6fc --- /dev/null +++ b/src/app/components/modules/Header.jsx @@ -0,0 +1,243 @@ +import React from 'react'; +import { Link } from 'react-router'; +import {connect} from 'react-redux'; +import TopRightMenu from 'app/components/modules/TopRightMenu'; +import Icon from 'app/components/elements/Icon'; +import resolveRoute from 'app/ResolveRoute'; +import DropdownMenu from 'app/components/elements/DropdownMenu'; +import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'; +import HorizontalMenu from 'app/components/elements/HorizontalMenu'; +import normalizeProfile from 'app/utils/NormalizeProfile'; +import tt from 'counterpart'; +import { APP_NAME } from 'app/client_config'; + +function sortOrderToLink(so, topic, account) { + if (so === 'home') return '/@' + account + '/feed'; + if (topic) return `/${so}/${topic}`; + return `/${so}`; +} + +class Header extends React.Component { + static propTypes = { + location: React.PropTypes.object.isRequired, + current_account_name: React.PropTypes.string, + account_meta: React.PropTypes.object + }; + + constructor() { + super(); + this.state = {subheader_hidden: false} + this.shouldComponentUpdate = shouldComponentUpdate(this, 'Header'); + this.hideSubheader = this.hideSubheader.bind(this); + } + + componentDidMount() { + window.addEventListener('scroll', this.hideSubheader, {capture: false, passive: true}); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.location.pathname !== this.props.location.pathname) { + const route = resolveRoute(nextProps.location.pathname); + if (route && route.page === 'PostsIndex' && route.params && route.params.length > 0) { + const sort_order = route.params[0] !== 'home' ? route.params[0] : null; + if (sort_order) window.last_sort_order = this.last_sort_order = sort_order; + } + } + } + + componentWillUnmount() { + window.removeEventListener('scroll', this.hideSubheader); + } + + hideSubheader() { + const subheader_hidden = this.state.subheader_hidden; + const y = window.scrollY >= 0 ? window.scrollY : document.documentElement.scrollTop; + if (y === this.prevScrollY) return; + + if (y < 5) { + this.setState({subheader_hidden: false}); + } else if (y > this.prevScrollY) { + if (!subheader_hidden) this.setState({subheader_hidden: true}) + } else { + if (subheader_hidden) this.setState({subheader_hidden: false}) + } + this.prevScrollY = y; + } + + render() { + const route = resolveRoute(this.props.location.pathname); + const current_account_name = this.props.current_account_name; + let home_account = false; + let page_title = route.page; + + let sort_order = ''; + let topic = ''; + let user_name = null; + let page_name = null; + this.state.subheader_hidden = false; + if (route.page === 'PostsIndex') { + sort_order = route.params[0]; + if (sort_order === 'home') { + page_title = tt('header_jsx.home') + const account_name = route.params[1]; + if (current_account_name && account_name.indexOf(current_account_name) === 1) + home_account = true; + } else { + topic = (route.params.length > 1 ? route.params[1] : '') + const type = (route.params[0] == 'payout_comments' ? 'comments' : 'posts'); + let prefix = route.params[0]; + if(prefix == 'created') prefix = 'New' + if(prefix == 'payout') prefix = 'Pending payout' + if(prefix == 'payout_comments') prefix = 'Pending payout' + if(topic !== '') prefix += ` ${topic}`; + page_title = `${prefix} ${type}` + } + } else if (route.page === 'Post') { + sort_order = ''; + topic = route.params[0]; + } else if (route.page == 'SubmitPost') { + page_title = tt('header_jsx.create_a_post'); + } else if (route.page == 'Privacy') { + page_title = tt('navigation.privacy_policy'); + } else if (route.page == 'Tos') { + page_title = tt('navigation.terms_of_service'); + } else if (route.page == 'ChangePassword') { + page_title = tt('header_jsx.change_account_password'); + } else if (route.page == 'CreateAccount') { + page_title = tt('header_jsx.create_account'); + } else if (route.page == 'PickAccount') { + page_title = `Pick Your New Steemit Account`; + this.state.subheader_hidden = true; + } else if (route.page == 'Approval') { + page_title = `Account Confirmation`; + this.state.subheader_hidden = true; + } else if (route.page == 'RecoverAccountStep1' || route.page == 'RecoverAccountStep2') { + page_title = tt('header_jsx.stolen_account_recovery'); + } else if (route.page === 'UserProfile') { + user_name = route.params[0].slice(1); + const acct_meta = this.props.account_meta.getIn([user_name]); + const name = acct_meta ? normalizeProfile(acct_meta.toJS()).name : null; + const user_title = name ? `${name} (@${user_name})` : user_name; + page_title = user_title; + if(route.params[1] === "followers"){ + page_title = tt('header_jsx.people_following') + " " + user_title; + } + if(route.params[1] === "followed"){ + page_title = tt('header_jsx.people_followed_by') + " " + user_title; + } + if(route.params[1] === "curation-rewards"){ + page_title = tt('header_jsx.curation_rewards_by') + " " + user_title; + } + if(route.params[1] === "author-rewards"){ + page_title = tt('header_jsx.author_rewards_by') + " " + user_title; + } + if(route.params[1] === "recent-replies"){ + page_title = tt('header_jsx.replies_to') + " " + user_title; + } + // @user/"posts" is deprecated in favor of "comments" as of oct-2016 (#443) + if(route.params[1] === "posts" || route.params[1] === "comments"){ + page_title = tt('header_jsx.comments_by') + " " + user_title; + } + } else { + page_name = ''; //page_title = route.page.replace( /([a-z])([A-Z])/g, '$1 $2' ).toLowerCase(); + } + + // Format first letter of all titles and lowercase user name + if (route.page !== 'UserProfile') { + page_title = page_title.charAt(0).toUpperCase() + page_title.slice(1); + } + + + if (process.env.BROWSER && (route.page !== 'Post' && route.page !== 'PostNoCategory')) document.title = page_title + ' — ' + APP_NAME; + + const logo_link = route.params && route.params.length > 1 && this.last_sort_order ? '/' + this.last_sort_order : (current_account_name ? `/@${current_account_name}/feed` : '/'); + const topic_link = topic ? {topic} : null; + + const sort_orders = [ + ['trending', tt('main_menu.trending')], + ['created', tt('g.new')], + ['hot', tt('main_menu.hot')], + ['promoted', tt('g.promoted')] + ]; + if (current_account_name) sort_orders.unshift(['home', tt('header_jsx.home')]); + const sort_order_menu = sort_orders.filter(so => so[0] !== sort_order).map(so => ({link: sortOrderToLink(so[0], topic, current_account_name), value: so[1]})); + const selected_sort_order = sort_orders.find(so => so[0] === sort_order); + + const sort_orders_horizontal = [ + ['trending', tt('main_menu.trending')], + ['created', tt('g.new')], + ['hot', tt('main_menu.hot')], + ['promoted', tt('g.promoted')] + ]; + // if (current_account_name) sort_orders_horizontal.unshift(['home', tt('header_jsx.home')]); + const sort_order_menu_horizontal = sort_orders_horizontal.map((so) => { + let active = (so[0] === sort_order); + if (so[0] === 'home' && sort_order === 'home' && !home_account) active = false; + return {link: sortOrderToLink(so[0], topic, current_account_name), value: so[1], active}; + }); + + return ( +
+
+
+
+
    +
  • + + {/* + + */} + + + Steemit Logo + + + + + + + + + Steemit Logo + + + + + + + + + +
  • +
  • beta
  • + + + {(topic_link || user_name || page_name) && sort_order } + {selected_sort_order && } + +
+
+
+ +
+
+
+
+ ); + } +} + +export {Header as _Header_}; + +export default connect( + (state) => { + const current_user = state.user.get('current'); + const account_user = state.global.get('accounts'); + const current_account_name = current_user ? current_user.get('username') : state.offchain.get('account'); + return { + location: state.app.get('location'), + current_account_name, + account_meta: account_user + } + } +)(Header); diff --git a/src/app/components/modules/Header.scss b/src/app/components/modules/Header.scss new file mode 100644 index 0000000..18f008d --- /dev/null +++ b/src/app/components/modules/Header.scss @@ -0,0 +1,243 @@ +.Header { + position: fixed; + backface-visibility: hidden; + top: 0; + left: 0; + width: 100%; + z-index: 100; + background-color: $white; + box-shadow: 0 2px 4px 0 rgba(0,0,0,0.05); + @include themify($themes) { + background-color: themed('navBackgroundColor'); + border-bottom: themed('border'); + } + h3 { + padding-left: 1rem; + margin-bottom: 0; + } + .menu { + // background-color: $white; + padding-left: 0; + .hamburger { + @include hamburger(); + position: relative; + top: 2px; + &::after { + transition: 0.2s all ease-in-out; + @include themify($themes) { + background: themed('textColorPrimary'); + box-shadow: 0 7px 0 themed('textColorPrimary'), 0 14px 0 themed('textColorPrimary'); + } + } + &:hover { + &::after { + @include themify($themes) { + background: themed('textColorAccent'); + box-shadow: 0 7px 0 themed('textColorAccent'), 0 14px 0 themed('textColorAccent'); + } + } + } + } + } + ul.menu > li.toggle-menu > a { + padding-right: 0; + } + .menu-icon { + margin: 0.2rem 0; + } + svg > path { + @include themify($themes) { + fill: themed('textColorPrimary'); + } + } + li.submit-story > a { + @extend .e-btn; + margin-right: 22px; + margin-left: 16px; + } + a { + @extend .link; + @extend .link--primary; + font-family: helvetica, sans-serif; + } +} + +.Header__top { + // background-color: $white; + + padding: 0.25rem 0; + transition: all 0.3s ease-out; + @media print, screen and (min-width: 52.5em) { + padding: 0.75rem 0; + } + ul > li.Header__top-logo { + padding: .35rem 0 .35rem 0; + } + ul > li.delim { + color: $medium-gray; + } + ul > li > a { + font-size: 1.1rem; + } + ul > li > span { + font-size: 1.1rem; + line-height: 1.5rem; + padding: 0.7rem 1rem; + display: flex; + text-transform: lowercase; + } + @media screen and (max-width: 39.9375em) { + .shrink { + padding: 0 1rem; + } + } +} + +ul > li.Header__top-logo > a { + padding: 0; + transition: none; +} + +.Header { + ul > li > .button { + padding-top: 0.2rem; + padding-bottom: 0.2rem; + } +} + + +.Header__sub-nav { + position: relative; + padding: 0; + background-color: $white; + border-bottom: 1px solid $light-gray; + transition: margin .3s; + z-index: -1; + will-change: margin; + li { + line-height: 1rem; + padding: 1.2rem 0 1.2rem 0; + margin: 0 0 0 2rem; + } + // No margin adjustments for first child element in menu items + li:first-child { + margin: 0 0 0 0; + } + li > a { + color: $dark-gray; + background-color: $white; + border-bottom: 1px solid transparent; + font-size: 1rem; + line-height: 0; + padding: 0; + } + a.active { + color: $black; + //border-bottom: 1px solid $dark-gray; + } +} + + +.Header__sub-nav.hidden { + // display: none; + margin-top: -42px; +} + + +.Header__topic { + background-color: $white; + line-height: 1.5rem; + padding: 0.7rem 1rem; + text-transform: uppercase; +} + +.Header__top-steemit { + margin-right: -22px; + .beta { + position: absolute; + top: 43px; + left: 140px; + font-size: 0.7rem; + @include opacity(0); + display: none; + @include themify($themes) { + color: themed('textColorSecondary'); + } + @include MQ(L) { + display: block; + } + } +} + +.Header__sort-order-menu { + .VerticalMenu { + left: 0; + } +} + +.logo { + padding-top: 10px!important; + padding-bottom: 1px!important; + padding-right: 5px!important; +} + +.ConnectionError { + margin-right: 4rem; + color: #ec5840; +} + +.logo-new { + transition: 0.2s ease-in-out; + display: none; + transition: 0.2s all ease-in-out; + &--mobile { + display: block; + width: 30px; + height: 30px; + @media only screen and (min-width: 400px) { + display: none; + } + } + &--desktop { + display: none; + width: 120px; + height: auto; + @media only screen and (min-width: 400px) { + display: block; + } + @include MQ(M) { + width: 144px; + height: auto; + } + } + + .icon-svg { + fill: $color-teal; + @include themify($themes) { + fill: themed('colorAccent'); + } + } +} + +.Header__top-logo:hover { + .logo-new { + .icon-svg { + fill: $color-blue-black; + @include themify($themes) { + fill: themed('colorAccentReverse'); + } + } + } +} + +.Header__top-logo a { + &:visited, &:active { + .logo-new { + .icon-svg { + @include themify($themes) { + fill: themed('ColorAccent'); + } + } + } + } +} diff --git a/src/app/components/modules/Header.test.js b/src/app/components/modules/Header.test.js new file mode 100644 index 0000000..c8ff7f5 --- /dev/null +++ b/src/app/components/modules/Header.test.js @@ -0,0 +1,11 @@ +/*global describe, it, before, beforeEach, after, afterEach */ +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import {_Header_} from './Header'; + +describe('Header', () => { + it('contains class .header', () => { + expect(shallow(<_Header_ />).is('.Header')).to.equal(true); + }); +}); diff --git a/src/app/components/modules/LoginForm.jsx b/src/app/components/modules/LoginForm.jsx new file mode 100644 index 0000000..2a7d499 --- /dev/null +++ b/src/app/components/modules/LoginForm.jsx @@ -0,0 +1,325 @@ +/* eslint react/prop-types: 0 */ +import React, { PropTypes, Component } from 'react'; +import transaction from 'app/redux/Transaction' +import g from 'app/redux/GlobalReducer' +import user from 'app/redux/User' +import {validate_account_name} from 'app/utils/ChainValidation'; +import runTests from 'app/utils/BrowserTests'; +import shouldComponentUpdate from 'app/utils/shouldComponentUpdate' +import reactForm from 'app/utils/ReactForm' +import {serverApiRecordEvent} from 'app/utils/ServerApiClient'; +import tt from 'counterpart'; +import { APP_URL } from 'app/client_config'; +import {PrivateKey, PublicKey} from 'steem/lib/auth/ecc'; + +class LoginForm extends Component { + + static propTypes = { + //Steemit + login_error: PropTypes.string, + onCancel: PropTypes.func, + }; + + static defaultProps = { + afterLoginRedirectToWelcome: false + } + + constructor(props) { + super() + const cryptoTestResult = runTests(); + let cryptographyFailure = false; + this.SignUp = this.SignUp.bind(this); + if (cryptoTestResult !== undefined) { + console.error('CreateAccount - cryptoTestResult: ', cryptoTestResult); + cryptographyFailure = true + } + this.state = {cryptographyFailure}; + this.usernameOnChange = e => { + const value = e.target.value.toLowerCase(); + this.state.username.props.onChange(value) + }; + this.onCancel = (e) => { + if(e.preventDefault) e.preventDefault() + const {onCancel, loginBroadcastOperation} = this.props; + const errorCallback = loginBroadcastOperation && loginBroadcastOperation.get('errorCallback'); + if (errorCallback) errorCallback('Canceled'); + if (onCancel) onCancel() + }; + this.qrReader = () => { + const {qrReader} = props + const {password} = this.state + qrReader(data => {password.props.onChange(data)}) + }; + this.initForm(props) + } + + componentDidMount() { + if (this.refs.username && !this.refs.username.value) this.refs.username.focus(); + if (this.refs.username && this.refs.username.value) this.refs.pw.focus(); + } + + shouldComponentUpdate = shouldComponentUpdate(this, 'LoginForm'); + + initForm(props) { + reactForm({ + name: 'login', + instance: this, + fields: ['username', 'password', 'saveLogin:checked'], + initialValues: props.initialValues, + validation: values => ({ + username: ! values.username ? tt('g.required') : validate_account_name(values.username.split('/')[0]), + password: ! values.password ? tt('g.required') : + PublicKey.fromString(values.password) ? tt('loginform_jsx.you_need_a_private_password_or_key') : + null, + }) + }) + } + + SignUp() { + const onType = document.getElementsByClassName("OpAction")[0].textContent; + serverApiRecordEvent('FreeMoneySignUp', onType); + window.location.href = "/pick_account"; + } + + SignIn() { + const onType = document.getElementsByClassName("OpAction")[0].textContent; + serverApiRecordEvent('SignIn', onType); + } + + saveLoginToggle = () => { + const {saveLogin} = this.state; + saveLoginDefault = !saveLoginDefault; + localStorage.setItem('saveLogin', saveLoginDefault ? 'yes' : 'no'); + saveLogin.props.onChange(saveLoginDefault); // change UI + }; + + showChangePassword = () => { + const {username, password} = this.state; + this.props.showChangePassword(username.value, password.value) + }; + + render() { + if (!process.env.BROWSER) { + return
+
+

{('loading')}...

+
+
; + } + if (this.state.cryptographyFailure) { + return
+
+
+

{tt('loginform_jsx.cryptography_test_failed')}

+

{tt('loginform_jsx.unable_to_log_you_in')}

+

{tt('loginform_jsx.the_latest_versions_of')} Chrome {tt('g.and')} Firefox {tt('loginform_jsx.are_well_tested_and_known_to_work_with', {APP_URL})}

+
+
+
; + } + + if ($STM_Config.read_only_mode) { + return
+
+
+

{tt('loginform_jsx.due_to_server_maintenance')}

+
+
; + } + + const {loginBroadcastOperation, dispatchSubmit, afterLoginRedirectToWelcome, msg} = this.props; + const {username, password, saveLogin} = this.state; + const {submitting, valid, handleSubmit} = this.state.login; + const {usernameOnChange, onCancel, /*qrReader*/} = this; + const disabled = submitting || !valid; + const opType = loginBroadcastOperation ? loginBroadcastOperation.get('type') : null; + let postType = ""; + if (opType === "vote") { + postType = tt('loginform_jsx.login_to_vote') + } else if (opType === "custom_json" && loginBroadcastOperation.getIn(['operation', 'id']) === "follow") { + postType = 'Login to Follow Users' + } else if (loginBroadcastOperation) { + // check for post or comment in operation + postType = loginBroadcastOperation.getIn(['operation', 'title']) ? tt('loginform_jsx.login_to_post') : tt('loginform_jsx.login_to_comment'); + } + const title = postType ? postType : tt('g.login'); + const authType = /^vote|comment/.test(opType) ? tt('loginform_jsx.posting') : tt('loginform_jsx.active_or_owner'); + const submitLabel = loginBroadcastOperation ? tt('g.sign_in') : tt('g.login'); + let error = password.touched && password.error ? password.error : this.props.login_error; + if (error === 'owner_login_blocked') { + error = {tt('loginform_jsx.this_password_is_bound_to_your_account_owner_key')} + {tt('loginform_jsx.however_you_can_use_it_to')}{tt('loginform_jsx.update_your_password')} {tt('loginform_jsx.to_obtain_a_more_secure_set_of_keys')} + } else if (error === 'active_login_blocked') { + error = {tt('loginform_jsx.this_password_is_bound_to_your_account_active_key')} {tt('loginform_jsx.you_may_use_this_active_key_on_other_more')} + } + let message = null; + if (msg) { + if (msg === 'accountcreated') { + message =
+

{tt('loginform_jsx.you_account_has_been_successfully_created')}

+
; + } + else if (msg === 'accountrecovered') { + message =
+

{tt('loginform_jsx.you_account_has_been_successfully_recovered')}

+
; + } + else if (msg === 'passwordupdated') { + message =
+

{tt('loginform_jsx.password_update_succes', {accountName: username.value})}

+
; + } + } + const password_info = checkPasswordChecksum(password.value) === false ? tt('loginform_jsx.password_info') : null + + const form = ( +
{ + // bind redux-form to react-redux + console.log('Login\tdispatchSubmit'); + return dispatchSubmit(data, loginBroadcastOperation, afterLoginRedirectToWelcome) + })} + onChange={this.props.clearError} + method="post" + > +
+ @ + +
+ {username.touched && username.blur && username.error ?
{username.error} 
: null} + +
+ + {error &&
{error} 
} + {error && password_info &&
{password_info} 
} +
+ {loginBroadcastOperation &&
+
{tt('loginform_jsx.this_operation_requires_your_key_or_master_password', {authType})}
+
} +
+ +
+
+
+ + {this.props.onCancel && } +
+
+
+

{tt('loginform_jsx.join_our')} {tt('loginform_jsx.amazing_community')}{tt('loginform_jsx.to_comment_and_reward_others')}

+ +
+
+ ); + + return ( +
+
+ {message} +

{tt('loginform_jsx.returning_users')}{title}

+ {form} +
+
+ ) + } +} + +let hasError +let saveLoginDefault = true +if (process.env.BROWSER) { + const s = localStorage.getItem('saveLogin') + if (s === 'no') saveLoginDefault = false +} + +function urlAccountName() { + let suggestedAccountName = ''; + const account_match = window.location.hash.match(/account\=([\w\d\-\.]+)/); + if (account_match && account_match.length > 1) suggestedAccountName = account_match[1]; + return suggestedAccountName +} + +function checkPasswordChecksum(password) { + // A Steemit generated password is a WIF prefixed with a P .. + // It is possible to login directly with a WIF + const wif = /^P/.test(password) ? password.substring(1) : password + + if(!/^5[HJK].{45,}/i.test(wif)) {// 51 is the wif length + // not even close + return undefined + } + + return PrivateKey.isWif(wif) +} + +import {connect} from 'react-redux' +export default connect( + + // mapStateToProps + (state) => { + const login_error = state.user.get('login_error') + const currentUser = state.user.get('current') + const loginBroadcastOperation = state.user.get('loginBroadcastOperation') + + const initialValues = { + saveLogin: saveLoginDefault, + } + + // The username input has a value prop, so it should not use initialValues + const initialUsername = currentUser && currentUser.has('username') ? currentUser.get('username') : urlAccountName() + const loginDefault = state.user.get('loginDefault') + if(loginDefault) { + const {username, authType} = loginDefault.toJS() + if(username && authType) initialValues.username = username + '/' + authType + } else if (initialUsername) { + initialValues.username = initialUsername; + } + const offchainUser = state.offchain.get('user'); + if (!initialUsername && offchainUser && offchainUser.get('account')) { + initialValues.username = offchainUser.get('account'); + } + let msg = ''; + const msg_match = window.location.hash.match(/msg\=([\w]+)/); + if (msg_match && msg_match.length > 1) msg = msg_match[1]; + hasError = !!login_error + return { + login_error, + loginBroadcastOperation, + initialValues, + initialUsername, + msg, + offchain_user: state.offchain.get('user') + } + }, + + // mapDispatchToProps + dispatch => ({ + dispatchSubmit: (data, loginBroadcastOperation, afterLoginRedirectToWelcome) => { + const {password, saveLogin} = data + const username = data.username.trim().toLowerCase() + if (loginBroadcastOperation) { + const {type, operation, successCallback, errorCallback} = loginBroadcastOperation.toJS() + dispatch(transaction.actions.broadcastOperation({type, operation, username, password, successCallback, errorCallback})) + dispatch(user.actions.usernamePasswordLogin({username, password, saveLogin, afterLoginRedirectToWelcome, operationType: type})) + dispatch(user.actions.closeLogin()) + } else { + dispatch(user.actions.usernamePasswordLogin({username, password, saveLogin, afterLoginRedirectToWelcome})) + } + }, + clearError: () => { if (hasError) dispatch(user.actions.loginError({error: null})) }, + qrReader: (dataCallback) => { + dispatch(g.actions.showDialog({name: 'qr_reader', params: {handleScan: dataCallback}})); + }, + showChangePassword: (username, defaultPassword) => { + dispatch(user.actions.closeLogin()) + dispatch(g.actions.remove({key: 'changePassword'})) + dispatch(g.actions.showDialog({name: 'changePassword', params: {username, defaultPassword}})) + }, + }) +)(LoginForm) diff --git a/src/app/components/modules/LoginForm.scss b/src/app/components/modules/LoginForm.scss new file mode 100644 index 0000000..d93f375 --- /dev/null +++ b/src/app/components/modules/LoginForm.scss @@ -0,0 +1,45 @@ +.LoginForm { + max-width: 28rem; + margin: 1rem auto 0.5rem auto; + label { + text-transform: none; + } + form { + margin-top: 1.5rem; + } +} +.sign-up { + .button { + background-color: transparent; + padding-top: 1rem; + padding-bottom: 1rem; + font-size: 1.2rem; + text-transform: none; + } + .button.hollow { + border: 1px solid #ddd; + color: $color-blue-black; + transition: 0.2s all ease-in-out; + @include font-size(16px); + + &:hover { + border: 1px solid $color-teal-dark; + color: $color-teal-dark; + box-shadow: 0 2px 4px 0 rgba(0,0,0,0.05); + } + } + em { + font-weight: bold; + font-style: normal; + } + hr { + margin: 1.75rem auto 2rem auto; + } + p { + margin-bottom: 1rem; + } +} + +.LoginForm__save-login { + margin-top: 0.5rem; +} diff --git a/src/app/components/modules/MiniHeader.jsx b/src/app/components/modules/MiniHeader.jsx new file mode 100644 index 0000000..a044bbe --- /dev/null +++ b/src/app/components/modules/MiniHeader.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import Icon from 'app/components/elements/Icon'; +import { APP_NAME } from 'app/client_config'; + +export default function MiniHeader() { + return
+
+
+
+ +
+
+
+
; +} diff --git a/src/app/components/modules/MiniHeader.scss b/src/app/components/modules/MiniHeader.scss new file mode 100644 index 0000000..51e64a8 --- /dev/null +++ b/src/app/components/modules/MiniHeader.scss @@ -0,0 +1,21 @@ + + +.mini-header, .mini-header.theme-light, .mini-header.theme-dark { + .logo-new { + transition: 0.2s ease-in-out; + width: 120px; + height: auto; + opacity: 1; + fill: $color-teal; + @include MQ(M) { + width: 144px; + height: auto; + } + } + .icon-svg { + fill: $color-teal; + } + .logo-new:hover .icon-svg { + fill: $color-blue-black; + } +} \ No newline at end of file diff --git a/src/app/components/modules/Modals.jsx b/src/app/components/modules/Modals.jsx new file mode 100644 index 0000000..5114999 --- /dev/null +++ b/src/app/components/modules/Modals.jsx @@ -0,0 +1,128 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import CloseButton from 'react-foundation-components/lib/global/close-button'; +import Reveal from 'react-foundation-components/lib/global/reveal'; +import LoginForm from 'app/components/modules/LoginForm'; +import ConfirmTransactionForm from 'app/components/modules/ConfirmTransactionForm'; +import Transfer from 'app/components/modules/Transfer'; +import SignUp from 'app/components/modules/SignUp'; +import user from 'app/redux/User'; +import Powerdown from 'app/components/modules/Powerdown'; +import tr from 'app/redux/Transaction'; +import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'; +import {NotificationStack} from 'react-notification'; +import {OrderedSet} from 'immutable'; +import TermsAgree from 'app/components/modules/TermsAgree'; + +class Modals extends React.Component { + static propTypes = { + show_login_modal: React.PropTypes.bool, + show_confirm_modal: React.PropTypes.bool, + show_transfer_modal: React.PropTypes.bool, + show_powerdown_modal: React.PropTypes.bool, + show_signup_modal: React.PropTypes.bool, + show_promote_post_modal: React.PropTypes.bool, + hideLogin: React.PropTypes.func.isRequired, + hideConfirm: React.PropTypes.func.isRequired, + hideSignUp: React.PropTypes.func.isRequired, + hideTransfer: React.PropTypes.func.isRequired, + hidePowerdown: React.PropTypes.func.isRequired, + hidePromotePost: React.PropTypes.func.isRequired, + notifications: React.PropTypes.object, + show_terms_modal: React.PropTypes.bool, + removeNotification: React.PropTypes.func, + }; + + constructor() { + super(); + this.shouldComponentUpdate = shouldComponentUpdate(this, 'Modals'); + } + + render() { + const { + show_login_modal, show_confirm_modal, show_transfer_modal, show_powerdown_modal, show_signup_modal, + hideLogin, hideTransfer, hidePowerdown, hideConfirm, hideSignUp, show_terms_modal, + notifications, removeNotification, hidePromotePost, show_promote_post_modal + } = this.props; + + const notifications_array = notifications ? notifications.toArray().map(n => { + n.onClick = () => removeNotification(n.key); + return n; + }) : []; + + return ( +
+ {show_login_modal && + + } + {show_confirm_modal && + + + } + {show_transfer_modal && + + + } + {show_powerdown_modal && + + + } + {show_signup_modal && + + + } + {show_terms_modal && + + } + removeNotification(n.key)} + /> +
+ ); + } +} + +export default connect( + state => { + return { + show_login_modal: state.user.get('show_login_modal'), + show_confirm_modal: state.transaction.get('show_confirm_modal'), + show_transfer_modal: state.user.get('show_transfer_modal'), + show_powerdown_modal: state.user.get('show_powerdown_modal'), + show_promote_post_modal: state.user.get('show_promote_post_modal'), + show_signup_modal: state.user.get('show_signup_modal'), + notifications: state.app.get('notifications'), + show_terms_modal: state.user.get('show_terms_modal') + } + }, + dispatch => ({ + hideLogin: e => { + if (e) e.preventDefault(); + dispatch(user.actions.hideLogin()) + }, + hideConfirm: e => { + if (e) e.preventDefault(); + dispatch(tr.actions.hideConfirm()) + }, + hideTransfer: e => { + if (e) e.preventDefault(); + dispatch(user.actions.hideTransfer()) + }, + hidePowerdown: e => { + if (e) e.preventDefault(); + dispatch(user.actions.hidePowerdown()) + }, + hidePromotePost: e => { + if (e) e.preventDefault(); + dispatch(user.actions.hidePromotePost()) + }, + hideSignUp: e => { + if (e) e.preventDefault(); + dispatch(user.actions.hideSignUp()) + }, + // example: addNotification: ({key, message}) => dispatch({type: 'ADD_NOTIFICATION', payload: {key, message}}), + removeNotification: (key) => dispatch({type: 'REMOVE_NOTIFICATION', payload: {key}}) + }) +)(Modals) diff --git a/src/app/components/modules/Powerdown.jsx b/src/app/components/modules/Powerdown.jsx new file mode 100644 index 0000000..79dc365 --- /dev/null +++ b/src/app/components/modules/Powerdown.jsx @@ -0,0 +1,180 @@ +import React from 'react'; +import {connect} from 'react-redux' +import g from 'app/redux/GlobalReducer' +import reactForm from 'app/utils/ReactForm' +import Slider from 'react-rangeslider'; +import transaction from 'app/redux/Transaction'; +import user from 'app/redux/User'; +import tt from 'counterpart' +import {VEST_TICKER, LIQUID_TICKER, VESTING_TOKEN} from 'app/client_config' +import {numberWithCommas, spToVestsf, vestsToSpf, vestsToSp, assetFloat} from 'app/utils/StateFunctions' + +class Powerdown extends React.Component { + + constructor(props, context) { + super(props, context) + let new_withdraw + if (props.to_withdraw - props.withdrawn > 0) { + new_withdraw = props.to_withdraw - props.withdrawn + } else { + // Set the default withrawal amount to (available - 5 STEEM) + // This should be removed post hf20 + new_withdraw = Math.max(0, props.available_shares - spToVestsf(props.state, 5.001)) + } + this.state = { + broadcasting: false, + manual_entry: false, + new_withdraw, + } + } + + render() { + const {broadcasting, new_withdraw, manual_entry} = this.state + const {account, available_shares, withdrawn, to_withdraw, vesting_shares, delegated_vesting_shares} = this.props + const formatSp = (amount) => numberWithCommas(vestsToSp(this.props.state, amount)) + const sliderChange = (value) => { + this.setState({new_withdraw: value, manual_entry: false}) + } + const inputChange = (event) => { + event.preventDefault() + let value = spToVestsf(this.props.state, parseFloat(event.target.value.replace(/,/g, ''))) + if (!isFinite(value)) { + value = new_withdraw + } + this.setState({new_withdraw: value, manual_entry: event.target.value}) + } + const powerDown = (event) => { + event.preventDefault() + this.setState({broadcasting: true, error_message: undefined}) + const successCallback = this.props.successCallback + const errorCallback = (error) => { + this.setState({broadcasting: false, error_message: String(error)}) + } + // workaround bad math in react-rangeslider + let withdraw = new_withdraw + if (withdraw > vesting_shares - delegated_vesting_shares) { + withdraw = vesting_shares - delegated_vesting_shares + } + const vesting_shares = `${ withdraw.toFixed(6) } ${ VEST_TICKER }` + this.props.withdrawVesting({account, vesting_shares, errorCallback, successCallback}) + } + + const notes = [] + if (to_withdraw - withdrawn > 0) { + const AMOUNT = formatSp(to_withdraw) + const WITHDRAWN = formatSp(withdrawn) + notes.push( +
  • + {tt('powerdown_jsx.already_power_down', {AMOUNT, WITHDRAWN, LIQUID_TICKER})} +
  • + ) + } + if (delegated_vesting_shares !== 0) { + const AMOUNT = formatSp(delegated_vesting_shares) + notes.push( +
  • + {tt('powerdown_jsx.delegating', {AMOUNT, LIQUID_TICKER})} +
  • + ) + } + if (notes.length === 0) { + let AMOUNT = vestsToSpf(this.props.state, new_withdraw) / 13 + AMOUNT = AMOUNT.toFixed(AMOUNT >= 10 ? 0 : 1) + notes.push( +
  • + {tt('powerdown_jsx.per_week', {AMOUNT, LIQUID_TICKER})} +
  • + ) + } + // NOTE: remove this post hf20 + if (new_withdraw > vesting_shares - delegated_vesting_shares - spToVestsf(this.props.state, 5)) { + const AMOUNT = 5 + notes.push( +
  • + {tt('powerdown_jsx.warning', {AMOUNT, VESTING_TOKEN})} +
  • + ) + } + + if (this.state.error_message) { + const MESSAGE = this.state.error_message + notes.push( +
  • + {tt('powerdown_jsx.error', {MESSAGE})} +
  • + ) + } + + return ( +
    +
    +

    {tt('powerdown_jsx.power_down')} {broadcasting}

    +
    + +

    + {tt('powerdown_jsx.amount')}
    + + {LIQUID_TICKER} +

    +
      {notes}
    + +
    + ) + } +} + +export default connect( + // mapStateToProps + (state, ownProps) => { + const values = state.user.get('powerdown_defaults') + const account = values.get('account') + const to_withdraw = parseFloat(values.get('to_withdraw')) / 1e6 + const withdrawn = parseFloat(values.get('withdrawn')) / 1e6 + const vesting_shares = assetFloat(values.get('vesting_shares'), VEST_TICKER) + const delegated_vesting_shares = assetFloat(values.get('delegated_vesting_shares'), VEST_TICKER) + const available_shares = vesting_shares - to_withdraw - withdrawn - delegated_vesting_shares + + return { + ...ownProps, + account, + available_shares, + delegated_vesting_shares, + state, + to_withdraw, + vesting_shares, + withdrawn, + } + }, + // mapDispatchToProps + dispatch => ({ + successCallback: () => { + dispatch(user.actions.hidePowerdown()) + }, + powerDown: (e) => { + e.preventDefault() + const name = 'powerDown'; + dispatch(g.actions.showDialog({name})) + }, + withdrawVesting: ({account, vesting_shares, errorCallback, successCallback}) => { + const successCallbackWrapper = (...args) => { + dispatch({type: 'global/GET_STATE', payload: {url: `@${account}/transfers`}}) + return successCallback(...args) + } + dispatch(transaction.actions.broadcastOperation({ + type: 'withdraw_vesting', + operation: {account, vesting_shares}, + errorCallback, + successCallback: successCallbackWrapper, + })) + }, + }) +)(Powerdown) diff --git a/src/app/components/modules/Powerdown.scss b/src/app/components/modules/Powerdown.scss new file mode 100644 index 0000000..204d79c --- /dev/null +++ b/src/app/components/modules/Powerdown.scss @@ -0,0 +1,70 @@ + + +.PowerdownModal { + + .powerdown-amount { + margin: 18px; + input { + margin-top: 10px; + margin-right: 10px; + width: 30%; + } + } + + li + li { + margin-top: 10px; + } + + .button { + margin: 14px; + } + + .powerdown-notes { + font-size: 80%; + list-style: none; + } + + .rangeslider { + position: relative; + background: #e6e6e6; + .rangeslider__fill, .rangeslider__handle { + position: absolute; + } + &, .rangeslider__fill { + display: block; + box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.3); + border-radius: 10px; + } + .rangeslider__handle { + background: #fff; + border: 1px solid #ccc; + cursor: pointer; + display: inline-block; + position: absolute; + &:active { + background: $color-teal; + } + } + } + .rangeslider-horizontal { + height: 10px; + background: none; + margin: 18px; + .rangeslider__fill { + height: 100%; + background: $color-teal; + box-shadow: none; + left: 0; + } + .rangeslider__handle { + width: 30px; + height: 30px; + border-radius: 50%; + top: -10px; + &:active { + box-shadow: none; + } + } + } + +} \ No newline at end of file diff --git a/src/app/components/modules/PromotePost.jsx b/src/app/components/modules/PromotePost.jsx new file mode 100644 index 0000000..bee738d --- /dev/null +++ b/src/app/components/modules/PromotePost.jsx @@ -0,0 +1,133 @@ +import React, { PropTypes, Component } from 'react'; +import {connect} from 'react-redux'; +import ReactDOM from 'react-dom'; +import transaction from 'app/redux/Transaction'; +import LoadingIndicator from 'app/components/elements/LoadingIndicator'; +import { DEBT_TOKEN, DEBT_TOKEN_SHORT, CURRENCY_SIGN, DEBT_TICKER} from 'app/client_config'; +import tt from 'counterpart'; + +class PromotePost extends Component { + + static propTypes = { + author: PropTypes.string.isRequired, + permlink: PropTypes.string.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + amount: '1.0', + asset: '', + loading: false, + amountError: '', + trxError: '' + }; + this.onSubmit = this.onSubmit.bind(this); + this.errorCallback = this.errorCallback.bind(this); + this.amountChange = this.amountChange.bind(this); + // this.assetChange = this.assetChange.bind(this); + } + + componentDidMount() { + setTimeout(() => { + ReactDOM.findDOMNode(this.refs.amount).focus() + }, 300) + } + + errorCallback(estr) { + this.setState({ trxError: estr, loading: false }); + } + + onSubmit(e) { + e.preventDefault(); + const {author, permlink, onClose} = this.props + const {amount} = this.state + this.setState({loading: true}); + console.log('-- PromotePost.onSubmit -->'); + this.props.dispatchSubmit({amount, asset: DEBT_TICKER, author, permlink, onClose, + currentUser: this.props.currentUser, errorCallback: this.errorCallback}); + } + + amountChange(e) { + const amount = e.target.value; + // console.log('-- PromotePost.amountChange -->', amount); + this.setState({amount}); + } + + // assetChange(e) { + // const asset = e.target.value; + // console.log('-- PromotePost.assetChange -->', e.target.value); + // this.setState({asset}); + // } + + render() { + const {amount, loading, amountError, trxError} = this.state; + const {currentAccount} = this.props; + const balanceValue = currentAccount.get('sbd_balance'); + const balance = balanceValue ? balanceValue.split(' ')[0] : 0.0; + const submitDisabled = !amount; + + return ( +
    +
    +
    this.setState({trxError: ''})}> +

    {tt('promote_post_jsx.promote_post')}

    +

    {tt('promote_post_jsx.spend_your_DEBT_TOKEN_to_advertise_this_post', {DEBT_TOKEN})}.

    +
    +
    +
    + +
    + + {DEBT_TOKEN_SHORT + ' '} ({CURRENCY_SIGN}) +
    {amountError}
    +
    +
    +
    +
    {`${tt('g.balance')}: ${balance} ${DEBT_TOKEN_SHORT} (${CURRENCY_SIGN})`}
    +
    + {loading &&
    } + {!loading && + {trxError &&
    {trxError}
    } + +
    } +
    +
    +
    + ) + } +} + +// const AssetBalance = ({onClick, balanceValue}) => +// Balance: {balanceValue} + +export default connect( + (state, ownProps) => { + const currentUser = state.user.getIn(['current']); + const currentAccount = state.global.getIn(['accounts', currentUser.get('username')]); + return {...ownProps, currentAccount, currentUser} + }, + + // mapDispatchToProps + dispatch => ({ + dispatchSubmit: ({amount, asset, author, permlink, currentUser, onClose, errorCallback}) => { + const username = currentUser.get('username') + const successCallback = () => { + dispatch({type: 'global/GET_STATE', payload: {url: `@${username}/transfers`}}) // refresh transfer history + onClose() + } + const operation = { + from: username, + to: 'null', amount: parseFloat(amount, 10).toFixed(3) + ' ' + asset, + memo: `@${author}/${permlink}`, + __config: {successMessage: tt('promote_post_jsx.you_successfully_promoted_this_post') + '.'} + } + dispatch(transaction.actions.broadcastOperation({ + type: 'transfer', + operation, + successCallback, + errorCallback + })) + } + }) +)(PromotePost) diff --git a/src/app/components/modules/Settings.jsx b/src/app/components/modules/Settings.jsx new file mode 100644 index 0000000..f32358c --- /dev/null +++ b/src/app/components/modules/Settings.jsx @@ -0,0 +1,263 @@ +import React from 'react'; +import {connect} from 'react-redux' +import user from 'app/redux/User'; +import tt from 'counterpart'; +import transaction from 'app/redux/Transaction' +import o2j from 'shared/clash/object2json' +import LoadingIndicator from 'app/components/elements/LoadingIndicator' +import reactForm from 'app/utils/ReactForm' +import UserList from 'app/components/elements/UserList'; + + +class Settings extends React.Component { + + constructor(props) { + super(props); + this.state = { + errorMessage: '', + successMessage: '', + } + this.initForm(props); + this.onNsfwPrefChange = this.onNsfwPrefChange.bind(this); + } + + initForm(props) { + reactForm({ + instance: this, + name: 'accountSettings', + fields: ['profile_image', 'cover_image', 'name', 'about', 'location', 'website'], + initialValues: props.profile, + validation: values => ({ + profile_image: values.profile_image && !/^https?:\/\//.test(values.profile_image) ? tt('settings_jsx.invalid_url') : null, + cover_image: values.cover_image && !/^https?:\/\//.test(values.cover_image) ? tt('settings_jsx.invalid_url') : null, + name: values.name && values.name.length > 20 ? tt('settings_jsx.name_is_too_long') : values.name && /^\s*@/.test(values.name) ? tt('settings_jsx.name_must_not_begin_with') : null, + about: values.about && values.about.length > 160 ? tt('settings_jsx.about_is_too_long') : null, + location: values.location && values.location.length > 30 ? tt('settings_jsx.location_is_too_long') : null, + website: values.website && values.website.length > 100 ? tt('settings_jsx.website_url_is_too_long') : values.website && !/^https?:\/\//.test(values.website) ? tt('settings_jsx.invalid_url') : null, + }) + }) + this.handleSubmitForm = + this.state.accountSettings.handleSubmit(args => this.handleSubmit(args)) + } + + componentWillMount() { + const {accountname} = this.props + const nsfwPref = (process.env.BROWSER ? localStorage.getItem('nsfwPref-' + accountname) : null) || 'warn' + this.setState({nsfwPref, oldNsfwPref: nsfwPref}) + } + + onNsfwPrefChange(e) { + const nsfwPref = e.currentTarget.value; + const userPreferences = {...this.props.user_preferences, nsfwPref} + this.props.setUserPreferences(userPreferences) + } + + handleSubmit = ({updateInitialValues}) => { + let {metaData} = this.props + if (!metaData) metaData = {} + if(!metaData.profile) metaData.profile = {} + delete metaData.user_image; // old field... cleanup + + const {profile_image, cover_image, name, about, location, website} = this.state + + // Update relevant fields + metaData.profile.profile_image = profile_image.value + metaData.profile.cover_image = cover_image.value + metaData.profile.name = name.value + metaData.profile.about = about.value + metaData.profile.location = location.value + metaData.profile.website = website.value + + // Remove empty keys + if(!metaData.profile.profile_image) delete metaData.profile.profile_image; + if(!metaData.profile.cover_image) delete metaData.profile.cover_image; + if(!metaData.profile.name) delete metaData.profile.name; + if(!metaData.profile.about) delete metaData.profile.about; + if(!metaData.profile.location) delete metaData.profile.location; + if(!metaData.profile.website) delete metaData.profile.website; + + const {account, updateAccount} = this.props + this.setState({loading: true}) + updateAccount({ + json_metadata: JSON.stringify(metaData), + account: account.name, + memo_key: account.memo_key, + errorCallback: (e) => { + if (e === 'Canceled') { + this.setState({ + loading: false, + errorMessage: '' + }) + } else { + console.log('updateAccount ERROR', e) + this.setState({ + loading: false, + changed: false, + errorMessage: tt('g.server_returned_error') + }) + } + }, + successCallback: () => { + this.setState({ + loading: false, + changed: false, + errorMessage: '', + successMessage: tt('g.saved') + '!', + }) + // remove successMessage after a while + setTimeout(() => this.setState({successMessage: ''}), 4000) + updateInitialValues() + } + }) + } + + handleLanguageChange = (event) => { + const locale = event.target.value; + const userPreferences = {...this.props.user_preferences, locale} + this.props.setUserPreferences(userPreferences) + } + + render() { + const {state, props} = this + + const {submitting, valid, touched} = this.state.accountSettings + const disabled = !props.isOwnAccount || state.loading || submitting || !valid || !touched + + const {profile_image, cover_image, name, about, location, website} = this.state + + const {follow, account, isOwnAccount, user_preferences} = this.props + const following = follow && follow.getIn(['getFollowingAsync', account.name]); + const ignores = isOwnAccount && following && following.get('ignore_result') + + return
    +
    +
    + +
    +
    +
    +
    +
    +

    {tt('settings_jsx.public_profile_settings')}

    + +
    {profile_image.blur && profile_image.touched && profile_image.error}
    + + +
    {cover_image.blur && cover_image.touched && cover_image.error}
    + + +
    {name.touched && name.error}
    + + +
    {about.touched && about.error}
    + + +
    {location.touched && location.error}
    + + +
    {website.blur && website.touched && website.error}
    + +
    + {state.loading &&
    } + {!state.loading && } + {' '}{ + state.errorMessage + ? {state.errorMessage} + : state.successMessage + ? {state.successMessage} + : null + } +
    +
    + + {isOwnAccount && +
    +
    +

    +

    {tt('settings_jsx.private_post_display_settings')}

    +
    + {tt('settings_jsx.not_safe_for_work_nsfw_content')} +
    + +
    +
     
    +
    +
    } + {ignores && ignores.size > 0 && +
    +
    +

    + +
    +
    } +
    + } +} + +export default connect( + // mapStateToProps + (state, ownProps) => { + const {accountname} = ownProps.routeParams + const account = state.global.getIn(['accounts', accountname]).toJS() + const current_user = state.user.get('current') + const username = current_user ? current_user.get('username') : '' + let metaData = account ? o2j.ifStringParseJSON(account.json_metadata) : {} + if (typeof metaData === 'string') metaData = o2j.ifStringParseJSON(metaData); // issue #1237 + const profile = metaData && metaData.profile ? metaData.profile : {}; + const user_preferences = state.app.get('user_preferences').toJS(); + + return { + account, + metaData, + accountname, + isOwnAccount: username == accountname, + profile, + follow: state.global.get('follow'), + user_preferences, + ...ownProps + } + }, + // mapDispatchToProps + dispatch => ({ + changeLanguage: (language) => { + dispatch(user.actions.changeLanguage(language)) + }, + updateAccount: ({successCallback, errorCallback, ...operation}) => { + const options = {type: 'account_update', operation, successCallback, errorCallback} + dispatch(transaction.actions.broadcastOperation(options)) + }, + setUserPreferences: (payload) => { + dispatch({type: 'SET_USER_PREFERENCES', payload}) + } + }) +)(Settings) diff --git a/src/app/components/modules/Settings.scss b/src/app/components/modules/Settings.scss new file mode 100644 index 0000000..48cdd74 --- /dev/null +++ b/src/app/components/modules/Settings.scss @@ -0,0 +1,57 @@ + +.Settings { + .button { + text-decoration: none; + font-weight: bold; + transition: 0.2s all ease-in-out; + text-transform: capitalize; + border-radius: 0; + @include font-size(18px); + @include themify($themes) { + background-color: themed('buttonBackground'); + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0), 5px 5px 0 0 themed('buttonBoxShadow'); + color: themed('buttonText'); + } + &:hover, &:focus { + @include themify($themes) { + background-color: themed('buttonBackgroundHover'); + box-shadow: 2px 2px 2px 0 rgba(0,0,0,0.1), 7px 7px 0 0 themed('buttonBoxShadowHover'); + color: themed('buttonTextHover'); + } + } + &:visited, &:active { + @include themify($themes) { + color: themed('ButtonText'); + } + } + } + .button.disabled, .button[disabled] { + opacity: 0.25; + cursor: not-allowed; + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0); + &:hover { + @include themify($themes) { + background-color: themed('buttonBackground'); + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0); + color: themed('buttonText'); + } + } + } + .success, .error { + text-transform: capitalize; + padding-left: 8px; + } + .success { + @include themify($themes) { + color: themed('textColorAccent'); + } + } + p.error { + position: relative; + top: 4px; + line-height: 1.2; + } + div.error { + padding-left: 0; + } +} \ No newline at end of file diff --git a/src/app/components/modules/SidePanel.jsx b/src/app/components/modules/SidePanel.jsx new file mode 100644 index 0000000..c90fa98 --- /dev/null +++ b/src/app/components/modules/SidePanel.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import CloseButton from 'react-foundation-components/lib/global/close-button'; + +export default class SidePanel extends React.Component { + static propTypes = { + children: React.PropTypes.array, + alignment: React.PropTypes.string + }; + + constructor(props) { + super(props); + this.state = {visible: false}; + this.hide = this.hide.bind(this); + } + + componentWillUnmount() { + document.removeEventListener('click', this.hide); + } + + show = () => { + this.setState({visible: true}); + document.addEventListener('click', this.hide); + }; + + hide = () => { + this.setState({visible: false}); + document.removeEventListener('click', this.hide); + }; + + render() { + const {visible} = this.state; + const {children, alignment} = this.props; + return
    +
    + + {children} +
    +
    ; + } +} + diff --git a/src/app/components/modules/SidePanel.scss b/src/app/components/modules/SidePanel.scss new file mode 100644 index 0000000..29c9e97 --- /dev/null +++ b/src/app/components/modules/SidePanel.scss @@ -0,0 +1,86 @@ +$menu-width: 250px; + +.SidePanel { + display: block; + + .menu > li.last { + border-bottom: 1px solid $color-border-dark; + color: $color-text-gray-light; + } + + > div { + background-color: $color-blue-black-darkest; + color: $color-white; + padding-top: 3rem; + .close-button { + color: $color-white; + } + .menu > li { + > a { + transition: 0.2s all ease-in-out; + border-top: 1px solid $color-blue-black; + color: $color-white; + border-bottom: 1px solid $color-blue-black-darkest; + } + > a:hover, &:focus { + background-color: $color-blue-black; + border-bottom: 1px solid $color-teal; + + path { + fill: $color-teal; + } + } + path { + fill: $color-text-gray-light; + } + } + ul:nth-of-type(n+3) { + margin-top: 2rem; + } + + position: fixed; + z-index: 1000; + top: 0; + width: $menu-width; + height: 100%; + min-height: 100vh; + box-sizing: border-box; + transition: visibility 250ms, transform ease 250ms; + + &.left { + left: -$menu-width; + } + + &.visible.left { + transform: translate3d($menu-width, 0, 0); + } + + &.right { + right: -$menu-width; + visibility: hidden; + overflow-y: auto; + } + + &.visible.right { + transform: translate3d(-$menu-width, 0, 0); + visibility: visible; + } + } + .Icon.extlink { + position: relative; + top: 3px; + left: 2px; + } +} + +/* Small only */ +@media screen and (max-width: 39.9375em) { + .SidePanel { + div ul:nth-of-type(n+2) { + margin-top: 2rem; + } + > div > .menu > li > a { + padding: 0.3rem 1rem; + } +} +} diff --git a/src/app/components/modules/SidebarModule.jsx b/src/app/components/modules/SidebarModule.jsx new file mode 100644 index 0000000..aabcb58 --- /dev/null +++ b/src/app/components/modules/SidebarModule.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +export class SidebarModule extends React.Component { + + render() { + return ( +
    +
    +

    Links React Component

    +
    +
    + +
    +
    + ); + } +} + diff --git a/src/app/components/modules/SignUp.jsx b/src/app/components/modules/SignUp.jsx new file mode 100644 index 0000000..0acbcab --- /dev/null +++ b/src/app/components/modules/SignUp.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import SvgImage from 'app/components/elements/SvgImage'; +import AddToWaitingList from 'app/components/modules/AddToWaitingList'; + +class SignUp extends React.Component { + constructor() { + super(); + this.state = {waiting_list: false}; + } + render() { + if ($STM_Config.read_only_mode) { + return
    +
    +
    +

    Due to server maintenance we are running in read only mode. We are sorry for the inconvenience.

    +
    +
    ; + } + + if (this.props.serverBusy || $STM_Config.disable_signups) { + return
    +
    +

    Membership to Steemit.com is now under invitation only because of unexpectedly high sign up rate. + Submit your email to get on the waiting list.

    + +
    +
    ; + } + + return
    +
    +
    +

    Sign Up

    +

    Steemit funds each account with over {this.props.signup_bonus} worth of Steem Power; to prevent abuse, we + require new users to login via social media.
    + Your personal information will be kept private. +

    +
    +
    +
    +
    + +
    + +
    +
    +   +
    +
    +
    + +
    +
    + Continue with Reddit +
    (requires 5 or more Reddit comment karma) +
    +
    +
    +
    +
    + Don't have a Facebook or Reddit account?
    + {this.state.waiting_list ? : this.setState({waiting_list: true})}> + Subscribe to get a notification when SMS confirmation is available. + } +
    +
    +
    +
    +
    +

    By verifying your account you agree to the Steemit terms and conditions.

    +
    +
    +
    + } +} + +export default connect( + state => { + return { + signup_bonus: state.offchain.get('signup_bonus'), + serverBusy: state.offchain.get('serverBusy') + }; + } +)(SignUp); diff --git a/src/app/components/modules/SignUp.scss b/src/app/components/modules/SignUp.scss new file mode 100644 index 0000000..bb5f297 --- /dev/null +++ b/src/app/components/modules/SignUp.scss @@ -0,0 +1,31 @@ +.SignUp { + padding: 1rem 1rem 2rem 1rem; + a.button { + margin-top: 0.5rem; + } + .button { + width: 16rem; + } +} + +.SignUp--reddit-button { + background-color: #ef4623; + &:hover { + background-color: scale-color(#ef4623, -6%); + } +} + +.SignUp--fb-button { + background-color: #3b5998; + &:hover { + background-color: scale-color(#3b5998, -6%); + } +} + +.TermsAgree { + height: 10em; + resize: none; + display: block; + overflow: auto; + border: 1px solid #cacaca; +} diff --git a/src/app/components/modules/TermsAgree.jsx b/src/app/components/modules/TermsAgree.jsx new file mode 100644 index 0000000..5c903f2 --- /dev/null +++ b/src/app/components/modules/TermsAgree.jsx @@ -0,0 +1,176 @@ +/* eslint react/prop-types: 0 */ +import React, { PropTypes, Component } from 'react'; +import {serverApiRecordEvent} from 'app/utils/ServerApiClient'; +import { translate } from 'app/Translator'; +import { Tos } from 'app/components/pages/Tos'; + +class TermsAgree extends Component { + + constructor() { + super(); + this.termsAgree = this.termsAgree.bind(this); + this.termsCancel = this.termsCancel.bind(this); + } + + termsAgree(e) { + // let user proceed + serverApiRecordEvent('AgreeTerms', true); + document.querySelectorAll('[role=dialog]')[0].remove(); + } + + termsCancel() { + // do not allow to proceed + serverApiRecordEvent('CancelTerms', true); + window.location.href = "/"; + } + + static propTypes = { + // redux + }; + + render() { + return ( +
    +

    Terms and Service

    +
    +
    + +
    +
    +

    Steemit Terms of Service

    +

    Last Updated April 28, 2016

    Welcome to Steemit! These Terms of Service (“Terms”) apply to your access to and use of Steemit.com and any other products or services that link to these Terms (“Steemit”). Steemit is provided by Steemit, Inc. (“Steemit”, “we” + or “us”). By accessing or using Steemit, you agree to be bound by these Terms. If you do not agree to these Terms, including the mandatory arbitration provision and class action waiver in Section 14, do not access or use Steemit. If we make changes to these Terms, we will provide notice of those changes by updating the “Last Updated” date above or posting notice on Steemit. Your continued use of Steemit will confirm your acceptance of the changes. +

    You understand and agree that these Terms apply solely to your access to, and use of, Steemit and that, when you use other Steemit services such as Steemit.com or Steemitgifts.com, the terms and policies particular to those services apply. +

    1. Privacy Policy

    Please refer to our Privacy Policy for information about how we collect, use and disclose information about you. +

    2. Eligibility

    Steemit is not targeted towards, nor intended for use by, anyone under the age of 13. You must be at least 13 years of age to access or use Steemit. If you are between 13 and 18 years of age (or the age of legal majority where you reside), you may only access or use Steemit under the supervision of a parent or legal guardian who agrees to be bound by these Terms. +

    3. Copyright and Limited License

    Steemit contains data, text, photographs, images, video, audio, graphics, articles, comments, software, code, scripts and other content supplied by us, the Steem blockchain or our licensors, which we call “Steemit Content.” Steemit Content is protected by intellectual property laws, including copyright and other proprietary rights of the United States and foreign countries. Except as explicitly stated in these Terms, Steemit does not grant any express or implied rights to use Steemit Content. +

    You are granted a limited, nonexclusive, non-transferable, and non-sublicensable license to access and use Steemit and Steemit Content for your personal, non-commercial use. This license is subject to these Terms and does not include any right to: (a) distribute, publicly perform or publicly display any Steemit Content; (b) modify or otherwise make any derivative uses of Steemit or Steemit Content, or any portion thereof; (c) use any data mining, robots or similar data gathering or extraction methods; and (d) use Steemit or Steemit Content other than for their intended purposes. Any use of Steemit or Steemit Content other than as authorized in these Terms is strictly prohibited and will terminate the license granted herein. This license is revocable at any time. +

    4. Adult-Oriented Content

    Steemit is intended for a general audience and, as a result, some Steemit Content may discuss or depict adult-oriented topics. We realize that this content may not be appropriate or desirable for some of our readers depending on their current location, age, background or personal views. As a result, we mark this content as Not Safe For Work (“NSFW”).

    Marking Steemit Content as NSFW does not prevent you from being able to access this content but, instead, helps you make informed decisions about the type of content you view on Steemit. You understand and agree that you access content marked as NSFW at your own risk. +

    5. Trademarks

    “Steem,” “ + Steemit,” the Steemit logo and any other product or service names, logos or slogans that may appear on Steemit are trademarks of Steemit and may not be copied, imitated or used, in whole or in part, without our prior written permission. You may not use any metatags or other “ + hidden text” utilizing “Steemit” or any other name, trademark or product or service name of Steemit without our prior written permission. In addition, the look and feel of Steemit, including, without limitation, all page headers, custom graphics, button icons and scripts, constitute the service mark, trademark or trade dress of Steemit and may not be copied, imitated or used, in whole or in part, without our prior written permission. All other trademarks, registered trademarks, product names and company names or logos mentioned or used on Steemit are the property of their respective owners and may not be copied, imitated or used, in whole or in part, without the permission of the applicable trademark holder. Reference to any products, services, processes or other information by name, trademark, manufacturer, supplier or otherwise does not constitute or imply endorsement, sponsorship or recommendation by Steemit.                                         +

    6. Assumption of Risk, Limitations on Liability & Indemnity

    6.1.   + You accept and acknowledge that there are risks associated with utilizing an Internet- based STEEM account service including, but not limited to, the risk of failure of hardware, software and Internet connections, the risk of malicious software introduction, and the risk that third parties may obtain unauthorized access to information stored within your Account, including, but not limited to your Private Key (as defined below at 10.2.). You accept and acknowledge that Steemit will not be responsible for any communication failures, disruptions, errors, distortions or delays you may experience when using the Services, however caused.         +

    6.2.  We will use reasonable endeavours to verify the accuracy of any information on the Service but we make no representation or warranty of any kind, express or implied, statutory or otherwise, regarding the contents of the Service, information and functions made accessible through the Service, any hyperlinks to third party websites, nor for any breach of security associated with the transmission of information through the Service or any website linked to by the Service.                                                 +

    6.3.  We will not be responsible or liable to you for any loss and take no responsibility for and will not be liable to you for any use of our Services, including but not limited to any losses, damages or claims arising from: (a) User error such as forgotten passwords, incorrectly constructed transactions, or mistyped STEEM addresses; (b) Server failure or data loss; (c) Corrupted Account files; (d) Unauthorized access to applications; (e) Any unauthorized third party activities, including without limitation the use of viruses, phishing, bruteforcing or other means of attack against the Service or Services.                                                                 +

    6.4.  We make no warranty that the Service or the server that makes it available, are free of viruses or errors, that its content is accurate, that it will be uninterrupted, or that defects will be corrected. We will not be responsible or liable to you for any loss of any kind, from action taken, or taken in reliance on material, or information, contained on the Service.                                         +

    6.5.  Subject to 7.1 below, any and all indemnities, warranties, terms and conditions (whether express or implied) are hereby excluded to the fullest extent permitted under Luxembourg law. +

    6.6.  We will not be liable, in contract, or tort (including, without limitation, negligence), other than where we have been fraudulent or made negligent misrepresentations. +

    6.7.  Nothing in these Terms excludes or limits liability for death or personal injury caused by negligence, fraudulent misrepresentation, or any other liability which may not otherwise be limited or excluded under United States law. +

    7. Agreement to Hold Steemit Harmless

    7.1.  You agree to hold harmless Steemit (and each of our officers, directors, members, employees, agents and affiliates) from any claim, demand, action, damage, loss, cost or expense, including without limitation reasonable legal fees, arising out or relating to:         +

    7.1.1.  Your use of, or conduct in connection with, our Services;

    7.1.2.  Any feedback or submissions you provide (see 19 below);

    7.1.3.   + Your violation of these Terms; or

    7.1.4.  Violation of any rights of any other person or entity. +

    7.2.  If you are obligated to indemnify us, we will have the right, in our sole discretion, to control any action or proceeding (at our expense) and determine whether we wish to settle it.                                                                                  +

    8. No Liability For Third Party Services And Content

    8.1. In using our Services, you may view content or utilize services provided by third parties, including links to web pages and services of such parties (“ + Third Party Content”). We do not control, endorse or adopt any Third-Party Content and will have no responsibility for Third Party Content including, without limitation, material that may be misleading, incomplete, erroneous, offensive, indecent or otherwise objectionable in your jurisdiction. In addition, your dealings or correspondence with such third parties are solely between you and the third parties. We are not responsible or liable for any loss or damage of any sort incurred as a result of any such dealings and you understand that your use of Third Party Content, and your interactions with third parties, is at your own risk.                                 +

    9. Account Registration        

    9.1.  You need not use a Steemit Account. If you wish to use an Account, you must create an Account with Steemit to access the Services (“ + Account”). When you create an Account, you are strongly advised to take the following precautions, as failure to do so may result in loss of access to, and/or control over, your Wallet: (a) Create a strong password that you do not use for any other website or online service; (b) Provide accurate and truthful information; (c) Maintain and promptly update your Account information; (d) maintain the security of your Account by protecting your Account password and access to your computer and your Account; (e) Promptly notify us if you discover or otherwise suspect any security breaches related to your Account.                 +

    9.2.  You hereby accept and acknowledge that you take responsibility for all activities that occur under your Account and accept all risks of any authorized or unauthorized access to your Account, to the maximum extent permitted by law.                                                                                                  +

    10. The Steemit Services                 +

    10.1.  As described in more detail below, the Services, among other things, provide in-browser (or otherwise local) software that (a) generates and stores STEEM ACCOUNT Addresses and encrypted Private Keys (defined below), and (b) Facilitates the submission of STEEM transaction data to the Steem network without requiring you to access the STEEM command line interface.         +

    10.2.  Account Names and Private Keys. When you create an Account, the Services generate and store a cryptographic private and public key pair that you may use to send and receive STEEM and Steem Dollars via the Steem network.. The Private Key uniquely matches the Account Name and must be used in connection with the Account Name to authorize the transfer of STEEM and Steem Dollars from that Account. You are solely responsible for maintaining the security of your Private Key and any password phrase associated with your wallet. You must keep your Account, password phrase and Private Key access information secure. Failure to do so may result in the loss of control of Steem, Steem Power and Steem Dollars associated with the Wallet. +

    10.3.  No Password Retrieval. Steemit does not receive or store your Account password, nor the unencrypted keys and addresses. Therefore, we cannot assist you with Account password retrieval. Our Services provide you with tools to help you remember or recover your password, including by allowing you to set password hints, but the Services cannot generate a new password for your Account. You are solely responsible for remembering your Account password. If you have not safely stored a backup of any Account Names and password pairs maintained in your Account, you accept and acknowledge that any STEEM, Steem Dollars and Steem Power you have associated with such Account will become inaccessible if you do not have your Account password. +

    10.4.  Transactions. In order to be completed, all proposed Steem transactions must be confirmed and recorded in the Steem public ledger via the Steem distributed consensus network (a peer-to-peer economic network that operates on a cryptographic protocol), which is not owned, controlled or operated by Steemit. The Steem Network is operated by a decentralized network of independent third parties. Blockchain has no control over the Steem network and therefore cannot and does not ensure that any transaction details you submit via the Services will be confirmed via the Steem network. You acknowledge and agree that the transaction details you submit via the Services may not be completed, or may be substantially delayed, by the Steem network. You may use the Services to submit these details to the network.                                                          +

    10.5.  No Storage or Transmission of STEEM, Steem Dollars or Steem Power. Steem, in any of its forms (STEEM, Steem Dollars and Steem Power) is an intangible, digital asset. They exist only by virtue of the ownership record maintained in the Steem network. The Service does not store, send or receive Steem. Any transfer of title that might occur in any STEEM, Steem Dollars or Steem Power occurs on the decentralized ledger within the Steem network and not within the Services. We do not guarantee that the Service can effect the transfer of title or right in any Steem, Steem Dollars or Steem Power. +

    10.6.  Relationship. Nothing in these Terms is intended to nor shall create any partnership, joint venture, agency, consultancy or trusteeship, you and Steemit being with respect to one another independent contractors. +

    10.7.  Accuracy of Information. You represent and warrant that any information you provide via the Services is accurate and complete. You accept and acknowledge that Steemit is not responsible for any errors or omissions that you make in connection with any Steem transaction initiated via the Services, for instance, if you mistype an Account Name or otherwise provide incorrect information. We strongly encourage you to review your transaction details carefully before completing them via the Services. +

    10.8.  No Cancellations or Modifications. Once transaction details have been submitted to the Steem network via the Services, The Services cannot assist you to cancel or otherwise modify your transaction details. Steemit has no control over the Steem Network and does not have the ability to facilitate any cancellation or modification requests. +

    10.9.  Taxes. It is your responsibility to determine what, if any, taxes apply to the transactions you for which you have submitted transaction details via the Services, and it is your responsibility to report and remit the correct tax to the appropriate tax authority. You agree that Steemit is not responsible for determining whether taxes apply to your Steem transactions or for collecting, reporting, withholding or remitting any taxes arising from any Steem transactions.                                                                                                  +

    11. Fees for Using the Steemit Services

    11.1. Company Fees Creating an Account. Steemit does not currently charge fees for any Services, however we reserve the right to do so in future, and in such case any applicable fees will be displayed prior to you using any Service to which a fee applies. +

    12. No Right To Cancel And/Or Reverse Steem Transactions

    14.1. If you use a Service to which Steem, Steem Dollars or Steem Power is transacted, you will not be able to change your mind once you have confirmed that you wish to proceed with the Service or transaction. +

    15. Discontinuation of Services                                                          +

    15.1.  We may, in our sole discretion and without cost to you, with or without prior notice and at any time, modify or discontinue, temporarily or permanently, any portion of our Services. You are solely responsible for storing, outside of the Services, a backup of any Account and Private Key pair that you maintain in your Wallet.                                 +

    15.2.  If you do not maintain a backup of your Account data outside of the Services, you will be may not be able to access Steem, Steem Dollars and Steem Power associated with any Account Name maintained in your Account in the event that we discontinue or deprecate the Services. +

    16. Suspension or Termination of Service.

    16.1. We may suspend or terminate your access to the Services in our sole discretion, immediately and without prior notice, and delete or deactivate your Account and all related information and files in such without cost to you, including, for instance, in the event that you breach any term of these Terms. In the event of termination, your access to funds will depend on your access to your backup of your Account data including your Account Name and Private Keys. +

    17. User Conduct

    17.1. When accessing or using the Services, you agree that you will not commit any unlawful act, and that you are solely responsible for your conduct while using our Services. Without limiting the generality of the foregoing, you agree that you will not: +

    17.1.1. Use our Services in any manner that could interfere with, disrupt, negatively affect or inhibit other users from fully enjoying our Services, or that could damage, disable, overburden or impair the functioning of our Services in any manner;         +

    17.1.2.  Use our Services to pay for, support or otherwise engage in any illegal activities, including, but not limited to illegal gambling, fraud, money- laundering, or terrorist activities. +

    17.1.3.  Use any robot, spider, crawler, scraper or other automated means or interface not provided by us to access our Services or to extract data;                                 +

    17.1.4.  Use or attempt to use another user’s Wallet without authorization;

    17.1.5.  Attempt to circumvent any content filtering techniques we employ, or attempt to access any service or area of our Services that you are not authorized to access; +

    17.1.6.   + Introduce to the Services any virus, Trojan, worms, logic bombs or other harmful material;

    17.1.7.   + Develop any third-party applications that interact with our Services without our prior written consent;                                 +

    17.1.8.   + Provide false, inaccurate, or misleading information; or                 +

    17.1.9.  Encourage or induce any third party to engage in any of the activities prohibited under this Section.          +

    17.1.10.  Reverse engineer any aspect of Steemit or do anything that might discover source code or bypass or circumvent measures employed to prevent or limit access to any Steemit Content, area or code of Steemit. +

    18. Third-Party Content and Sites

    + Steemit may include links and other content owned or operated by third parties, including advertisements and social “ + widgets” (we call these “Third-Party Content“). You agree that Steemit is not responsible or liable for Third-Party Content and that you access and use Third-Party Content at your own risk. Your interactions with Third-Party Content are solely between you and the third party providing the content. When you leave Steemit, you should understand that these Terms no longer govern and that the terms and policies of those third-party sites or services will then apply. +

    19. Feedback

    You may submit questions, comments, feedback, suggestions, and other information regarding Steemit (we call this “Feedback“). You acknowledge and agree that Feedback is non-confidential and will become the sole property of Steemit. Steemit shall own exclusive rights, including, without limitation, all intellectual property rights, in and to such Feedback and is entitled to the unrestricted use and dissemination of this Feedback for any purpose, without acknowledgment or compensation to you. You agree to execute any documentation required by Steemit to confirm such assignment to Steemit. +

    20. Copyright Complaints

    Steemit respects the intellectual property of others by not reading infringed content from the Steem blockchain. If you believe that your work has been copied in a way that constitutes copyright infringement, you may notify Steemit’ + s Designated Agent by contacting:

    contact@steemit.com

    Please see 17 + U.S.C. §512(c)(3) for the requirements of a proper notification. You should note that if you knowingly misrepresent in your notification that the material or activity is infringing, you may be liable for any damages, including costs and attorneys’ + fees, incurred by Steemit or the alleged infringer, as the result of Steemit’s relying upon such misrepresentation in removing or disabling access to the material or activity claimed to be infringing. +

    21. Disclaimers

    TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, STEEMIT AND THE STEEMIT CONTENT ARE PROVIDED ON AN “ + AS IS” AND “AS AVAILABLE” BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT AND ANY WARRANTIES IMPLIED BY ANY COURSE OF PERFORMANCE OR USAGE OF TRADE. STEEMIT DOES NOT REPRESENT OR WARRANT THAT STEEMIT AND THE STEEMIT CONTENT: (A) WILL BE SECURE OR AVAILABLE AT ANY PARTICULAR TIME OR LOCATION; (B) ARE ACCURATE, COMPLETE, RELIABLE, CURRENT OR ERROR-FREE OR THAT ANY DEFECTS OR ERRORS WILL BE CORRECTED; AND (C) ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS. YOUR USE OF STEEMIT AND THE STEEMIT CONTENT IS SOLELY AT YOUR OWN RISK. SOME JURISDICTIONS DO NOT ALLOW THE DISCLAIMER OF IMPLIED TERMS IN CONTRACTS WITH CONSUMERS, SO SOME OR ALL OF THE DISCLAIMERS IN THIS SECTION MAY NOT APPLY TO YOU. +

    22. Limitation of Liability

    TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL STEEMIT OR THE STEEMIT PARTIES BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, EXEMPLARY OR PUNITIVE DAMAGES, OR ANY OTHER DAMAGES OF ANY KIND, INCLUDING, BUT NOT LIMITED TO, LOSS OF USE, LOSS OF PROFITS OR LOSS OF DATA, WHETHER IN AN ACTION IN CONTRACT, TORT (INCLUDING, BUT NOT LIMITED TO, NEGLIGENCE) OR OTHERWISE, ARISING OUT OF, OR IN ANY WAY CONNECTED WITH, THE USE OF, OR INABILITY TO USE, STEEMIT OR THE STEEMIT CONTENT. TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE AGGREGATE LIABILITY OF STEEMIT OR THE STEEMIT PARTIES, WHETHER IN CONTRACT, WARRANTY, TORT (INCLUDING NEGLIGENCE, WHETHER ACTIVE, PASSIVE OR IMPUTED), PRODUCT LIABILITY, STRICT LIABILITY OR OTHER THEORY, ARISING OUT OF OR RELATING TO: (A) THE USE OF OR INABILITY TO USE STEEMIT OR THE STEEMIT CONTENT; OR (B) THESE TERMS EXCEED ANY COMPENSATION YOU PAY, IF ANY, TO STEEMIT FOR ACCESS TO OR USE OF STEEMIT. +

    SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES, SO SOME OR ALL OF THE EXCLUSIONS AND LIMITATIONS IN THIS SECTION MAY NOT APPLY TO YOU. +

    23. Modifications to Steemit

    Steemit reserves the right to modify or discontinue, temporarily or permanently, Steemit, or any features or portions of Steemit, without prior notice. You agree that Steemit will not be liable for any modification, suspension or discontinuance of Steemit, or any part of Steemit. +

    24. Arbitration

    PLEASE READ THE FOLLOWING SECTION CAREFULLY BECAUSE IT REQUIRES YOU TO ARBITRATE CERTAIN DISPUTES WITH STEEMIT AND LIMITS THE MANNER IN WHICH YOU CAN SEEK RELIEF FROM STEEMIT. +

    24.1. Binding Arbitration

    Except for disputes in which either party seeks to bring an individual action in small claims court or seeks injunctive or other equitable relief for the alleged unlawful use of copyrights, trademarks, trade names, logos, trade secrets or patents, you and Steemit: (a) waive your right to have any and all disputes or Claims arising from these Terms or Steemit (collectively, “Disputes“) resolved in a court; and (b) waive your right to a jury trial. Instead, you and Steemit will arbitrate Disputes through binding arbitration (which is the referral of a Dispute to one or more persons charged with reviewing the Dispute and making a final and binding determination to resolve it, instead of having the Dispute decided by a judge or jury in court). +

    24.2. No Class Arbitrations, Class Actions or Representative Actions

    YOU AND STEEMIT AGREE THAT ANY DISPUTE IS PERSONAL TO YOU AND STEEMIT AND THAT ANY SUCH DISPUTE WILL BE RESOLVED SOLELY THROUGH INDIVIDUAL ARBITRATION AND WILL NOT BE BROUGHT AS A CLASS ARBITRATION, CLASS ACTION OR ANY OTHER TYPE OF REPRESENTATIVE PROCEEDING. NEITHER PARTY AGREES TO CLASS ARBITRATION OR TO AN ARBITRATION IN WHICH AN INDIVIDUAL ATTEMPTS TO RESOLVE A DISPUTE AS A REPRESENTATIVE OF ANOTHER INDIVIDUAL OR GROUP OF INDIVIDUALS. FURTHER, YOU AND STEEMIT AGREE THAT A DISPUTE CANNOT BE BROUGHT AS A CLASS, OR OTHER TYPE OF REPRESENTATIVE ACTION, WHETHER WITHIN OR OUTSIDE OF ARBITRATION, OR ON BEHALF OF ANY OTHER INDIVIDUAL OR GROUP OF INDIVIDUALS. +

    24.3. Federal Arbitration Act

    You and Steemit agree that these Terms affect interstate commerce and that the enforceability of this Section 14 shall be governed by, construed and enforced, both substantively and procedurally, by the Federal Arbitration Act, 9 U.S.C. § + 1 et seq. (the “FAA”) to the maximum extent permitted by applicable law.

    24.4. Process +

    You and Steemit agree that you will notify each other in writing of any Dispute within thirty (30) days of when it arises so that the parties can attempt, in good faith, to resolve the Dispute informally. Notice to Steemit shall be provided by filling out the form available at https://steemit.com/contact/ or sending an email to contact@Steemit.com. Your notice must include: (1) your name, postal address and email address; (2) a description of the nature or basis of the Dispute; and (3) the specific relief that you are seeking. If you and Steemit cannot agree how to resolve the Dispute within thirty (30) days of Steemit receiving the notice, either you or Steemit may, as appropriate pursuant to this Section 14, commence an arbitration proceeding or file a claim in court. You and Steemit agree that any arbitration or claim must be commenced or filed within one (1) year after the Dispute arose; otherwise, you and Steemit agree that the claim is permanently barred (which means that you will no longer have the right to assert a claim regarding the Dispute). You and Steemit agree that: (a) any arbitration will occur in San Francisco County, California; (b) arbitration will be conducted confidentially by a single arbitrator in accordance with the rules of JAMS; and (c) the state or federal courts in California will have exclusive jurisdiction over the enforcement of an arbitration award and over any Dispute between the parties that is not subject to arbitration. You may also litigate a Dispute in small claims court located in the county where you reside if the Dispute meets the requirements to be heard in small claims court. +

    24.5. Authority of Arbitrator

    As limited by the FAA, these Terms and applicable JAMS rules, the arbitrator will have: (a) the exclusive authority and jurisdiction to make all procedural and substantive decisions regarding a Dispute; and (b) the authority to grant any remedy that would otherwise be available in court. The arbitrator may only conduct an individual arbitration and may not consolidate more than one individual’ + s claims, preside over any type of class or representative proceeding or preside over any proceeding involving more than one individual. +

    24.6. Rules of JAMS

    The rules of, and additional information about, JAMS are available on the JAMS website at http://www.jamsadr.com/, as may be updated from time to time. By agreeing to be bound by these Terms, you either: (a) acknowledge and agree that you have read and understand the rules of JAMS; or (b) waive your opportunity to read the rules of JAMS and any claim that the rules of JAMS are unfair or should not apply for any reason. +

    24.7. Severability

    If any term, clause or provision of this Section 14 is held invalid or unenforceable, it will be so held to the minimum extent required by law and all other terms, clauses or provisions will remain valid and enforceable. Further, the waivers set forth in Section 24.2 are severable from the other provisions of these Terms and will remain valid and enforceable, except as prohibited by applicable law. +

    25. Applicable Law and Venue

    These Terms and your access to and use of Steemit and the Steemit Content will be governed by, and construed in accordance with, the laws of California, without resort to its conflict of law provisions. To the extent the arbitration provision in Section 14 does not apply and the Dispute cannot be heard in small claims court, you agree that any action at law or in equity arising out of, or relating to, these Terms shall be filed only in the state and federal courts located in San Francisco County, California and you hereby irrevocably and unconditionally consent and submit to the exclusive jurisdiction of such courts over any suit, action or proceeding arising out of these Terms. +

    26. Termination

    Steemit reserves the right, without notice and in our sole discretion, to terminate your license to access and use Steemit and to block or prevent your future access to, and use of, Steemit. +

    27. Severability

    If any term, clause or provision of these Terms is deemed to be unlawful, void or for any reason unenforceable, then that term, clause or provision shall be deemed severable from these Terms and shall not affect the validity and enforceability of any remaining provisions. +

    28. Questions & Contact Information

    Questions or comments about Steemit may be directed to contact@steemit.com

    +
    +
    +
    +
    + + +
    +
    + ) + } +} + +import {connect} from 'react-redux' +export default connect( + // mapStateToProps +)(TermsAgree) diff --git a/src/app/components/modules/TopRightMenu.jsx b/src/app/components/modules/TopRightMenu.jsx new file mode 100644 index 0000000..96e125c --- /dev/null +++ b/src/app/components/modules/TopRightMenu.jsx @@ -0,0 +1,149 @@ +import React from 'react'; +import { Link } from 'react-router'; +import {connect} from 'react-redux'; +import Icon from 'app/components/elements/Icon'; +import user from 'app/redux/User'; +import Userpic from 'app/components/elements/Userpic'; +import { browserHistory } from 'react-router'; +import { LinkWithDropdown } from 'react-foundation-components/lib/global/dropdown'; +import VerticalMenu from 'app/components/elements/VerticalMenu'; +import LoadingIndicator from 'app/components/elements/LoadingIndicator'; +import NotifiCounter from 'app/components/elements/NotifiCounter'; +import tt from 'counterpart'; + +const defaultNavigate = (e) => { + if (e.metaKey || e.ctrlKey) { + // prevent breaking anchor tags + } else { + e.preventDefault(); + } + const a = e.target.nodeName.toLowerCase() === 'a' ? e.target : e.target.parentNode; + browserHistory.push(a.pathname + a.search + a.hash); +}; + +function TopRightMenu({username, showLogin, logout, loggedIn, vertical, navigate, toggleOffCanvasMenu, probablyLoggedIn, nightmodeEnabled, toggleNightmode}) { + const mcn = 'menu' + (vertical ? ' vertical show-for-small-only' : ''); + const mcl = vertical ? '' : ' sub-menu'; + const lcn = vertical ? '' : 'show-for-medium'; + const nav = navigate || defaultNavigate; + const submit_story = $STM_Config.read_only_mode ? null :
  • {tt('g.submit_a_story')}
  • ; + const submit_icon = $STM_Config.read_only_mode ? null :
  • ; + const feed_link = `/@${username}/feed`; + const replies_link = `/@${username}/recent-replies`; + const wallet_link = `/@${username}/transfers`; + const account_link = `/@${username}`; + const comments_link = `/@${username}/comments`; + const reset_password_link = `/@${username}/password`; + const settings_link = `/@${username}/settings`; + const tt_search = tt('g.search'); + if (loggedIn) { // change back to if(username) after bug fix: Clicking on Login does not cause drop-down to close #TEMP! + const user_menu = [ + {link: feed_link, icon: "home", value: tt('g.feed'), addon: }, + {link: account_link, icon: 'profile', value: tt('g.blog')}, + {link: comments_link, icon: 'replies', value: tt('g.comments')}, + {link: replies_link, icon: 'reply', value: tt('g.replies'), addon: }, + {link: wallet_link, icon: 'wallet', value: tt('g.wallet'), addon: }, + {link: '#', icon: 'eye', onClick: toggleNightmode, value: tt('g.toggle_nightmode') }, + {link: reset_password_link, icon: 'key', value: tt('g.change_password')}, + {link: settings_link, icon: 'cog', value: tt('g.settings')}, + loggedIn ? + {link: '#', icon: 'enter', onClick: logout, value: tt('g.logout')} : + {link: '#', onClick: showLogin, value: tt('g.login')} + ]; + return ( + + ); + } + if (probablyLoggedIn) { + return ( +
      + {!vertical &&
    • } +
    • + {toggleOffCanvasMenu &&
    • + +
    • } +
    + ); + } + return ( + + ); +} + +TopRightMenu.propTypes = { + username: React.PropTypes.string, + loggedIn: React.PropTypes.bool, + probablyLoggedIn: React.PropTypes.bool, + showLogin: React.PropTypes.func.isRequired, + logout: React.PropTypes.func.isRequired, + vertical: React.PropTypes.bool, + navigate: React.PropTypes.func, + toggleOffCanvasMenu: React.PropTypes.func, + nightmodeEnabled: React.PropTypes.bool, + toggleNightmode: React.PropTypes.func, +}; + +export default connect( + state => { + if (!process.env.BROWSER) { + return { + username: null, + loggedIn: false, + probablyLoggedIn: !!state.offchain.get('account') + } + } + const username = state.user.getIn(['current', 'username']); + const loggedIn = !!username; + return { + username, + loggedIn, + probablyLoggedIn: false, + nightmodeEnabled: state.user.getIn(['user_preferences', 'nightmode']), + } + }, + dispatch => ({ + showLogin: e => { + if (e) e.preventDefault(); + dispatch(user.actions.showLogin()) + }, + logout: e => { + if (e) e.preventDefault(); + dispatch(user.actions.logout()) + }, + toggleNightmode: e => { + if (e) e.preventDefault(); + dispatch({ type: 'TOGGLE_NIGHTMODE' }); + }, + }) +)(TopRightMenu); diff --git a/src/app/components/modules/TopRightMenu.scss b/src/app/components/modules/TopRightMenu.scss new file mode 100644 index 0000000..5ed142d --- /dev/null +++ b/src/app/components/modules/TopRightMenu.scss @@ -0,0 +1,79 @@ +.sub-menu { + li { + padding: .7rem 0.5rem; + a { + padding: 0px; + } + } + li:last-child { + padding: 0 0 0 1rem; + } +} + +li.Header__userpic { + padding: 0; + position: relative; + a { + padding: 6px; + } + .Userpic { + width: 36px !important; + height: 36px !important; + } +} + +.TopRightMenu__notificounter { + position: absolute; + top: 4px; + right: 4px; + > .NotifiCounter { + display: block; + } +} + +div.Header__top +{ + li.Header__search { + padding: 0; + &:hover path, &:focus path { + @include themify($themes) { + fill: themed('textColorAccent'); + } + } + &:visited path, &:active path { + @include themify($themes) { + fill: themed('textColorPrimary'); + } + } + > a { + padding: 14px; + } + > a path { + transition: 0.2s all ease-in-out; + @include themify($themes) { + fill: themed('textColorPrimary'); + } + } + } + li.Header__hamburger.toggle-menu { + &.toggle-menu { + padding-left: 0.25rem; + } + > a { + padding: 14px 0 14px 14px; + } + } +} + +li.submit-story { + padding: 0; + > a { + @include themify($themes) { + background: themed('textColorAccent'); + } + padding: 8px 24px; + margin: 0 6px; + color: white; + transition: all 0.3s ease-in-out; + } +} \ No newline at end of file diff --git a/src/app/components/modules/Transfer.jsx b/src/app/components/modules/Transfer.jsx new file mode 100644 index 0000000..686a1e6 --- /dev/null +++ b/src/app/components/modules/Transfer.jsx @@ -0,0 +1,325 @@ +import React, { PropTypes, Component } from 'react'; +import ReactDOM from 'react-dom'; +import reactForm from 'app/utils/ReactForm'; +import {Map} from 'immutable'; +import transaction from 'app/redux/Transaction'; +import user from 'app/redux/User'; +import LoadingIndicator from 'app/components/elements/LoadingIndicator'; +import runTests, {browserTests} from 'app/utils/BrowserTests' +import {validate_account_name, validate_memo_field} from 'app/utils/ChainValidation'; +import {countDecimals} from 'app/utils/ParsersAndFormatters' +import tt from 'counterpart'; +import { APP_NAME, LIQUID_TOKEN, VESTING_TOKEN } from 'app/client_config'; + +/** Warning .. This is used for Power UP too. */ +class TransferForm extends Component { + + static propTypes = { + // redux + currentUser: PropTypes.object.isRequired, + toVesting: PropTypes.bool.isRequired, + currentAccount: PropTypes.object.isRequired, + }; + + constructor(props) { + super(); + const {transferToSelf} = props; + this.state = {advanced: !transferToSelf}; + this.initForm(props) + } + + componentDidMount() { + setTimeout(() => { + const {advanced} = this.state; + if (advanced) + ReactDOM.findDOMNode(this.refs.to).focus(); + else + ReactDOM.findDOMNode(this.refs.amount).focus() + }, 300); + runTests() + } + + onAdvanced = (e) => { + e.preventDefault(); // prevent form submission!! + const username = this.props.currentUser.get('username'); + this.state.to.props.onChange(username); + // setTimeout(() => {ReactDOM.findDOMNode(this.refs.amount).focus()}, 300) + this.setState({advanced: !this.state.advanced}) + }; + + initForm(props) { + const {transferType} = props.initialValues; + const insufficientFunds = (asset, amount) => { + const {currentAccount} = props; + const isWithdraw = transferType && transferType === 'Savings Withdraw'; + const balanceValue = + !asset || asset === 'STEEM' ? + isWithdraw ? currentAccount.get('savings_balance') : currentAccount.get('balance') : + asset === 'SBD' ? + isWithdraw ? currentAccount.get('savings_sbd_balance') : currentAccount.get('sbd_balance') : + null; + if(!balanceValue) return false; + const balance = balanceValue.split(' ')[0]; + return parseFloat(amount) > parseFloat(balance) + }; + const {toVesting} = props; + const fields = toVesting ? ['to', 'amount'] : ['to', 'amount', 'asset']; + if(!toVesting && transferType !== 'Transfer to Savings' && transferType !== 'Savings Withdraw') + fields.push('memo'); + reactForm({ + name: 'transfer', + instance: this, fields, + initialValues: props.initialValues, + validation: values => ({ + to: + ! values.to ? tt('g.required') : validate_account_name(values.to, values.memo), + amount: + ! values.amount ? 'Required' : + ! /^\d+(\.\d+)?$/.test(values.amount) ? tt('transfer_jsx.amount_is_in_form') : + insufficientFunds(values.asset, values.amount) ? tt('transfer_jsx.insufficient_funds') : + countDecimals(values.amount) > 3 ? tt('transfer_jsx.use_only_3_digits_of_precison') : + null, + asset: + props.toVesting ? null : + ! values.asset ? tt('g.required') : null, + memo: + values.memo ? validate_memo_field(values.memo, props.currentUser.get('username'), props.currentAccount.get('memo_key')): + values.memo && (!browserTests.memo_encryption && /^#/.test(values.memo)) ? + 'Encrypted memos are temporarily unavailable (issue #98)' : + null, + }) + }) + } + + clearError = () => {this.setState({ trxError: undefined })}; + + errorCallback = estr => { this.setState({ trxError: estr, loading: false }) }; + + balanceValue() { + const {transferType} = this.props.initialValues; + const {currentAccount} = this.props; + const {asset} = this.state; + const isWithdraw = transferType && transferType === 'Savings Withdraw'; + return !asset || + asset.value === 'STEEM' ? + isWithdraw ? currentAccount.get('savings_balance') : currentAccount.get('balance') : + asset.value === 'SBD' ? + isWithdraw ? currentAccount.get('savings_sbd_balance') : currentAccount.get('sbd_balance') : + null + } + + assetBalanceClick = e => { + e.preventDefault(); + // Convert '9.999 STEEM' to 9.999 + this.state.amount.props.onChange(this.balanceValue().split(' ')[0]) + }; + + onChangeTo = (e) => { + const {value} = e.target; + this.state.to.props.onChange(value.toLowerCase().trim()) + }; + + render() { + const transferTips = { + 'Transfer to Account': tt('transfer_jsx.move_funds_to_another_account', {APP_NAME}), + 'Transfer to Savings': tt('transfer_jsx.protect_funds_by_requiring_a_3_day_withdraw_waiting_period'), + 'Savings Withdraw': tt('transfer_jsx.withdraw_funds_after_the_required_3_day_waiting_period'), + }; + const powerTip3 = tt('tips_js.converted_VESTING_TOKEN_can_be_sent_to_yourself_but_can_not_transfer_again', {LIQUID_TOKEN, VESTING_TOKEN}) + const {to, amount, asset, memo} = this.state; + const {loading, trxError, advanced} = this.state; + const {currentUser, toVesting, transferToSelf, dispatchSubmit} = this.props; + const {transferType} = this.props.initialValues; + const {submitting, valid, handleSubmit} = this.state.transfer; + // const isMemoPrivate = memo && /^#/.test(memo.value); -- private memos are not supported yet + const isMemoPrivate = false; + const form = ( +
    { + this.setState({loading: true}); + dispatchSubmit({...data, errorCallback: this.errorCallback, currentUser, toVesting, transferType}) + })} + onChange={this.clearError} + > + {toVesting &&
    +
    +

    {tt('tips_js.influence_token')}

    +

    {tt('tips_js.non_transferable', {LIQUID_TOKEN, VESTING_TOKEN})}

    +
    +
    } + + {!toVesting &&
    +
    +
    + {transferTips[transferType]} +
    +
    +
    +
    } + +
    +
    {tt('transfer_jsx.from')}
    +
    +
    + @ + +
    +
    +
    + + {advanced &&
    +
    {tt('transfer_jsx.to')}
    +
    +
    + @ + +
    + {to.touched && to.blur && to.error ? +
    {to.error} 
    : +

    {toVesting && powerTip3}

    + } +
    +
    } + +
    +
    {tt('g.amount')}
    +
    +
    + + {asset && + + } +
    +
    + +
    + {(asset && asset.touched && asset.error ) || (amount.touched && amount.error) ? +
    + {asset && asset.touched && asset.error && asset.error}  + {amount.touched && amount.error && amount.error}  +
    : null} +
    +
    + + {memo &&
    +
    {tt('g.memo')}
    +
    + {isMemoPrivate ? tt('transfer_jsx.this_memo_is_private') : tt('transfer_jsx.this_memo_is_public')} + +
    {memo.touched && memo.error && memo.error} 
    +
    +
    } +
    +
    + {loading &&
    } + {!loading && + {trxError &&
    {trxError}
    } + + {transferToSelf && } +
    } +
    +
    +
    + ); + return ( +
    +
    +

    {toVesting ? tt('transfer_jsx.convert_to_VESTING_TOKEN', {VESTING_TOKEN}) : transferType}

    +
    + {form} +
    + ) + } +} + +const AssetBalance = ({onClick, balanceValue}) => + {tt('g.balance') + ": " + balanceValue}; + +import {connect} from 'react-redux' + +export default connect( + // mapStateToProps + (state, ownProps) => { + const initialValues = state.user.get('transfer_defaults', Map()).toJS(); + const toVesting = initialValues.asset === 'VESTS'; + const currentUser = state.user.getIn(['current']); + const currentAccount = state.global.getIn(['accounts', currentUser.get('username')]); + + if(!toVesting && !initialValues.transferType) + initialValues.transferType = 'Transfer to Account'; + + let transferToSelf = toVesting || /Transfer to Savings|Savings Withdraw/.test(initialValues.transferType); + if (transferToSelf && !initialValues.to) + initialValues.to = currentUser.get('username'); + + if(initialValues.to !== currentUser.get('username')) + transferToSelf = false // don't hide the to field + + return {...ownProps, currentUser, currentAccount, toVesting, transferToSelf, initialValues} + }, + + // mapDispatchToProps + dispatch => ({ + dispatchSubmit: ({ + to, amount, asset, memo, transferType, + toVesting, currentUser, errorCallback + }) => { + if(!toVesting && !/Transfer to Account|Transfer to Savings|Savings Withdraw/.test(transferType)) + throw new Error(`Invalid transfer params: toVesting ${toVesting}, transferType ${transferType}`); + + const username = currentUser.get('username'); + const successCallback = () => { + // refresh transfer history + dispatch({type: 'global/GET_STATE', payload: {url: `@${username}/transfers`}}); + if(/Savings Withdraw/.test(transferType)) { + dispatch({type: 'user/LOAD_SAVINGS_WITHDRAW', payload: {}}) + } + dispatch(user.actions.hideTransfer()) + }; + const asset2 = toVesting ? 'STEEM' : asset; + const operation = { + from: username, + to, amount: parseFloat(amount, 10).toFixed(3) + ' ' + asset2, + memo: toVesting ? undefined : (memo ? memo : '') + } + + if(transferType === 'Savings Withdraw') + operation.request_id = Math.floor((Date.now() / 1000) % 4294967295); + + dispatch(transaction.actions.broadcastOperation({ + type: toVesting ? 'transfer_to_vesting' : ( + transferType === 'Transfer to Account' ? 'transfer' : + transferType === 'Transfer to Savings' ? 'transfer_to_savings' : + transferType === 'Savings Withdraw' ? 'transfer_from_savings' : + null + ), + operation, + successCallback, + errorCallback + })) + } + }) +)(TransferForm) diff --git a/src/app/components/modules/UserWallet.jsx b/src/app/components/modules/UserWallet.jsx new file mode 100644 index 0000000..76c4c54 --- /dev/null +++ b/src/app/components/modules/UserWallet.jsx @@ -0,0 +1,452 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; +import {connect} from 'react-redux' +import {Link} from 'react-router' +import g from 'app/redux/GlobalReducer' +import SavingsWithdrawHistory from 'app/components/elements/SavingsWithdrawHistory'; +import TransferHistoryRow from 'app/components/cards/TransferHistoryRow'; +import TransactionError from 'app/components/elements/TransactionError'; +import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper'; +import {numberWithCommas, vestingSteem, delegatedSteem} from 'app/utils/StateFunctions' +import FoundationDropdownMenu from 'app/components/elements/FoundationDropdownMenu' +import WalletSubMenu from 'app/components/elements/WalletSubMenu' +import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'; +import Tooltip from 'app/components/elements/Tooltip' +import {FormattedHTMLMessage} from 'app/Translator'; +import tt from 'counterpart'; +import {List} from 'immutable' +import { LIQUID_TOKEN, LIQUID_TICKER, DEBT_TOKENS, VESTING_TOKEN } from 'app/client_config'; +import transaction from 'app/redux/Transaction'; + +const assetPrecision = 1000; + +class UserWallet extends React.Component { + constructor() { + super(); + this.state = { + claimInProgress: false, + }; + this.onShowDepositSteem = (e) => { + if (e && e.preventDefault) e.preventDefault(); + const name = this.props.current_user.get('username'); + const new_window = window.open(); + new_window.opener = null; + new_window.location = 'https://blocktrades.us/?input_coin_type=btc&output_coin_type=steem&receive_address=' + name; + }; + this.onShowWithdrawSteem = (e) => { + e.preventDefault(); + const new_window = window.open(); + new_window.opener = null; + new_window.location = 'https://blocktrades.us/unregistered_trade/steem/btc'; + }; + this.onShowDepositPower = (current_user_name, e) => { + e.preventDefault(); + const new_window = window.open(); + new_window.opener = null; + new_window.location = 'https://blocktrades.us/?input_coin_type=btc&output_coin_type=steem_power&receive_address=' + current_user_name; + }; + this.onShowDepositSBD = (current_user_name, e) => { + e.preventDefault(); + const new_window = window.open(); + new_window.opener = null; + new_window.location = 'https://blocktrades.us/?input_coin_type=btc&output_coin_type=sbd&receive_address=' + current_user_name; + }; + this.onShowWithdrawSBD = (e) => { + e.preventDefault(); + const new_window = window.open(); + new_window.opener = null; + new_window.location = 'https://blocktrades.us/unregistered_trade/sbd/btc'; + }; + this.shouldComponentUpdate = shouldComponentUpdate(this, 'UserWallet'); + } + + handleClaimRewards = (account) => { + this.setState({ claimInProgress: true }); // disable the claim button + this.props.claimRewards(account); + } + + render() { + const {onShowDepositSteem, onShowWithdrawSteem, + onShowDepositSBD, onShowWithdrawSBD, onShowDepositPower} = this; + const {convertToSteem, price_per_steem, savings_withdraws, account, + current_user, open_orders} = this.props; + const gprops = this.props.gprops.toJS(); + + if (!account) return null; + let vesting_steem = vestingSteem(account.toJS(), gprops); + let delegated_steem = delegatedSteem(account.toJS(), gprops); + + let isMyAccount = current_user && current_user.get('username') === account.get('name'); + + const disabledWarning = false; + // isMyAccount = false; // false to hide wallet transactions + + const showTransfer = (asset, transferType, e) => { + e.preventDefault(); + this.props.showTransfer({ + to: (isMyAccount ? null : account.get('name')), + asset, transferType + }); + }; + + const savings_balance = account.get('savings_balance'); + const savings_sbd_balance = account.get('savings_sbd_balance'); + + const powerDown = (cancel, e) => { + e.preventDefault() + const name = account.get('name'); + if (cancel) { + const vesting_shares = cancel ? '0.000000 VESTS' : account.get('vesting_shares'); + this.setState({toggleDivestError: null}); + const errorCallback = e2 => {this.setState({toggleDivestError: e2.toString()})}; + const successCallback = () => {this.setState({toggleDivestError: null})} + this.props.withdrawVesting({account: name, vesting_shares, errorCallback, successCallback}) + } else { + const to_withdraw = account.get('to_withdraw') + const withdrawn = account.get('withdrawn') + const vesting_shares = account.get('vesting_shares') + const delegated_vesting_shares = account.get('delegated_vesting_shares') + this.props.showPowerdown({ + account: name, + to_withdraw, withdrawn, + vesting_shares, delegated_vesting_shares, + }); + } + } + + // Sum savings withrawals + let savings_pending = 0, savings_sbd_pending = 0; + if(savings_withdraws) { + savings_withdraws.forEach(withdraw => { + const [amount, asset] = withdraw.get('amount').split(' '); + if(asset === 'STEEM') + savings_pending += parseFloat(amount); + else { + if(asset === 'SBD') + savings_sbd_pending += parseFloat(amount) + } + }) + } + + // Sum conversions + let conversionValue = 0; + const currentTime = (new Date()).getTime(); + const conversions = account.get('other_history', List()).reduce( (out, item) => { + if(item.getIn([1, 'op', 0], "") !== 'convert') return out; + + const timestamp = new Date(item.getIn([1, 'timestamp'])).getTime(); + const finishTime = timestamp + (86400000 * 3.5); // add 3.5day conversion delay + if(finishTime < currentTime) return out; + + const amount = parseFloat(item.getIn([1, 'op', 1, 'amount']).replace(" SBD", "")); + conversionValue += amount; + + return out.concat([ +
    + + (+{tt('userwallet_jsx.in_conversion', {amount: numberWithCommas('$' + amount.toFixed(3))})}) + +
    + ]); + }, []); + + const balance_steem = parseFloat(account.get('balance').split(' ')[0]); + const saving_balance_steem = parseFloat(savings_balance.split(' ')[0]); + const divesting = parseFloat(account.get('vesting_withdraw_rate').split(' ')[0]) > 0.000000; + const sbd_balance = parseFloat(account.get('sbd_balance')) + const sbd_balance_savings = parseFloat(savings_sbd_balance.split(' ')[0]); + const sbdOrders = (!open_orders || !isMyAccount) ? 0 : open_orders.reduce((o, order) => { + if (order.sell_price.base.indexOf("SBD") !== -1) { + o += order.for_sale; + } + return o; + }, 0) / assetPrecision; + + const steemOrders = (!open_orders || !isMyAccount) ? 0 : open_orders.reduce((o, order) => { + if (order.sell_price.base.indexOf("STEEM") !== -1) { + o += order.for_sale; + } + return o; + }, 0) / assetPrecision; + + // set displayed estimated value + const total_sbd = sbd_balance + sbd_balance_savings + savings_sbd_pending + sbdOrders + conversionValue; + const total_steem = vesting_steem + balance_steem + saving_balance_steem + savings_pending + steemOrders; + let total_value = '$' + numberWithCommas( + ((total_steem * price_per_steem) + total_sbd + ).toFixed(2)) + + // format spacing on estimated value based on account state + let estimate_output =

    {total_value}

    ; + if (isMyAccount) { + estimate_output =

    {total_value}     

    ; + } + + /// transfer log + let idx = 0 + const transfer_log = account.get('transfer_history') + .map(item => { + const data = item.getIn([1, 'op', 1]); + const type = item.getIn([1, 'op', 0]); + + // Filter out rewards + if (type === "curation_reward" || type === "author_reward" || type === "comment_benefactor_reward") { + return null; + } + + if(data.sbd_payout === '0.000 SBD' && data.vesting_payout === '0.000000 VESTS') + return null + return ; + }).filter(el => !!el).reverse(); + + let steem_menu = [ + { value: tt('g.transfer'), link: '#', onClick: showTransfer.bind( this, 'STEEM', 'Transfer to Account' ) }, + { value: tt('userwallet_jsx.transfer_to_savings'), link: '#', onClick: showTransfer.bind( this, 'STEEM', 'Transfer to Savings' ) }, + { value: tt('userwallet_jsx.power_up'), link: '#', onClick: showTransfer.bind( this, 'VESTS', 'Transfer to Account' ) }, + ] + let power_menu = [ + { value: tt('userwallet_jsx.power_down'), link: '#', onClick: powerDown.bind(this, false) } + ] + let dollar_menu = [ + { value: tt('g.transfer'), link: '#', onClick: showTransfer.bind( this, 'SBD', 'Transfer to Account' ) }, + { value: tt('userwallet_jsx.transfer_to_savings'), link: '#', onClick: showTransfer.bind( this, 'SBD', 'Transfer to Savings' ) }, + { value: tt('userwallet_jsx.market'), link: '/market' }, + { value: tt('userwallet_jsx.convert_to_LIQUID_TOKEN', {LIQUID_TOKEN}), link: '#', onClick: convertToSteem }, + ] + if(isMyAccount) { + steem_menu.push({ value: tt('g.buy'), link: '#', onClick: onShowDepositSteem.bind(this, current_user.get('username')) }); + steem_menu.push({ value: tt('g.sell'), link: '#', onClick: onShowWithdrawSteem }); + steem_menu.push({ value: tt('userwallet_jsx.market'), link: '/market' }); + power_menu.push({ value: tt('g.buy'), link: '#', onClick: onShowDepositPower.bind(this, current_user.get('username')) }) + dollar_menu.push({ value: tt('g.buy'), link: '#', onClick: onShowDepositSBD.bind(this, current_user.get('username')) }); + dollar_menu.push({ value: tt('g.sell'), link: '#', onClick: onShowWithdrawSBD }); + } + if( divesting ) { + power_menu.push( { value: 'Cancel Power Down', link:'#', onClick: powerDown.bind(this, true) } ); + } + + const isWithdrawScheduled = new Date(account.get('next_vesting_withdrawal') + 'Z').getTime() > Date.now() + + const steem_balance_str = numberWithCommas(balance_steem.toFixed(3)); + const steem_orders_balance_str = numberWithCommas(steemOrders.toFixed(3)); + const power_balance_str = numberWithCommas(vesting_steem.toFixed(3)); + const received_power_balance_str = (delegated_steem < 0 ? '+' : '') + numberWithCommas((-delegated_steem).toFixed(3)); + const sbd_balance_str = numberWithCommas('$' + sbd_balance.toFixed(3)); // formatDecimal(account.sbd_balance, 3) + const sbd_orders_balance_str = numberWithCommas('$' + sbdOrders.toFixed(3)); + const savings_balance_str = numberWithCommas(saving_balance_steem.toFixed(3) + ' STEEM'); + const savings_sbd_balance_str = numberWithCommas('$' + sbd_balance_savings.toFixed(3)); + + const savings_menu = [ + { value: tt('userwallet_jsx.withdraw_LIQUID_TOKEN', {LIQUID_TOKEN}), link: '#', onClick: showTransfer.bind( this, 'STEEM', 'Savings Withdraw' ) }, + ]; + const savings_sbd_menu = [ + { value: tt('userwallet_jsx.withdraw_DEBT_TOKENS', {DEBT_TOKENS}), link: '#', onClick: showTransfer.bind( this, 'SBD', 'Savings Withdraw' ) }, + ]; + // set dynamic secondary wallet values + const sbdInterest = this.props.sbd_interest / 100; + const sbdMessage = {tt('userwallet_jsx.tokens_worth_about_1_of_LIQUID_TICKER', {LIQUID_TICKER, sbdInterest})} + + const reward_steem = parseFloat(account.get('reward_steem_balance').split(' ')[0]) > 0 ? account.get('reward_steem_balance') : null; + const reward_sbd = parseFloat(account.get('reward_sbd_balance').split(' ')[0]) > 0 ? account.get('reward_sbd_balance') : null; + const reward_sp = parseFloat(account.get('reward_vesting_steem').split(' ')[0]) > 0 ? account.get('reward_vesting_steem').replace('STEEM', 'SP') : null; + + let rewards = []; + if(reward_steem) rewards.push(reward_steem); + if(reward_sbd) rewards.push(reward_sbd); + if(reward_sp) rewards.push(reward_sp); + + let rewards_str; + switch(rewards.length) { + case 3: + rewards_str = `${rewards[0]}, ${rewards[1]} and ${rewards[2]}`; + break; + case 2: + rewards_str = `${rewards[0]} and ${rewards[1]}`; + break; + case 1: + rewards_str = `${rewards[0]}`; + break; + } + + let claimbox; + if(current_user && rewards_str && isMyAccount) { + claimbox =
    +
    +
    + Your current rewards: {rewards_str} + +
    +
    +
    + } + + return (
    + {claimbox} +
    +
    + {isMyAccount ? :

    {tt('g.balances')}


    } +
    + {
    + {isMyAccount && } +
    } +
    +
    +
    + STEEM + +
    +
    + {isMyAccount ? + + : steem_balance_str + ' STEEM'} + {steemOrders ?
    (+{steem_orders_balance_str} STEEM)
    : null} +
    +
    +
    +
    + STEEM POWER + + {delegated_steem != 0 ? {tt('tips_js.part_of_your_steem_power_is_currently_delegated')} : null} +
    +
    + {isMyAccount ? + + : power_balance_str + ' STEEM'} + {delegated_steem != 0 ?
    ({received_power_balance_str} STEEM)
    : null} +
    +
    +
    +
    + STEEM DOLLARS +
    {sbdMessage}
    +
    +
    + {isMyAccount ? + + : sbd_balance_str} + {sbdOrders ?
    (+{sbd_orders_balance_str})
    : null} + {conversions} +
    +
    +
    +
    + {tt('userwallet_jsx.savings')} +
    + {tt('transfer_jsx.balance_subject_to_3_day_withdraw_waiting_period')} + {tt('transfer_jsx.asset_currently_collecting', {asset: DEBT_TOKENS, interest: sbdInterest})} +
    +
    +
    + {isMyAccount ? + + : savings_balance_str} +
    + {isMyAccount ? + + : savings_sbd_balance_str} +
    +
    +
    +
    + {tt('userwallet_jsx.estimated_account_value')} +
    {tt('tips_js.estimated_value', {LIQUID_TOKEN})}
    +
    +
    + {estimate_output} +
    +
    +
    +
    + {isWithdrawScheduled && {tt('userwallet_jsx.next_power_down_is_scheduled_to_happen')}  . } + {/*toggleDivestError &&
    {toggleDivestError}
    */} + +
    +
    + {disabledWarning &&
    +
    +
    + {tt('userwallet_jsx.transfers_are_temporary_disabled')} +
    +
    +
    } +
    +
    +
    +
    +
    + + {isMyAccount && } + +
    +
    + {/** history */} +

    {tt('userwallet_jsx.history')}

    +
    + {tt('transfer_jsx.beware_of_spam_and_phishing_links')} +
    + + + {transfer_log} + +
    +
    +
    +
    ); + } +} + +export default connect( + // mapStateToProps + (state, ownProps) => { + let price_per_steem = undefined + const feed_price = state.global.get('feed_price') + if(feed_price && feed_price.has('base') && feed_price.has('quote')) { + const {base, quote} = feed_price.toJS() + if(/ SBD$/.test(base) && / STEEM$/.test(quote)) + price_per_steem = parseFloat(base.split(' ')[0]) + } + const savings_withdraws = state.user.get('savings_withdraws') + const gprops = state.global.get('props'); + const sbd_interest = gprops.get('sbd_interest_rate') + return { + ...ownProps, + open_orders: state.market.get('open_orders'), + price_per_steem, + savings_withdraws, + sbd_interest, + gprops + } + }, + // mapDispatchToProps + dispatch => ({ + claimRewards: (account) => { + const username = account.get('name') + const successCallback = () => { + dispatch({type: 'global/GET_STATE', payload: {url: `@${username}/transfers`}}) + }; + + const operation = { + account: username, + reward_steem: account.get('reward_steem_balance'), + reward_sbd: account.get('reward_sbd_balance'), + reward_vests: account.get('reward_vesting_balance') + }; + + dispatch(transaction.actions.broadcastOperation({ + type: 'claim_reward_balance', + operation, + successCallback, + })) + }, + convertToSteem: (e) => { + e.preventDefault() + const name = 'convertToSteem'; + dispatch(g.actions.showDialog({name})) + }, + showChangePassword: (username) => { + const name = 'changePassword'; + dispatch(g.actions.remove({key: name})); + dispatch(g.actions.showDialog({name, params: {username}})) + }, + }) +)(UserWallet) diff --git a/src/app/components/modules/UserWallet.scss b/src/app/components/modules/UserWallet.scss new file mode 100644 index 0000000..f21bf51 --- /dev/null +++ b/src/app/components/modules/UserWallet.scss @@ -0,0 +1,86 @@ + +.UserWallet__buysp { + @extend .e-btn-hollow; +} + +.UserWallet__balance { + padding-bottom: 1rem; + padding-top: 1rem; + + &.UserWallet__balance.zebra { + @include themify($themes) { + background-color: themed('tableRowEvenBackgroundColor'); + color: themed('textColorPrimary'); + } + } +} + +.UserWallet__buysp { + margin-right: 0 !important; + margin-top: 1rem; +} + +.UserWallet__claimbox { + margin: 1.5rem 0 0.5rem; + padding: 1rem 1rem 1.1rem 1rem; + border-radius: 5px; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + + @include themify($themes) { + border: themed('border'); + background-color: themed('highlightBackgroundColor'); + color: themed('textColorPrimary'); + } + + &-text { + font-weight: bold; + } + .button { + @extend .e-btn; + @include font-size(14px); + margin: 0; + } + // spacing for mobile collapse + @media only screen and (max-width: 680px) { + .button { + margin: 8px 0 0 0; + } + } +} + +.WalletSubMenu { + > li > a { + transition: 0.2s all ease-in-out; + @include themify($themes) { + color: themed('textColorSecondary'); + } + + &:hover, &:focus { + @include themify($themes) { + color: themed('textColorAccent'); + } + } + } + > li > a.active { + cursor: default; + @include themify($themes) { + color: themed('textColorPrimary'); + } + font-weight: bold; + } + @include themify($themes) { + border-bottom: themed('border'); + } + + margin: 0.5rem 0 1rem; +} + +// protect for foundation bug for collapsed state drop-downs +@media only screen and (max-width: 40em) { + .Wallet_dropdown { + left: 10px!important; + } +} diff --git a/src/app/components/modules/lp/LpFooter.jsx b/src/app/components/modules/lp/LpFooter.jsx new file mode 100644 index 0000000..6d9f87d --- /dev/null +++ b/src/app/components/modules/lp/LpFooter.jsx @@ -0,0 +1,12 @@ +import React, {PropTypes} from 'react'; +import { Link } from 'react-router'; +import {connect} from 'react-redux'; + +export default class LpFooter extends React.Component { + render() { + return ( +
    +
    + ); + } +} diff --git a/src/app/components/modules/lp/LpFooter.scss b/src/app/components/modules/lp/LpFooter.scss new file mode 100644 index 0000000..8c7ee4c --- /dev/null +++ b/src/app/components/modules/lp/LpFooter.scss @@ -0,0 +1,4 @@ +.LpFooter { + height: 183px; + background-image: url(../images/lp-bottom.jpg); +} diff --git a/src/app/components/modules/lp/LpHeader.jsx b/src/app/components/modules/lp/LpHeader.jsx new file mode 100644 index 0000000..9f1a5de --- /dev/null +++ b/src/app/components/modules/lp/LpHeader.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Link } from 'react-router'; +import tt from 'counterpart'; + +export default class LpHeader extends React.Component { + render() { + return ( +
    +
    +
    +
    +
    +
      +
    • {tt('g.browse')}
    • +
    +
    +
    +
    + ); + } +} diff --git a/src/app/components/modules/lp/LpHeader.scss b/src/app/components/modules/lp/LpHeader.scss new file mode 100644 index 0000000..8b2e136 --- /dev/null +++ b/src/app/components/modules/lp/LpHeader.scss @@ -0,0 +1,8 @@ +.LpHeader { + .top-bar { + background-color: #fafbfc; + } + ul > li > a { + background-color: #fafbfc; + } +} diff --git a/src/app/components/pages/About.jsx b/src/app/components/pages/About.jsx new file mode 100644 index 0000000..3c693d7 --- /dev/null +++ b/src/app/components/pages/About.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { APP_NAME, APP_URL } from 'app/client_config'; +import tt from 'counterpart'; + +class About extends React.Component { + render() { + return ( +
    +
    + +

    {tt('about_jsx.about_app', {APP_NAME})}

    +

    + {tt('about_jsx.about_app_details')} + {tt('about_jsx.learn_more_at_app_url', {APP_URL})}. +

    +

    {tt('about_jsx.resources')}

    +

    {tt('navigation.APP_NAME_whitepaper', {APP_NAME})} [PDF]

    +
    +
    + ); + } +} + +module.exports = { + path: 'about.html', + component: About +}; diff --git a/src/app/components/pages/Approval.jsx b/src/app/components/pages/Approval.jsx new file mode 100644 index 0000000..c4682d8 --- /dev/null +++ b/src/app/components/pages/Approval.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import {connect} from 'react-redux'; + +class Approval extends React.Component { + constructor(props) { + super(props); + this.state = { + confirm_email: false + } + } + + componentWillMount() { + if (this.props.location.query.confirm_email) { + this.setState({confirm_email: true}); + } + } + + render() { + let body = ''; + if (this.state.confirm_email) { + body =
    +

    Thanks for confirming your email!

    +

    After validating your sign up request with us we'll look it over for approval. As soon as your turn is up and you're approved, you'll be sent a link to finalize your account!

    +

    You'll be among the earliest members of the Steemit community!

    +
    + } else { + body =
    +

    Thanks for confirming your phone number!

    +

    You're a few steps away from getting to the top of the list. Check your email and click the email validation link.

    +

    After validating your sign up request with us we'll look it over for approval. As soon as your turn is up and you're approved, you'll be sent a link to finalize your account!

    +

    You'll be among the earliest members of the Steemit community!

    +
    + } + return ( +
    +
    +
    + {body} +
    +
    +
    + ); + } +} + +module.exports = { + path: 'approval', + component: connect( + state => { + return { + + } + }, + dispatch => ({ + }) + )(Approval) +}; diff --git a/src/app/components/pages/ChangePasswordPage.jsx b/src/app/components/pages/ChangePasswordPage.jsx new file mode 100644 index 0000000..f710744 --- /dev/null +++ b/src/app/components/pages/ChangePasswordPage.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import ChangePassword from 'app/components/elements/ChangePassword'; +import tt from 'counterpart'; + +class ChangePasswordPage extends React.Component { + + render() { + if (!process.env.BROWSER) { // don't render this page on the server + return
    +
    + {tt('g.loading')}.. +
    +
    ; + } + + return ( +
    +
    +

    {tt('g.change_password')}

    + +
    +
    + ); + } +} + +module.exports = { + path: 'change_password', + component: ChangePasswordPage +}; diff --git a/src/app/components/pages/CreateAccount.jsx b/src/app/components/pages/CreateAccount.jsx new file mode 100644 index 0000000..75503ef --- /dev/null +++ b/src/app/components/pages/CreateAccount.jsx @@ -0,0 +1,335 @@ +/* eslint react/prop-types: 0 */ +/*global $STM_csrf, $STM_Config */ +import React from 'react'; +import {connect} from 'react-redux'; +import LoadingIndicator from 'app/components/elements/LoadingIndicator'; +import user from 'app/redux/User'; +import {PrivateKey} from 'steem/lib/auth/ecc'; +import {validate_account_name} from 'app/utils/ChainValidation'; +import runTests from 'app/utils/BrowserTests'; +import GeneratedPasswordInput from 'app/components/elements/GeneratedPasswordInput'; +import {saveCords} from 'app/utils/ServerApiClient'; +import {api} from 'steem'; +import { Link } from 'react-router'; + +class CreateAccount extends React.Component { + + static propTypes = { + loginUser: React.PropTypes.func.isRequired, + serverBusy: React.PropTypes.bool + }; + + constructor(props) { + super(props); + this.state = { + name: '', + password: '', + password_valid: '', + name_error: '', + server_error: '', + loading: false, + cryptographyFailure: false, + showRules: false, + showPass: false, + account_has_keys_warning: false, // remove this after May 20th + // user_name_picked: this.props.offchainUser.getIn(["name"]) + }; + this.onSubmit = this.onSubmit.bind(this); + this.onNameChange = this.onNameChange.bind(this); + this.onPasswordChange = this.onPasswordChange.bind(this); + this.preventDoubleClick = this.preventDoubleClick.bind(this); + } + + componentDidMount() { + const newState = {showPass: true}; + const cryptoTestResult = runTests(); + if (cryptoTestResult !== undefined) { + console.error('CreateAccount - cryptoTestResult: ', cryptoTestResult); + newState.cryptographyFailure = true; + } else { + newState.showPass = true; + } + // let's find out if there is pre-approved not created account name + const offchainAccount = this.props.offchainUser ? this.props.offchainUser.get('account') : null; + if (offchainAccount) { + newState.name = offchainAccount; + this.validateAccountName(offchainAccount); + // remove below after May 20th + if (this.props.offchainUser.get('account_has_keys')) { + newState.account_has_keys_warning = true; + } + } + this.props.showTerms(); + this.setState(newState); + } + + mousePosition(e) { + // log x/y cords + console.log("--> mouse position --", e.type, e.screenX, e.screenY); + if(e.type === 'click') { + saveCords(e.screenX, e.screenY); + } + } + + onSubmit(e) { + e.preventDefault(); + this.setState({server_error: '', loading: true}); + const {password, password_valid} = this.state; + const name = this.state.name; + if (!password || !password_valid) return; + + let public_keys; + try { + const pk = PrivateKey.fromWif(password); + public_keys = [1, 2, 3, 4].map(() => pk.toPublicKey().toString()); + } catch (error) { + public_keys = ['owner', 'active', 'posting', 'memo'].map(role => { + const pk = PrivateKey.fromSeed(`${name}${role}${password}`); + return pk.toPublicKey().toString(); + }); + } + + // createAccount + fetch('/api/v1/accounts', { + method: 'post', + mode: 'no-cors', + credentials: 'same-origin', + headers: { + Accept: 'application/json', + 'Content-type': 'application/json' + }, + body: JSON.stringify({ + csrf: $STM_csrf, + name, + owner_key: public_keys[0], + active_key: public_keys[1], + posting_key: public_keys[2], + memo_key: public_keys[3] + }) + }).then(r => r.json()).then(res => { + if (res.error || res.status !== 'ok') { + console.error('CreateAccount server error', res.error); + this.setState({server_error: res.error || 'Unknown server error', loading: false}); + } else { + window.location = `/login.html#account=${name}&msg=accountcreated`; + } + }).catch(error => { + console.error('Caught CreateAccount server error', error); + this.setState({server_error: (error.message ? error.message : error), loading: false}); + }); + } + + onPasswordChange(password, password_valid) { + this.setState({password, password_valid}); + } + + preventDoubleClick() { + // return false; + } + + onNameChange(e) { + const name = e.target.value.trim().toLowerCase(); + this.validateAccountName(name); + this.setState({name}); + } + + validateAccountName(name) { + let name_error = ''; + let promise; + if (name.length > 0) { + name_error = validate_account_name(name); + if (!name_error) { + this.setState({name_error: ''}); + promise = api.getAccountsAsync([name]).then(res => { + return res && res.length > 0 ? 'Account name is not available' : ''; + }); + } + } + if (promise) { + promise + .then(error => this.setState({name_error: error})) + .catch(() => this.setState({ + name_error: "Account name can't be verified right now due to server failure. Please try again later." + })); + } else { + this.setState({name_error}); + } + } + + render() { + if (!process.env.BROWSER) { // don't render this page on the server - it will not work until rendered in browser + return
    +
    +

    LOADING..

    +
    +
    ; + } + + const { + name, password_valid, //showPasswordString, + name_error, server_error, loading, cryptographyFailure, showRules + } = this.state; + + const {loggedIn, logout, serverBusy} = this.props; + const submit_btn_disabled = loading || !password_valid; + const submit_btn_class = 'button action' + (submit_btn_disabled ? ' disabled' : ''); + + const account_status = this.props.offchainUser ? this.props.offchainUser.get('account_status') : null; + + if (serverBusy || $STM_Config.disable_signups) { + return
    +
    +
    +
    +

    Membership to Steemit.com is now under invitation only because of unexpectedly high sign up rate.

    +
    +
    +
    ; + } + if (cryptographyFailure) { + return
    +
    +
    +
    +

    Cryptography test failed

    +

    We will be unable to create your Steem account with this browser.

    +

    The latest versions of Chrome and Firefox + are well tested and known to work with steemit.com.

    +
    +
    +
    ; + } + + if (loggedIn) { + return
    +
    +
    +
    +

    You need to Logout before you can create another account.

    +

    Please note that Steemit can only register one account per verified user.

    +
    +
    +
    ; + } + + if (account_status !== 'approved') { + return
    +
    +
    +
    +

    It looks like your sign up request is not approved yet or you already created an account.
    + Please try again later or contact support@steemit.com for the status of your request.
    + If you didn't submit your sign up application yet, apply now! +

    +
    +
    +
    ; + } + + let next_step = null; + if (server_error) { + if (server_error === 'Email address is not confirmed') { + next_step = ; + } else if (server_error === 'Phone number is not confirmed') { + next_step = ; + } else { + next_step =
    +
    Couldn't create account. Server returned the following error:
    +

    {server_error}

    +
    ; + } + } + + return ( +
    +
    +
    +

    Please read the Steemit Rules and fill in the form below to create your Steemit account

    + {/**/} + {showRules ?
    +

    + The first rule of Steemit is: Do not lose your password.
    + The second rule of Steemit is: Do not lose your password.
    + The third rule of Steemit is: We cannot recover your password, or your account if you lose your password.
    + The forth rule: Do not tell anyone your password.
    + The fifth rule: Always back up your password. +
    +
    + Seriously, we are, for technical reasons, entirely unable to gain + access to an account without knowing the password. Steemit is a + new model, entirely unlike other sites on the Internet. It's not + simply policy: We cannot recover your account or password + if you lose it. +
    + Print out your password or write it down in a safe place. +

    + + +
    +
    : } +
    +
    +
    + +

    {name_error}

    +
    + { // TODO: remove this after May 20th + this.state.account_has_keys_warning &&
    + Please note: due to recent security changes if you chosen a password before during signup, this one below will override it — this is the one you need to save. +
    + } + +
    + {next_step &&
    {next_step}
    } + + {loading && } + + +
    +
    +
    + ); + } +} + +module.exports = { + path: 'create_account', + component: connect( + state => { + return { + loggedIn: !!state.user.get('current'), + offchainUser: state.offchain.get('user'), + serverBusy: state.offchain.get('serverBusy'), + suggestedPassword: state.global.get('suggestedPassword'), + } + }, + dispatch => ({ + loginUser: (username, password) => dispatch(user.actions.usernamePasswordLogin({username, password, saveLogin: true})), + logout: e => { + if (e) e.preventDefault(); + dispatch(user.actions.logout()) + }, + showTerms: e => { + if (e) e.preventDefault(); + dispatch(user.actions.showTerms()) + } + }) + )(CreateAccount) +}; diff --git a/src/app/components/pages/CreateAccount.scss b/src/app/components/pages/CreateAccount.scss new file mode 100644 index 0000000..2bf9432 --- /dev/null +++ b/src/app/components/pages/CreateAccount.scss @@ -0,0 +1,79 @@ +.CreateAccount { + max-width: 36rem; + margin: 1rem auto; + padding: 1em; + background-color: $color-white; + border: 1px solid #eee; + @include MQ(M) { + padding: 2em; + } + &__title { + color: $color-blue-black; + font-weight: bold; + padding-top: 8px; + } + .progress { + height: 8px; + background-color: #eee; + } + .progress .progress-meter { + background-color: $color-teal; + } + p { + line-height: 1.3; + } + &__btn { + @extend .e-btn; + margin-top: 8px; + text-transform: capitalize; + } + input[type=submit].disabled, input[type=submit].disabled:focus { + opacity: 0.2; + background-color: $color-blue-black; + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0); + } + &__step { + padding-top: 2em; + } + .button, .button.g-recaptcha { + @extend .e-btn; + @extend .e-btn--black; + } +} + + +.CreateAccount__rules { + opacity: 0.5; + p { + text-align: center; + } +} + +.CreateAccount__rules-button { + background-color: #f2f2f2; + padding: 0.2rem 1rem; + border-radius: 10px; + margin: 1rem 0; + font-size: 0.8rem; + color: $dark-gray; + @include themify($themes) { + color: themed('textColorSecondary'); + } +} + + +.theme-dark { + .CreateAccount { + p { + color: $color-blue-black; + } + label { + color: $color-blue-black; + } + .error p, .error label { + color: $color-red; + } + } +} + + \ No newline at end of file diff --git a/src/app/components/pages/Faq.jsx b/src/app/components/pages/Faq.jsx new file mode 100644 index 0000000..f7a3d1b --- /dev/null +++ b/src/app/components/pages/Faq.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import HelpContent from 'app/components/elements/HelpContent'; + +class Faq extends React.Component { + render() { + return ( +
    +
    + +
    +
    + ); + } +} + +module.exports = { + path: 'faq.html', + component: Faq +}; diff --git a/src/app/components/pages/Index.jsx b/src/app/components/pages/Index.jsx new file mode 100644 index 0000000..a3dfbf1 --- /dev/null +++ b/src/app/components/pages/Index.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import SvgImage from 'app/components/elements/SvgImage'; +import { translateHtml } from 'app/Translator'; + +const mailchimp_form = ` + +
    +
    +
    + +
    + +
    +
    + + +
    + +
    +
    +
    +
    + + +`; + +export default class Index extends React.Component { + + constructor(params) { + super(params); + this.state = { + submitted: false, + error: '' + }; + } + + //onSubmit(e) { + // e.preventDefault(); + // const email = e.target.elements.email.value; + // console.log('-- Index.onSubmit -->', email); + //} + + render() { + return ( +
    +
    + {/**/} + +
    +

    + {translateHtml('APP_NAME_is_a_social_media_platform_where_everyone_gets_paid_for_creating_and_curating_content')}. +

    +
    +
    +
    ); + } +}; diff --git a/src/app/components/pages/Login.jsx b/src/app/components/pages/Login.jsx new file mode 100644 index 0000000..b90139b --- /dev/null +++ b/src/app/components/pages/Login.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import LoginForm from 'app/components/modules/LoginForm'; +import tt from 'counterpart'; + +class Login extends React.Component { + render() { + if (!process.env.BROWSER) { // don't render this page on the server + return
    +
    + {tt('g.loading')}.. +
    +
    ; + } + return ( +
    +
    + +
    +
    + ); + } +} + +module.exports = { + path: 'login.html', + component: Login +}; diff --git a/src/app/components/pages/Market.jsx b/src/app/components/pages/Market.jsx new file mode 100644 index 0000000..fb273d5 --- /dev/null +++ b/src/app/components/pages/Market.jsx @@ -0,0 +1,602 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import {connect} from 'react-redux'; +//import Highcharts from 'highcharts'; + +import transaction from 'app/redux/Transaction' +import TransactionError from 'app/components/elements/TransactionError' +import DepthChart from 'app/components/elements/DepthChart'; +import Orderbook from "app/components/elements/Orderbook"; +import OrderHistory from "app/components/elements/OrderHistory"; +import {Order, TradeHistory} from "app/utils/MarketClasses"; +import {roundUp, roundDown} from "app/utils/MarketUtils"; +import tt from 'counterpart'; +import { LIQUID_TOKEN, LIQUID_TOKEN_UPPERCASE, DEBT_TOKEN_SHORT, CURRENCY_SIGN, LIQUID_TICKER, DEBT_TICKER } from 'app/client_config'; + +class Market extends React.Component { + static propTypes = { + orderbook: React.PropTypes.object, + open_orders: React.PropTypes.array, + ticker: React.PropTypes.object, + // redux PropTypes + placeOrder: React.PropTypes.func.isRequired, + user: React.PropTypes.string, + }; + + constructor(props) { + super(props); + this.state = { + buy_disabled: true, + sell_disabled: true, + buy_price_warning: false, + sell_price_warning: false, + }; + } + + componentWillReceiveProps(np) { + if (!this.props.ticker && np.ticker) { + const {lowest_ask, highest_bid} = np.ticker; + if (this.refs.buySteem_price) this.refs.buySteem_price.value = parseFloat(lowest_ask).toFixed(6); + if (this.refs.sellSteem_price) this.refs.sellSteem_price.value = parseFloat(highest_bid).toFixed(6); + } + } + + shouldComponentUpdate = (nextProps, nextState) => { + if( this.props.user !== nextProps.user && nextProps.user) { + this.props.reload(nextProps.user) + } + + if( nextState.buy_disabled != this.state.buy_disabled || + nextState.sell_disabled != this.state.sell_disabled) { + return true + } + + if( nextState.buy_price_warning != this.state.buy_price_warning || + nextState.sell_price_warning != this.state.sell_price_warning) { + return true + } + + let tc = (typeof this.props.ticker == 'undefined') || + (this.props.ticker.latest !== nextProps.ticker.latest) || + (this.props.ticker.sbd_volume !== nextProps.ticker.sbd_volume) + + let bc = (typeof this.props.orderbook == 'undefined') || + (this.props.orderbook['asks'].length != nextProps.orderbook['asks'].length) || + (this.props.orderbook['bids'].length != nextProps.orderbook['bids'].length) + + let oc = (typeof nextProps.open_orders !== undefined) && ( + typeof this.props.open_orders == 'undefined' || + JSON.stringify(this.props.open_orders) != JSON.stringify(nextProps.open_orders)) + + // Update if ticker info changed, order book changed size, or open orders length changed. + //if(tc || bc || oc) console.log("tc?", tc, "bc?", bc, "oc?", oc) + return tc || bc || oc; + } + + buySteem = (e) => { + e.preventDefault() + const {placeOrder, user} = this.props + if(!user) return + const amount_to_sell = parseFloat(ReactDOM.findDOMNode(this.refs.buySteem_total).value) + const min_to_receive = parseFloat(ReactDOM.findDOMNode(this.refs.buySteem_amount).value) + const price = (amount_to_sell / min_to_receive).toFixed(6) + const {lowest_ask} = this.props.ticker; + placeOrder(user, `${amount_to_sell} ${DEBT_TICKER}`,`${min_to_receive} ${LIQUID_TICKER}`, `${CURRENCY_SIGN}${price}/${LIQUID_TICKER}`, !!this.state.buy_price_warning, lowest_ask, (msg) => { + this.props.notify(msg) + this.props.reload(user) + }) + } + sellSteem = (e) => { + e.preventDefault() + const {placeOrder, user} = this.props + if(!user) return + const min_to_receive = parseFloat(ReactDOM.findDOMNode(this.refs.sellSteem_total).value) + const amount_to_sell = parseFloat(ReactDOM.findDOMNode(this.refs.sellSteem_amount).value) + const price = (min_to_receive / amount_to_sell).toFixed(6) + const {highest_bid} = this.props.ticker; + placeOrder(user, `${amount_to_sell} ${LIQUID_TICKER}`, `${min_to_receive} ${DEBT_TICKER}`, `${CURRENCY_SIGN}${price}/${LIQUID_TICKER}`, !!this.state.sell_price_warning, highest_bid, (msg) => { + this.props.notify(msg) + this.props.reload(user) + }) + } + cancelOrderClick = (e, orderid) => { + e.preventDefault() + const {cancelOrder, user} = this.props + if(!user) return + cancelOrder(user, orderid, (msg) => { + this.props.notify(msg) + this.props.reload(user) + }) + } + + setFormPrice = (price) => { + const p = parseFloat(price) + + this.refs.sellSteem_price.value = p.toFixed(6) + this.refs.buySteem_price.value = p.toFixed(6) + + const samount = parseFloat(this.refs.sellSteem_amount.value) + if(samount >= 0) this.refs.sellSteem_total.value = roundDown(p * samount, 3) + + const bamount = parseFloat(this.refs.buySteem_amount.value) + if(bamount >= 0) this.refs.buySteem_total.value = roundUp(p * bamount, 3) + + this.validateBuySteem() + this.validateSellSteem() + } + + percentDiff = (marketPrice, userPrice) => { + marketPrice = parseFloat(marketPrice); + return 100 * (userPrice - marketPrice) / (marketPrice) + } + + validateBuySteem = () => { + const amount = parseFloat(this.refs.buySteem_amount.value) + const price = parseFloat(this.refs.buySteem_price.value) + const total = parseFloat(this.refs.buySteem_total.value) + const valid = (amount > 0 && price > 0 && total > 0) + const {lowest_ask} = this.props.ticker; + this.setState({buy_disabled: !valid, buy_price_warning: valid && this.percentDiff(lowest_ask, price) > 15 }); + } + + validateSellSteem = () => { + const amount = parseFloat(this.refs.sellSteem_amount.value) + const price = parseFloat(this.refs.sellSteem_price.value) + const total = parseFloat(this.refs.sellSteem_total.value) + const valid = (amount > 0 && price > 0 && total > 0) + const {highest_bid} = this.props.ticker; + this.setState({sell_disabled: !valid, sell_price_warning: valid && this.percentDiff(highest_bid, price) < -15 }); + } + + render() { + const {sellSteem, buySteem, cancelOrderClick, setFormPrice, + validateBuySteem, validateSellSteem} = this + const {buy_disabled, sell_disabled, + buy_price_warning, sell_price_warning} = this.state + + let ticker = { + latest: 0, + lowest_ask: 0, + highest_bid: 0, + percent_change: 0, + sbd_volume: 0, + feed_price: 0} + + if(typeof this.props.ticker != 'undefined') { + let {latest, lowest_ask, highest_bid, percent_change, sbd_volume} = this.props.ticker; + let {base, quote} = this.props.feed + ticker = { + latest: parseFloat(latest), + lowest_ask: roundUp(parseFloat(lowest_ask), 6), + highest_bid: roundDown(parseFloat(highest_bid), 6), + percent_change: parseFloat(percent_change), + sbd_volume: (parseFloat(sbd_volume)), + feed_price: parseFloat(base.split(' ')[0]) / parseFloat(quote.split(' ')[0]) + } + } + + + // Take raw orders from API and put them into a format that's clean & useful + function normalizeOrders(orders) { + if(typeof orders == 'undefined') return {'bids': [], 'asks': []} + return ['bids', 'asks'].reduce( (out, side) => { + out[side] = orders[side].map( o => { + return new Order(o, side); + }); + return out; + }, {}) + } + + function aggOrders(orders) { + return ['bids', 'asks'].reduce( (out, side) => { + + var buff = [], last = null + orders[side].map( o => { + // o.price = (side == 'asks') ? roundUp(o.price, 6) : Math.max(roundDown(o.price, 6), 0.000001) + // the following line should be checking o.price == last.price but it appears due to inverted prices from API, + // inverting again causes values to not be properly sorted. + if(last !== null && o.getStringPrice() === last.getStringPrice()) { + //if(last !== null && o.price == last.price) { + buff[buff.length-1] = buff[buff.length-1].add(o); + // buff[buff.length-1].steem += o.steem + // buff[buff.length-1].sbd += o.sbd + // buff[buff.length-1].sbd_depth = o.sbd_depth + // buff[buff.length-1].steem_depth = o.steem_depth + } else { + buff.push(o) + } + last = o + }); + + out[side] = buff + return out + }, {}) + } + + let account = this.props.account ? this.props.account.toJS() : null; + let open_orders = this.props.open_orders; + let orderbook = aggOrders(normalizeOrders(this.props.orderbook)); + + // ORDERBOOK TABLE GENERATOR + // function table(orderbook, side = 'bids', callback = ((price,steem,sbd) => {})) { + // + // let rows = orderbook[side].slice(0, 25).map( (o,i) => + // {callback( o.price, o.steem_depth, o.sbd_depth) }} /> + // ); + // + // return + // } + + + function normalizeOpenOrders(open_orders) { + return open_orders.map( o => { + const type = o.sell_price.base.indexOf(LIQUID_TICKER) > 0 ? 'ask' : 'bid' + //{orderid: o.orderid, + // created: o.created, + return {...o, + type: type, + price: parseFloat(type == 'ask' ? o.real_price : o.real_price), + steem: type == 'ask' ? o.sell_price.base : o.sell_price.quote, + sbd: type == 'bid' ? o.sell_price.base : o.sell_price.quote} + + }) + } + + // Logged-in user's open orders + function open_orders_table(open_orders) { + const rows = open_orders && normalizeOpenOrders(open_orders).map( o => + + {o.created.replace('T', ' ')} + {o.type == 'ask' ? tt('g.sell') : tt('g.buy')} + {CURRENCY_SIGN}{o.price.toFixed(6)} + {o.steem} + {o.sbd.replace('SBD', DEBT_TOKEN_SHORT)} + cancelOrderClick(e, o.orderid)}>{tt('g.cancel')} + ) + + return + + + + + + + + + + + + {rows} + +
    {tt('market_jsx.date_created')}{tt('g.type')}{tt('g.price')}{LIQUID_TOKEN}{`${DEBT_TOKEN_SHORT} (${CURRENCY_SIGN})`}{tt('market_jsx.action')}
    + } + + + function trade_history_table(trades) { + if (!trades || !trades.length) { + return []; + } + const norm = (trades) => {return trades.map( t => { + return new TradeHistory(t); + } )} + + return + } + + const pct_change = + {ticker.percent_change < 0 ? '' : '+'}{ticker.percent_change.toFixed(2)}% + + + return ( +
    +
    +
    +
      +
    • {tt('market_jsx.last_price')} {CURRENCY_SIGN}{ticker.latest.toFixed(6)} ({pct_change})
    • +
    • {tt('market_jsx.24h_volume')} {CURRENCY_SIGN}{ticker.sbd_volume.toFixed(2)}
    • +
    • {tt('g.bid')} {CURRENCY_SIGN}{ticker.highest_bid.toFixed(6)}
    • +
    • {tt('g.ask')} {CURRENCY_SIGN}{ticker.lowest_ask.toFixed(6)}
    • + {ticker.highest_bid > 0 && +
    • {tt('market_jsx.spread')} {(200 * (ticker.lowest_ask - ticker.highest_bid) / (ticker.highest_bid + ticker.lowest_ask)).toFixed(3)}%
    • } + {/*
    • Feed price ${ticker.feed_price.toFixed(3)}
    • */} +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +

    {tt('navigation.buy_LIQUID_TOKEN', {LIQUID_TOKEN})}

    +
    + +
    +
    + +
    +
    +
    + { + const amount = parseFloat(this.refs.buySteem_amount.value) + const price = parseFloat(this.refs.buySteem_price.value) + if(amount >= 0 && price >= 0) this.refs.buySteem_total.value = roundUp(price * amount, 3) + validateBuySteem() + }} /> + {`${DEBT_TOKEN_SHORT}/${LIQUID_TOKEN}`} +
    +
    +
    + +
    +
    + +
    +
    +
    + { + const price = parseFloat(this.refs.buySteem_price.value) + const amount = parseFloat(this.refs.buySteem_amount.value) + if(price >= 0 && amount >= 0) this.refs.buySteem_total.value = roundUp(price * amount, 3) + validateBuySteem() + }} /> + {LIQUID_TOKEN} +
    +
    +
    + +
    +
    + +
    +
    +
    + { + const price = parseFloat(this.refs.buySteem_price.value) + const total = parseFloat(this.refs.buySteem_total.value) + if(total >= 0 && price >= 0) this.refs.buySteem_amount.value = roundUp(total / price, 3) + validateBuySteem() + }} /> + {`${DEBT_TOKEN_SHORT} (${CURRENCY_SIGN})`} +
    +
    +
    + + +
    + +
    + + +
    +

    {tt('navigation.sell_LIQUID_TOKEN', {LIQUID_TOKEN})}

    + +
    +
    +
    + +
    +
    +
    + { + const amount = parseFloat(this.refs.sellSteem_amount.value) + const price = parseFloat(this.refs.sellSteem_price.value) + if(amount >= 0 && price >= 0) this.refs.sellSteem_total.value = roundDown(price * amount, 3) + validateSellSteem() + }} /> + {`${DEBT_TOKEN_SHORT}/${LIQUID_TOKEN}`} +
    +
    +
    + +
    +
    + +
    +
    +
    + { + const price = parseFloat(this.refs.sellSteem_price.value) + const amount = parseFloat(this.refs.sellSteem_amount.value) + if(price >= 0 && amount >= 0) this.refs.sellSteem_total.value = roundDown(price * amount, 3) + validateSellSteem() + }} /> + {LIQUID_TOKEN} +
    +
    +
    + +
    +
    + +
    +
    +
    + { + const price = parseFloat(this.refs.sellSteem_price.value) + const total = parseFloat(this.refs.sellSteem_total.value) + if(price >= 0 && total >= 0) this.refs.sellSteem_amount.value = roundUp(total / price, 3) + validateSellSteem() + }} /> + {`${DEBT_TOKEN_SHORT} (${CURRENCY_SIGN})`} +
    +
    +
    + + +
    +
    +
    + +
    + +
    +

    {tt('market_jsx.buy_orders')}

    + { + setFormPrice(price) + }} + /> +
    + +
    +

    {tt('market_jsx.sell_orders')}

    + { + setFormPrice(price) + }} + /> +
    + +
    +

    {tt('market_jsx.trade_history')}

    + {trade_history_table(this.props.history)} +
    +
    + + {account && +
    +
    +

    {tt('market_jsx.open_orders')}

    + {open_orders_table(open_orders)} +
    +
    } + +
    + ); + } +} +const DEFAULT_EXPIRE = 0xFFFFFFFF//Math.floor((Date.now() / 1000) + (60 * 60 * 24)) // 24 hours +module.exports = { + path: 'market', + component: connect(state => { + const username = state.user.get('current') ? state.user.get('current').get('username') : null; + return { + orderbook: state.market.get('orderbook'), + open_orders: process.env.BROWSER ? state.market.get('open_orders') : [], + ticker: state.market.get('ticker'), + account: state.global.getIn(['accounts', username]), + history: state.market.get('history'), + user: username, + feed: state.global.get('feed_price').toJS() + } + }, + dispatch => ({ + notify: (message) => { + dispatch({type: 'ADD_NOTIFICATION', payload: + {key: "mkt_" + Date.now(), + message: message, + dismissAfter: 5000} + }); + }, + reload: (username) => { + console.log("Reload market state...") + dispatch({type: 'market/UPDATE_MARKET', payload: {username: username}}) + }, + cancelOrder: (owner, orderid, successCallback) => { + const confirm = tt('market_jsx.order_cancel_confirm', {order_id: orderid, user: owner}) + const successMessage = tt('market_jsx.order_cancelled', {order_id: orderid}) + dispatch(transaction.actions.broadcastOperation({ + type: 'limit_order_cancel', + operation: {owner, orderid/*, __config: {successMessage}*/}, + confirm, + successCallback: () => {successCallback(successMessage);} + //successCallback + })) + }, + placeOrder: (owner, amount_to_sell, min_to_receive, effectivePrice, priceWarning, marketPrice, successCallback, fill_or_kill = false, expiration = DEFAULT_EXPIRE) => { + // create_order jsc 12345 "1.000 SBD" "100.000 STEEM" true 1467122240 false + + // Padd amounts to 3 decimal places + amount_to_sell = amount_to_sell.replace(amount_to_sell.split(' ')[0], + String(parseFloat(amount_to_sell).toFixed(3))) + min_to_receive = min_to_receive.replace(min_to_receive.split(' ')[0], + String(parseFloat(min_to_receive).toFixed(3))) + + const isSell = amount_to_sell.indexOf(LIQUID_TICKER) > 0; + const confirmStr = isSell ? + tt('market_jsx.sell_amount_for_atleast', {amount_to_sell, min_to_receive, effectivePrice}) + : tt('market_jsx.buy_atleast_amount_for', {amount_to_sell, min_to_receive, effectivePrice}) + const successMessage = tt('g.order_placed') + ': ' + confirmStr + const confirm = confirmStr + '?' + let warning = null; + if (priceWarning) { + const warning_args = {marketPrice: CURRENCY_SIGN + parseFloat(marketPrice).toFixed(4) + "/" + LIQUID_TOKEN_UPPERCASE}; + warning = isSell ? tt('market_jsx.price_warning_below', warning_args) : tt('market_jsx.price_warning_above', warning_args); + } + const orderid = Math.floor(Date.now() / 1000) + dispatch(transaction.actions.broadcastOperation({ + type: 'limit_order_create', + operation: {owner, amount_to_sell, min_to_receive, fill_or_kill, expiration, orderid}, //, + //__config: {successMessage}}, + confirm, + warning, + successCallback: () => {successCallback(successMessage);} + })) + } + }) + )(Market) +}; diff --git a/src/app/components/pages/Market.scss b/src/app/components/pages/Market.scss new file mode 100644 index 0000000..6e57e5d --- /dev/null +++ b/src/app/components/pages/Market.scss @@ -0,0 +1,198 @@ +$buy-color: $color-teal-dark; +.buy-color { + color: $buy-color !important; +} +input.buy-color { + border-color: $buy-color !important; + background-color: smart-scale($buy-color, 95%) !important; +} +input.buy-color:hover { + color: smart-scale($buy-color, -12%) !important; + border-color: smart-scale($buy-color, -12%) !important; + background-color: smart-scale($buy-color, 90%) !important; +} + +$sell-color: $color-red; +.sell-color { + color: $sell-color !important; +} +input.sell-color { + border-color: $sell-color !important; + background-color: smart-scale($sell-color, 95%) !important; +} +input.sell-color:hover { + color: smart-scale($sell-color, -12%) !important; + border-color: smart-scale($sell-color, -12%) !important; + background-color: smart-scale($sell-color, 90%) !important; +} + +.Market__orderbook { + tbody > tr { + cursor: pointer; + } +} + + +.Market__orderbook, .Market__trade-history { + thead th { + line-height: 1.1; + small { + color: #666; + font-weight: normal; + } + } + + thead th, tbody td { + padding: 0.1rem 0.3rem; + text-align: right; + } + + tbody > tr > td { + font-size: 90%; + } + +} + + +.Market__orderform { + margin-bottom: 1rem; + line-height: 1; + + .input-group-label { + min-width: 6rem; + font-size: 83%; + } + + label { + //display: inline-block; width: 20%; + } + + input[type='text'] { + display: inline-block; + width: auto; + height: auto; + padding: 0.1rem 0.5rem 0.1rem 0.25rem; + text-align: right; + } + + input.price_warning { + background: rgba(255, 153, 0, 0.13); + } +} + +.Market__ticker { + width: 100%; + overflow: hidden; + font-size: 90%; + text-align: center; + + li { + list-style: none; + padding: 0; + display: inline-block; + border: 1px solid $light-gray; + border-radius: 3px; + margin: 0 0.25rem; + padding-right: 0.5rem; + b { + padding: 0 0.5rem; + margin-right: 0.5rem; + display: inline-block; + @include themify($themes) { + background-color: themed('backgroundColorOpaque'); + border-right: themed('borderDark'); + } + } + } + +} + +.Market__ticker-pct-up { + color: #080; +} + +.Market__ticker-pct-down { + color: #C00; +} + + +table.Market__open-orders { + thead tr, tbody tr { + :nth-child(3), + :nth-child(4), + :nth-child(5), + :nth-child(6) { + text-align: right; + } + } +} + +table.Market__trade-history { + thead tr, tbody tr { + :nth-child(3), + :nth-child(4), + :nth-child(5) { + text-align: right; + } + } +} + +table.Market__orderbook > tbody > tr.animate, table.Market__trade-history > tbody > tr.animate { + background-color: #d1d2d2; +} + +tr { + -webkit-transition: background-color 750ms linear; + -moz-transition: background-color 750ms linear; + -o-transition: background-color 750ms linear; + -ms-transition: background-color 750ms linear; + transition: background-color 750ms linear; +} + +ul.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; + + > li { + display: inline; + cursor: pointer; + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Chrome/Safari/Opera */ + -khtml-user-select: none; /* Konqueror */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; + } + + .button.hollow { + transition: 0.2s all ease-in-out; + background-color: transparent; + @include themify($themes) { + border: themed('borderAccent'); + color: themed('textColorAccent'); + } + &:hover { + @include themify($themes) { + border: themed('borderDark'); + color: themed('textColorPrimary'); + } + } + } +} + +.DepthChart > div { + height: 250px; +} + + + +.DepthChart { + rect.highcharts-background { + fill-opacity: 0; + } + .highcharts-grid path { + stroke: #999; + } +} diff --git a/src/app/components/pages/NotFound.jsx b/src/app/components/pages/NotFound.jsx new file mode 100644 index 0000000..38eb645 --- /dev/null +++ b/src/app/components/pages/NotFound.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import SvgImage from 'app/components/elements/SvgImage'; +import { Link } from 'react-router'; +import Icon from 'app/components/elements/Icon'; + +class NotFound extends React.Component { + + render() { + return ( +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +

    Sorry! This page doesn't exist.

    +

    Not to worry. You can head back to our homepage, + or check out some great posts. +

    + +
    +
    +
    + ); + } +} + +module.exports = { + path: '*', + component: NotFound +}; diff --git a/src/app/components/pages/PickAccount.jsx b/src/app/components/pages/PickAccount.jsx new file mode 100644 index 0000000..ce55221 --- /dev/null +++ b/src/app/components/pages/PickAccount.jsx @@ -0,0 +1,242 @@ +/* eslint react/prop-types: 0 */ +/*global $STM_csrf, $STM_Config */ +import React from 'react'; +import {connect} from 'react-redux'; +import Progress from 'react-foundation-components/lib/global/progress-bar'; +import { Link } from 'react-router'; +import classNames from 'classnames'; +import {api} from 'steem'; +import user from 'app/redux/User'; +import {validate_account_name} from 'app/utils/ChainValidation'; +import runTests from 'app/utils/BrowserTests'; + +class PickAccount extends React.Component { + + static propTypes = { + loginUser: React.PropTypes.func.isRequired, + serverBusy: React.PropTypes.bool + }; + + constructor(props) { + super(props); + this.state = { + name: '', + password: '', + password_valid: '', + name_error: '', + server_error: '', + loading: false, + cryptographyFailure: false, + showRules: false, + subheader_hidden: true + }; + this.onSubmit = this.onSubmit.bind(this); + this.onNameChange = this.onNameChange.bind(this); + this.onPasswordChange = this.onPasswordChange.bind(this); + } + + componentDidMount() { + const cryptoTestResult = runTests(); + if (cryptoTestResult !== undefined) { + console.error('CreateAccount - cryptoTestResult: ', cryptoTestResult); + this.setState({cryptographyFailure: true}); // TODO: do not use setState in componentDidMount + } + } + + onSubmit(e) { + e.preventDefault(); + this.setState({server_error: '', loading: true}); + const {name} = this.state; + if (!name) return; + + window.location = "/enter_email?account=" + name; + } + + onPasswordChange(password, password_valid) { + this.setState({password, password_valid}); + } + + onNameChange(e) { + const name = e.target.value.trim().toLowerCase(); + this.validateAccountName(name); + this.setState({name}); + } + + validateAccountName(name) { + let name_error = ''; + let promise; + if (name.length > 0) { + name_error = validate_account_name(name); + if (!name_error) { + this.setState({name_error: ''}); + promise = api.getAccountsAsync([name]).then(res => { + return res && res.length > 0 ? 'Account name is not available' : ''; + }); + } + } + if (promise) { + promise + .then(name_error => this.setState({name_error})) + .catch(() => this.setState({ + name_error: "Account name can't be verified right now due to server failure. Please try again later." + })); + } else { + this.setState({name_error}); + } + } + + render() { + if (!process.env.BROWSER) { // don't render this page on the server + return
    +
    +

    LOADING..

    +
    +
    ; + } + + const { + name, name_error, server_error, loading, cryptographyFailure + } = this.state; + + const {loggedIn, logout, offchainUser, serverBusy} = this.props; + const submit_btn_disabled = loading || !name || name_error; + const submit_btn_class = classNames('button action', {disabled: submit_btn_disabled}); + const account_status = offchainUser ? offchainUser.get('account_status') : null; + + if (serverBusy || $STM_Config.disable_signups) { + return
    +
    +
    +

    The creation of new accounts is temporarily disabled.

    +
    +
    +
    ; + } + if (cryptographyFailure) { + return
    +
    +
    +

    Browser Out of Date

    +

    We will be unable to create your Steem account with this browser.

    +

    The latest versions of Chrome and Firefox + are well-tested and known to work well with steemit.com.

    +
    +
    +
    ; + } + + if (loggedIn) { + return
    +
    +
    +

    You need to Logout before you can create an additional account.

    +

    Please note that Steemit can only register one account per verified user.

    +
    +
    +
    ; + } + + if (account_status === 'waiting') { + return
    +
    +
    +
    +

    Your sign up request is being processed and you will receive an email from us when it is ready.

    +

    Signup requests can take up to 7 days to be processed, but usually complete in a day or two.

    +
    +
    +
    ; + } + + if (account_status === 'approved') { + return
    +
    +
    +
    +

    Congratulations! Your sign up request has been approved.

    +

    Let's get your account created!

    +
    +
    +
    ; + } + + // const existingUserAccount = offchainUser.get('account'); + // if (existingUserAccount) { + // return
    + //
    + //
    + //

    Our records indicate that you already have steem account: {existingUserAccount}

    + //

    In order to prevent abuse Steemit can only register one account per verified user.

    + //

    You can either login to your existing account + // or send us email if you need a new account.

    + //
    + //
    + //
    ; + // } + + let next_step = null; + if (server_error) { + if (server_error === 'Email address is not confirmed') { + next_step = ; + } else if (server_error === 'Phone number is not confirmed') { + next_step = ; + } else { + next_step =
    +
    Couldn't create account. The server returned the following error:
    +

    {server_error}

    +
    ; + } + } + + return ( +
    +
    +
    +
    + +
    +

    Welcome to Steemit

    +
    +

    Your account name is how you will be known on steemit.com.
    + {/*Your account name can never be changed, so please choose carefully.*/}

    +
    +
    +
    + + +

    {name_error}

    +
    + +
    +
    +

    Got an account? Login

    +
    +
    +
    + ); + } +} + +module.exports = { + path: 'pick_account', + component: connect( + state => { + return { + loggedIn: !!state.user.get('current'), + offchainUser: state.offchain.get('user'), + serverBusy: state.offchain.get('serverBusy') + } + }, + dispatch => ({ + loginUser: (username, password) => dispatch(user.actions.usernamePasswordLogin({username, password, saveLogin: true})), + logout: e => { + if (e) e.preventDefault(); + dispatch(user.actions.logout()) + } + }) + )(PickAccount) +}; diff --git a/src/app/components/pages/Post.jsx b/src/app/components/pages/Post.jsx new file mode 100644 index 0000000..c581e91 --- /dev/null +++ b/src/app/components/pages/Post.jsx @@ -0,0 +1,216 @@ +import React from 'react'; +// import ReactMarkdown from 'react-markdown'; +import Comment from 'app/components/cards/Comment'; +import PostFull from 'app/components/cards/PostFull'; +import {connect} from 'react-redux'; + +import {sortComments} from 'app/components/cards/Comment'; +// import { Link } from 'react-router'; +import FoundationDropdownMenu from 'app/components/elements/FoundationDropdownMenu'; +import {Set} from 'immutable' +import tt from 'counterpart'; +import { localizedCurrency } from 'app/components/elements/LocalizedCurrency'; +import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'; +import {serverApiRecordEvent} from 'app/utils/ServerApiClient'; +import { INVEST_TOKEN_UPPERCASE } from 'app/client_config'; + +class Post extends React.Component { + + static propTypes = { + content: React.PropTypes.object.isRequired, + post: React.PropTypes.string, + routeParams: React.PropTypes.object, + location: React.PropTypes.object, + signup_bonus: React.PropTypes.string, + current_user: React.PropTypes.object, + }; + constructor() { + super(); + this.state = { + showNegativeComments: false + }; + this.showSignUp = () => { + serverApiRecordEvent('SignUp', 'Post Promo'); + window.location = '/pick_account'; + }; + this.shouldComponentUpdate = shouldComponentUpdate(this, 'Post') + } + + componentDidMount() { + if (window.location.hash.indexOf('comments') !== -1) { + const comments_el = document.getElementById('comments'); + if (comments_el) comments_el.scrollIntoView(); + } + } + + toggleNegativeReplies = (e) => { + this.setState({ + showNegativeComments: !this.state.showNegativeComments + }); + e.preventDefault(); + }; + + onHideComment = () => { + this.setState({commentHidden: true}) + } + + showAnywayClick = () => { + this.setState({showAnyway: true}) + } + + render() { + const {showSignUp} = this + const {current_user, signup_bonus, content} = this.props + const {showNegativeComments, commentHidden, showAnyway} = this.state + let post = this.props.post; + if (!post) { + const route_params = this.props.routeParams; + post = route_params.username + '/' + route_params.slug; + } + const dis = content.get(post); + + if (!dis) return null; + + if(!showAnyway) { + const {gray} = dis.get('stats').toJS() + if(gray) { + return ( +
    +
    +
    +
    +

    {tt('promote_post_jsx.this_post_was_hidden_due_to_low_ratings')}.{' '} +

    +
    +
    +
    +
    + ) + } + } + + let replies = dis.get('replies').toJS(); + + let sort_order = 'trending'; + if( this.props.location && this.props.location.query.sort ) + sort_order = this.props.location.query.sort; + + sortComments( content, replies, sort_order ); + + // Don't render too many comments on server-side + const commentLimit = 100; + if (global['process'] !== undefined && replies.length > commentLimit) { + console.log(`Too many comments, ${ replies.length - commentLimit } omitted.`); + replies = replies.slice(0, commentLimit); + } + + const positiveComments = replies + .map(reply => ( + ) + ); + + const negativeGroup = commentHidden && + (
    +

    + {showNegativeComments ? tt('post_jsx.now_showing_comments_with_low_ratings') : tt('post_jsx.comments_were_hidden_due_to_low_ratings')}.{' '} + +

    +
    ); + + + let sort_orders = [ 'trending', 'votes', 'new']; + let sort_labels = [ tt('main_menu.trending'), tt('g.votes'), tt('g.age') ]; + let sort_menu = []; + let sort_label; + + let selflink = `/${dis.get('category')}/@${post}`; + for( let o = 0; o < sort_orders.length; ++o ){ + if(sort_orders[o] == sort_order) sort_label = sort_labels[o]; + sort_menu.push({ + value: sort_orders[o], + label: sort_labels[o], + link: selflink + '?sort=' + sort_orders[o] + '#comments' + }); + } + const emptyPost = dis.get('created') === '1970-01-01T00:00:00' && dis.get('body') === '' + if(emptyPost) + return
    +
    +
    +

    Sorry! This page doesnt exist.

    +

    Not to worry. You can head back to our homepage, + or check out some great posts. +

    + +
    +
    +
    + + return ( +
    +
    +
    + +
    +
    + {!current_user &&
    +
    +
    + {tt('g.next_7_strings_single_block.authors_get_paid_when_people_like_you_upvote_their_post')}. +
    {tt('g.next_7_strings_single_block.if_you_enjoyed_what_you_read_earn_amount', {amount: '$'+localizedCurrency(signup_bonus.substring(1)), INVEST_TOKEN_UPPERCASE})} +
    + +
    +
    +
    } +
    +
    +
    + {positiveComments.length ? + (
    + {tt('post_jsx.sort_order')}:   + +
    ) : null} + {positiveComments} + {negativeGroup} +
    +
    +
    +
    + ); + } +} + +const emptySet = Set() + +export default connect(state => { + const current_user = state.user.get('current') + let ignoring + if(current_user) { + const key = ['follow', 'getFollowingAsync', current_user.get('username'), 'ignore_result'] + ignoring = state.global.getIn(key, emptySet) + } + return { + content: state.global.get('content'), + signup_bonus: state.offchain.get('signup_bonus'), + current_user, + ignoring, + } +} +)(Post); diff --git a/src/app/components/pages/Post.scss b/src/app/components/pages/Post.scss new file mode 100644 index 0000000..abb2406 --- /dev/null +++ b/src/app/components/pages/Post.scss @@ -0,0 +1,38 @@ +.Post__comments_sort_order { + margin: 0.5rem 0; + @include themify($themes) { + color: themed('textColorSecondary'); + } + font-size: 94%; + svg polygon { + fill: $dark-gray; + @include themify($themes) { + fill: themed('textColorSecondary'); + } + } + > span { + font-weight: bold; + } +} + +.Post__promo { + text-align: center; + font-style: italic; + font-weight: bold; + max-width: 50rem; + margin: 0 auto; + padding: 1rem 0; + @include themify($themes) { + border-bottom: themed('border'); + } + .button { + margin-top: 1rem; + text-transform: none; + } +} + +.Post_comments__content { + max-width: 50rem; + margin: 0 auto 3.5rem; + font-size: 92%; +} diff --git a/src/app/components/pages/PostPage.jsx b/src/app/components/pages/PostPage.jsx new file mode 100644 index 0000000..b6b1ab4 --- /dev/null +++ b/src/app/components/pages/PostPage.jsx @@ -0,0 +1,6 @@ +import Post from 'app/components/pages/Post'; + +module.exports = { + path: '/(:category/)@:username/:slug', + component: Post +}; diff --git a/src/app/components/pages/PostPageNoCategory.jsx b/src/app/components/pages/PostPageNoCategory.jsx new file mode 100644 index 0000000..84b478e --- /dev/null +++ b/src/app/components/pages/PostPageNoCategory.jsx @@ -0,0 +1,72 @@ +import React from 'react'; +import LoadingIndicator from 'app/components/elements/LoadingIndicator'; +import SvgImage from 'app/components/elements/SvgImage'; +import { browserHistory } from 'react-router'; +import {connect} from 'react-redux'; + +class PostWrapper extends React.Component { + + constructor() { + super(); + + this.state = { + loading: true + } + } + + componentWillMount() { + const route_params = this.props.routeParams; + const post = route_params.username + '/' + route_params.slug; + const dis = this.props.content.get(post); + if (!dis) { + this.props.getContent({author: route_params.username, permlink: route_params.slug}) + .then(content => { + if (content) { + browserHistory.replace(`/${content.category}/@${post}`) + } + }).catch(() => { + this.setState({loading: false}); + }); + } else if (dis.get("id") === "0.0.0") { // non-existing post + this.setState({loading: false}); + } else { + if (browserHistory) browserHistory.replace(`/${dis.get('category')}/@${post}`) + } + } + + shouldComponentUpdate(np, ns) { + return ( + ns.loading !== this.state.loading + ) + } + + render() { + return ( +
    + {this.state.loading ? +
    : +
    + +
    } +
    + ); + } +} + +const StoreWrapped = connect( + state => { + return { + content: state.global.get('content') + }; + }, + dispatch => ({ + getContent: (payload) => (new Promise((resolve, reject) => { + dispatch({type: 'GET_CONTENT', payload: {...payload, resolve, reject}}) + })) + }) +)(PostWrapper); + +module.exports = { + path: '/@:username/:slug', + component: StoreWrapped +}; diff --git a/src/app/components/pages/PostsIndex.jsx b/src/app/components/pages/PostsIndex.jsx new file mode 100644 index 0000000..7f99837 --- /dev/null +++ b/src/app/components/pages/PostsIndex.jsx @@ -0,0 +1,204 @@ +/* eslint react/prop-types: 0 */ +import React, {PropTypes} from 'react'; +import {connect} from 'react-redux'; +import constants from 'app/redux/constants'; +import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'; +import PostsList from 'app/components/cards/PostsList'; +import {isFetchingOrRecentlyUpdated} from 'app/utils/StateFunctions'; +import {Link} from 'react-router'; +import MarkNotificationRead from 'app/components/elements/MarkNotificationRead'; +import tt from 'counterpart'; +import Immutable from "immutable"; +import Callout from 'app/components/elements/Callout'; +// import SidebarStats from 'app/components/elements/SidebarStats'; +import SidebarLinks from 'app/components/elements/SidebarLinks'; +import SidebarNewUsers from 'app/components/elements/SidebarNewUsers'; +import Topics from './Topics'; +import ArticleLayoutSelector from 'app/components/modules/ArticleLayoutSelector'; + +class PostsIndex extends React.Component { + + static propTypes = { + discussions: PropTypes.object, + accounts: PropTypes.object, + status: PropTypes.object, + routeParams: PropTypes.object, + requestData: PropTypes.func, + loading: PropTypes.bool, + username: PropTypes.string, + blogmode: PropTypes.bool, + }; + + static defaultProps = { + showSpam: false + } + + constructor() { + super(); + this.state = {} + this.loadMore = this.loadMore.bind(this); + this.shouldComponentUpdate = shouldComponentUpdate(this, 'PostsIndex') + } + + componentDidUpdate(prevProps) { + if (window.innerHeight && window.innerHeight > 3000 && prevProps.discussions !== this.props.discussions) { + this.refs.list.fetchIfNeeded(); + } + } + + getPosts(order, category) { + const topic_discussions = this.props.discussions.get(category || ''); + if (!topic_discussions) return null; + return topic_discussions.get(order); + } + + loadMore(last_post) { + if (!last_post) return; + let {accountname} = this.props.routeParams + let {category, order = constants.DEFAULT_SORT_ORDER} = this.props.routeParams; + if (category === 'feed') { + accountname = order.slice(1); + order = 'by_feed'; + } + if (isFetchingOrRecentlyUpdated(this.props.status, order, category)) return; + const [author, permlink] = last_post.split('/'); + this.props.requestData({author, permlink, order, category, accountname}); + } + onShowSpam = () => { + this.setState({showSpam: !this.state.showSpam}) + } + render() { + let {category, order = constants.DEFAULT_SORT_ORDER} = this.props.routeParams; + let topics_order = order; + let posts = []; + let emptyText = ''; + let markNotificationRead = null; + if (category === 'feed') { + const account_name = order.slice(1); + order = 'by_feed'; + topics_order = 'trending'; + posts = this.props.accounts.getIn([account_name, 'feed']); + const isMyAccount = this.props.username === account_name; + if (isMyAccount) { + emptyText =
    + {tt('posts_index.empty_feed_1')}.

    + {tt('posts_index.empty_feed_2')}.

    + {tt('posts_index.empty_feed_3')}
    + {tt('posts_index.empty_feed_4')}
    + {tt('posts_index.empty_feed_5')}
    +
    ; + markNotificationRead = + } else { + emptyText =
    {tt('user_profile.user_hasnt_followed_anything_yet', {name: account_name})}
    ; + } + } else { + posts = this.getPosts(order, category); + if (posts && posts.size === 0) { + emptyText =
    {'No ' + topics_order + (category ? ' #' + category : '') + ' posts found'}
    ; + } + } + + const status = this.props.status ? this.props.status.getIn([category || '', order]) : null; + const fetching = (status && status.fetching) || this.props.loading; + const {showSpam} = this.state; + + // If we're at one of the four sort order routes without a tag filter, + // use the translated string for that sort order, f.ex "trending" + // + // If you click on a tag while you're in a sort order route, + // the title should be the translated string for that sort order + // plus the tag string, f.ex "trending: blog" + // + // Logged-in: + // At homepage (@user/feed) say "People I follow" + let page_title = 'Posts'; // sensible default here? + if (typeof this.props.username !== 'undefined' && category === 'feed') { + page_title = 'People I follow'; // todo: localization + } else { + switch (topics_order) { + case 'trending': // cribbed from Header.jsx where it's repeated 2x already :P + page_title = tt('main_menu.trending'); + break; + case 'created': + page_title = tt('g.new'); + break; + case 'hot': + page_title = tt('main_menu.hot'); + break; + case 'promoted': + page_title = tt('g.promoted'); + break; + } + if (typeof category !== 'undefined') { + page_title = `${page_title}: ${category}`; // maybe todo: localize the colon? + } + } + + const layoutClass = this.props.blogmode ? ' layout-block' : ' layout-list'; + + return ( +
    +
    +
    +
    +

    {page_title}

    +
    +
    +
    + +
    + +
    +
    +
    + {markNotificationRead} + {(!fetching && (posts && !posts.size)) ? {emptyText} : + + } +
    + + +
    + ); + } +} + + +module.exports = { + path: ':order(/:category)', + component: connect( + (state) => { + return { + discussions: state.global.get('discussion_idx'), + status: state.global.get('status'), + loading: state.app.get('loading'), + accounts: state.global.get('accounts'), + username: state.user.getIn(['current', 'username']) || state.offchain.get('account'), + blogmode: state.app.getIn(['user_preferences', 'blogmode']), + }; + }, + (dispatch) => { + return { + requestData: (args) => dispatch({type: 'REQUEST_DATA', payload: args}), + } + } + )(PostsIndex) +}; diff --git a/src/app/components/pages/PostsIndex.scss b/src/app/components/pages/PostsIndex.scss new file mode 100644 index 0000000..101a65c --- /dev/null +++ b/src/app/components/pages/PostsIndex.scss @@ -0,0 +1,862 @@ + +.PostsList { + clear: right; +} + +.PostsList__summaries { + list-style-type: none; + margin-left: 0; +} + +.PostsIndex__topics { + border-left: 1px solid $light-gray; +} + +.PostsIndex__topics_compact { + float: right; + width: 15rem; + position: relative; + top: -0.8rem; + > select { + border: none; + border-bottom: 1px solid $medium-gray; + border-radius: 0; + } +} + +.PostsIndex.fetching .PostsIndex__topics_compact { + visibility: hidden; +} + +/* Small only */ +@media screen and (max-width: 39.9375em) { + .PostsIndex__left { + padding: 0; + } + .PostsIndex__topics_compact { + padding: 0 0.5rem; + float: none; + width: auto; + } +} + +/* Medium and up */ +@media screen and (min-width: 39.94em) { + .PostsIndex__summaries { + > li:first-child { + .PostSummary { + margin-top: 0; + padding-top: 0; + } + } + } +} + + +// .container { +// width: 100%; +// padding-bottom: 4em; +// min-height: 100%; +// @include themify($themes) { +// background-color: themed('backgroundColor'); +// color: themed('textColor'); +// } +// } + + +.content-container { + display: flex; + flex-wrap: wrap; + width: 100%; + max-width: 1200px; + margin: 0 auto; + // padding-top: 65px; + @include MQ(M) { + flex-wrap: nowrap; + align-items: flex-start; + // padding-top: 89px; + } +} + +// Sidebar components on the homepage + +.c-sidebar { + width: 100%; + flex: 0 0 240px; + font-family: helvetica, sans-serif; + &__module { + padding: 1.5em 2em; + @include themify($themes) { + background-color: themed('moduleBackgroundColor'); + border: themed('border'); + } + margin-bottom: 1em; + box-shadow: 0px 5px 10px 0 rgba(0,0,0,0); + transition: 0.2s box-shadow ease-in-out; + &:hover { + @include MQ(M) { + box-shadow: 0px 5px 10px 0 rgba(0,0,0,0.03); + } + } + &--tags { + display: none; + @include MQ(M) { + display: block; + } + @include MQ(L) { + display: none; + } + } + } + &--left { + display: none; + order: 1; + @include MQ(L) { + display: block; + margin-left: 1em; + } + } + &--right { + display: none; + order: 3; + @include MQ(M) { + display: block; + margin-right: 1em; + } + } + &__list { + list-style: none; + margin: 0; + padding: 0; + } + &__list-item { + margin-bottom: 10px; + line-height: 1.2; + &:last-child { + margin-bottom: 0; + } + } + &__h3 { + // @extend .h3; + font-family: $font-primary; + font-weight: bold; + @include font-size(18px); + margin: 0 0 16px 0; + &--inline { + display: inline; + } + } + &__link { + @extend .link; + @extend .link--primary; + font-family: $font-primary; + &--emphasis { + font-weight: bold; + } + } + &__more-link { + @extend .link; + @extend .link--accent; + } + &__label { + display: block; + @include font-size(14px); + margin-bottom: 2px; + } + &__score { + font-weight: bold; + @include font-size(17px); + margin-bottom: 24px; + } +} + +.PostsIndex.row { + max-width: 860px; + display: flex; + flex-wrap: nowrap; + margin: 0 auto; + @include MQ(L) { + max-width: 1240px; + } +} + + + + +.PostsIndex.row.layout-list { + max-width: none; + @include MQ(L) { + max-width: 1600px; + } +} + + +.articles { + font-family: helvetica, sans-serif; + padding: 1em; + transition: all 0.2s ease-out; + border: transparent; + min-width: 300px; + max-width: 540px; + margin: auto; + background-color: transparent; + + @include MQ(M) { + margin: 0 1em; + padding: 0.5em 0em; + min-width: 550px; + max-width: 664px; + order: 2; + + } + h2 { + font-family: sans-serif; + } + @include MQ(L) { + min-width: 664px; + max-width: 664px; + padding: 1.5em 4em; + @include themify($themes) { + border: themed('border'); + box-shadow: 5px 5px 0 0 themed('contentBorderAccent'); + background-color: themed('moduleBackgroundColor'); + } + } + + &:hover { + @include MQ(L) { + @include themify($themes) { + box-shadow: 6px 6px 0 0 themed('contentBorderAccent'); + } + } + } + + &__hr { + margin-bottom: 20px; + margin-top: 0px; + @include themify($themes) { + border-bottom: themed('border'); + } + @include MQ(M) { + display: none; + } + } + + &__layout-selector { + display: none; + cursor: pointer; + @include MQ(M) { + display: inline-block; + padding-left: 14px; + } + } + &__icon--layout { + width: 24px; + height: 24px; + position: relative; + top: 5px; + } + &__summary { + margin: 0em 0 1em; + transition: 0.2s all ease-in-out; + border: transparent; + padding-bottom: 10px; + @include themify($themes) { + background-color: themed('moduleBackgroundColor'); + border-bottom: themed('border'); + } + @include MQ(M) { + margin: 0 0 40px; + box-shadow: 0px 5px 10px 0 rgba(0,0,0,0.0); + padding-bottom: 0; + @include themify($themes) { + border: themed('border'); + } + } + &:hover { + @include MQ(M) { + box-shadow: 0px 5px 10px 0 rgba(0,0,0,0.03); + } + } + } + + &__summary-header { + display: flex; + align-items: center; + padding: 6px 0 8px; + position: relative; + @include MQ(M) { + padding: 10px 16px 5px; + @include themify($themes) { + border-bottom: themed('border'); + } + } + &--footer { + @include themify($themes) { + border-top: themed('border'); + border-bottom: themed('border'); + } + @include MQ(M) { + @include themify($themes) { + border-bottom: transparent; + } + } + } + } + &__summary-footer { + display: flex; + align-items: center; + position: relative; + width: 100%; + @include font-size(15px); + @include MQ(M) { + padding: 16px; + @include themify($themes) { + border-top: themed('border'); + } + } + a { + @extend .link; + @extend .link--primary; + @include font-size(15px); + } + } + &__header { + padding-top:5px; + padding-bottom: 1em; + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + @include MQ(M) { + padding-top: 0; + } + } + &__header-col { + &--right { + order: 1; + width: 100%; + @media only screen and (min-width: 370px) { + width: auto; + order: 2; + } + } + } + &__resteem { + padding-bottom: 0px; + margin-top: 24px; + + @include MQ(M) { + margin-top: 8px; + padding-bottom: 5px; + @include themify($themes) { + border-bottom: themed('border'); + } + } + } + &__resteem-text { + @include font-size(14px); + margin-bottom: 4px; + @include themify($themes) { + color: themed('textColorSecondary'); + } + } + &__resteem-username { + @extend .link--secondary; + text-decoration: none; + } + &__icon-100 { + padding-left: 8px; + display: inline-block; + position: relative; + top: 0px; + } + &__h1 { + font-family: $font-primary; + font-weight: bold; + @include font-size(20px); + margin: 0; + text-transform: capitalize; + @include MQ(M) { + @include font-size(22px); + } + } + &__h2 { + // @extend .h2; + margin: 0; + @include font-size(16px); + overflow : hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + a { + font-weight: bold; + @include themify($themes) { + color: themed('textColorPrimary'); + } + &:visited { + @include themify($themes) { + color: themed('textColorSecondary'); + } + } + } + @include MQ(M) { + @include font-size(18px); + } + } + &__h3 { + display: inline; + } + &__tag-selector { + display: inline-block; + select.Topics { + margin-bottom: 0 !important; + border: transparent; + background-color: transparent; + border-bottom: 1px solid #999; + border-radius: 0; + position: relative; + top: -4px; + margin-top: 12px; + @include themify($themes) { + color: themed('textColorSecondary'); + } + @media only screen and (min-width: 370px) { + margin-top: 0; + } + } + @include MQ(L) { + display: none; + } + } + &__p { + // @extend .p; + margin: 0; + padding-top: 4px; + overflow : hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + } + &__link { + text-decoration: none; + @include themify($themes) { + color: themed('textColorPrimary'); + } + } + // &__profile-img { + // display: none; + // width: 54px; + // height: auto; + // } + &__text-content { + @include MQ(M) { + padding: 0 1.2em; + } + } + &__tags { + @include themify($themes) { + color: themed('textColorSecondary'); + } + @include font-size(14px); + margin-bottom: 18px; + } + &__tag-link { + @include font-size(14px); + @include themify($themes) { + color: themed('textColorSecondary'); + } + a { + @extend .link; + @extend .link--secondary; + } + } + &__flag { + width: 24px; + height: auto; + position: absolute; + right: -4px; + top: 0px; + padding: 2px; + @include MQ(M) { + right: 10px; + top: 12px; + } + + .icon-flag-svg { + @include themify($themes) { + fill: themed('textColorSecondary'); + } + transition: 0.2s all ease-in-out; + } + &:hover { + cursor: pointer; + .icon-flag-svg { + @include themify($themes) { + fill: themed('textColorError'); + } + } + } + } + &__content-block { + margin-bottom: 16px; + @include MQ(M) { + &--text { + margin-top: 1em; + } + } + } + + &__content-block--img + &__content-block--text { + margin-top: 0; + } + &__resteem, &__content-block--text, &__metadata { + @include MQ(M) { + padding-left: 1.1em; + padding-right: 1.1em; + } + } +} + +// ,user layout-block + +.user { + display: flex; + align-items: center; + &__col { + line-height: 1; + } + &__link { + @extend .link; + &:hover { + .user__username { + transition: 0.2s all ease-in-out; + @include themify($themes) { + color: themed('textColorAccent'); + } + } + } + } + &__profile-img { + width: 40px !important; + height: 40px !important; + margin-right: 8px; + transition: width 0.2s ease-out; + @include MQ(M) { + width: 48px !important; + height: 48px !important; + margin-right: 10px; + } + } + &__name { + display: inline; + font-weight: bold; + padding-right: 4px; + margin: 0; + line-height: 1.2; + + a { + @extend .link--primary; + font-weight: bold; + @include font-size(16px); + } + } + &__username, &__reputation { + @extend .link--secondary; + font-weight: normal; + @include font-size(14px); + } +} + + + +.timestamp { + &__link { + text-decoration: none; + } + &__time { + @extend .link--secondary; + font-weight: normal; + @include font-size(14px); + } +} + + + +.icon-svg { + transition: 0.2s all 0.05s ease-in-out; + @include themify($themes) { + fill: themed('textColorSecondary'); + } + &--accent { + @include themify($themes) { + fill: themed('textColorAccent'); + } + } + &--layout-line1, &--layout-line2, &--layout-line3 { + height: 2px; + @include opacity(1); + transition: 0.3s all ease-in-out; + } + &--layout-line2 { + y: 11px; + } + &--layout-line1 { + y: 6px; + } + &--layout-line3 { + y: 16px; + } +} + + +.articles__layout-selector .icon-svg { + &--accent { + @include themify($themes) { + fill: themed('textColorSecondary'); + } + } +} + +.articles__layout-selector:hover .icon-svg { + &--accent { + @include themify($themes) { + fill: themed('textColorPrimary'); + } + } +} + + + +// Compressed list view CSS + +a#changeLayout:focus { + outline: none; +} + +@include MQ(M) { + .layout-list { + transition: 0.3s all ease-in-out; + + .icon-svg { + &--layout-line3 { + y: 22px; + @include opacity(0); + } + &--layout-line1, &--layout-line2 { + + height: 4px; + } + &--layout-line2 { + y: 14px; + } + &--layout-line1 { + y: 6px; + } + } + .content-container { + max-width: 1520px; + } + .c-sidebar { + &--right { + display: none; + order: 3; + @include MQ(L) { + display: block; + margin-right: 1em; + } + } + } + .articles { + padding: 1.5em 1.5em; + max-width: none; + max-width: 1056px; + @include MQ(XL) { + min-width: 1050px; + } + @include themify($themes) { + background-color: themed('moduleBackgroundColor'); + border: themed('border'); + } + &__hr { + @include MQ(M) { + display: block; + @include themify($themes) { + border-bottom: themed('border'); + } + } + } + &__summary { + border: transparent; + box-shadow: none; + padding-bottom: 0px; + margin: 0 0 1.4em; + @include themify($themes) { + border-bottom: themed('borderLight'); + } + @include MQ(M) { + padding-bottom: 6px; + } + } + &__h2 { + @include font-size(16px); + + @include MQ(M) { + -webkit-line-clamp: 2; + } + @include MQ(XL) { + -webkit-line-clamp: 1; + } + + } + &__feature-img-container { + overflow: hidden; + width: 130px; + height: 77px; + position: relative; + display: inline-block; + } + &__feature-img { + position: absolute; + width:100% !important; + top: 50%; + transform:translateY(-50%) + } + &__summary-header { + padding: 2px 0 5px; + border: transparent; + } + &__summary-footer { + padding: 2px 0 5px; + border: transparent; + padding: 4px; + padding-top: 3px; + // @include themify($themes) { + // border-bottom: themed('borderLight'); + // } + } + &__p { + @include font-size(15px); + -webkit-line-clamp: 1; + padding-right: 6px; + margin-top: 2px; + padding-top: 0px; + } + &__content { + display: flex; + align-items: top; + } + &__content-block { + margin-bottom: 0; + &--img { + margin-right: 14px; + } + &--text { + margin-top: 0; + min-width: 300px; + } + } + &__tags { + margin: 4px 0 0; + } + &__flag { + top: 0; + } + &__resteem { + padding-bottom: 0; + border-bottom: transparent; + } + &__resteem-icon { + position: relative; + top: -2px; + } + &__resteem, &__content-block--text, &__metadata { + @include MQ(M) { + padding-left: 0; + padding-right: 0; + } + } + } + // ,user layout-list + .user { + &__name { + @include font-size(15px); + font-weight: normal; + } + } + } +} + +@include MQ(L) { + .layout-list { + .articles { + padding: 1.5em 3em; + } + } +} + +@include MQ(XL) { + .layout-list { + .articles { + padding: 1.5em 4em; + } + } +} + + +.layout-list .user .Userpic { + width: 24px !important; + height: 24px !important; + margin-right: 7px; +} + +.layout-block .Userpic { + margin-right: 8px; +} + +// .Comment__Userpic .Userpic { +// width: 48px !important; +// height: 48px !important; +// } + + +.icon-svg { + transition: 0.2s all ease-in-out; + @include themify($themes) { + fill: themed('textColorSecondary'); + } + &--accent { + @include themify($themes) { + fill: themed('textColorAccent'); + } + } + &--layout-line1, &--layout-line2, &--layout-line3 { + height: 2px; + @include opacity(1); + transition: 0.3s all ease-in-out; + } + &--layout-line2 { + y: 11px; + } + &--layout-line1 { + y: 6px; + } + &--layout-line3 { + y: 16px; + } +} + + + +.articles__resteem .username { + @extend .link--secondary; + text-decoration: none; +} + + +.articles__resteem-icon path { + fill: #cacaca; +} diff --git a/src/app/components/pages/Privacy.jsx b/src/app/components/pages/Privacy.jsx new file mode 100644 index 0000000..60d9250 --- /dev/null +++ b/src/app/components/pages/Privacy.jsx @@ -0,0 +1,353 @@ +import React from 'react'; + +class Privacy extends React.Component { + render() { + return ( +
    +
    +

    Steemit, Inc Privacy Policy

    +
    +

    Effective April 1, 2016

    +

    + 1 + We want you to understand how and why Steemit, Inc (“Steemit,” “we” or “us”) collects, uses, and shares information about you when + you access and use Steemit’s websites, mobile apps, widgets, and other online products and services (collectively, the "Services") + or when you otherwise interact with us. +

    +
    +
    +

    Information You Provide to Us

    +

    + 2 + We collect information you provide directly to us when you use our Services. Depending on which Service you use, we may collect + different information about you. This includes: +

    +
    +
    +

    Information Regarding Your Use of the Services

    +

    + 3 + We collect the content and other information you provide when you use our Services. This includes information used to create your + account (e.g., a username, an email address, phone number), account preferences, and the content of information you post to the + Services (e.g., text, photos, videos, links). +

    +
    +
    +

    Transactional Information

    +

    + 4 + If you purchase products or services from us (e.g., Steem Power), we will collect certain information from you, including your name, + address, email address, and information about the product or service you are purchasing. Payments are processed by third-party + payment processors (e.g., Stripe and PayPal), so please refer to the applicable processor’s terms and privacy policy for more + information about how payment information is processed and stored. +

    +
    +
    +

    Other Information

    +

    + 5 + You may choose to provide other information directly to us. For example, we may collect information when you fill out a form, + participate in contests, sweepstakes or promotions, apply for a job, communicate with us via third-party sites and services, request + customer support or otherwise communicate with us. +

    +
    +
    +

    Information We Collect Automatically

    +

    + 6 + When you access or use our Services, we may also automatically collect information about you. This includes: +

    +
    +
    +

    Log and Usage Data

    +

    + 7 + We may log information when you access and use the Services. This may include your IP address, user-agent string, browser type, + operating system, referral URLs, device information (e.g., device IDs), pages visited, links clicked, user interactions (e.g., + voting data), the requested URL, hardware settings, and search terms. +

    +
    +
    +

    Information Collected from Cookies

    +

    + 8 + We may receive information from cookies, which are pieces of data your browser stores and sends back to us when making requests. We + use this information to improve your experience, understand user activity, and improve the quality of our Services. For + example, we store and retrieve information about your preferred language and other settings. For more information on how you can + disable cookies, please see “Your Choices” below. +

    +
    +
    +

    Location Information

    +

    + 9 + With your consent, we may collect information about the specific location of your mobile device (for example, by using GPS or + Bluetooth). You can revoke this consent at any time by changing the preferences on your device, but doing so may affect your ability + to use all of the features and functionality of our Services. +

    +
    +
    +

    Social Sharing

    +

    + 10 + We may offer social sharing features or other integrated tools that let you share content or actions you take on our Services with + other media. Your use of these features enables the sharing of certain information with your friends or the public, depending on the + settings you establish with the third party that provides the social sharing feature. For more information about the purpose and + scope of data collection and processing in connection with social sharing features, please visit the privacy policies of the third + parties that provide these social sharing features (e.g., Tumblr, Facebook, Reddit, Pinterest, and Twitter). +

    +
    +
    +

    How We Use Information About You

    +
    + 11 + We use information about you to: +
      +
    • Provide, maintain, and improve the Services;
    • +
    • Help protect the safety of Steemit and our users, which includes blocking suspected spammers, addressing abuse, and + enforcing the Steemit user agreement and the Terms of Service; +
    • +
    • Send you technical notices, updates, security alerts, invoices and other support and administrative messages;
    • +
    • Provide customer service;
    • +
    • Communicate with you about products, services, offers, promotions, and events, and provide other news and information we + think will be of interest to you (for information about how to opt out of these communications, see “Your Choices” below); +
    • +
    • Monitor and analyze trends, usage, and activities in connection with our Services;
    • +
    • Personalize the Services and provide advertisements, content and features that match user profiles or interests.
    • +
    +
    +
    +
    +

    How We Share Information

    +
    + 12 + When you use the Services, certain information may be shared with other users and the public. For example: +
      +
    • When you submit a post or comment to the Services, visitors to and users of our Services will be able to see the content of + your posts and comments, the username associated with your posts or comments, and the date and time you originally submitted + the post or comment. Although some parts of the Services may be private or quarantined, they may become public and you + should take that into consideration before posting to the Services. +
    • +
    • When other users view your profile, they will be able to see information about your activities on the Services, such as your + username, prior posts and comments, Steem Power, and how long you have been a member of the Services. +
    • +
    +
    +
    +
    +

    + 13 + Please note that, even when you delete your account, the posts, comments and messages you submit through the Services may still be + viewable or available on our servers. For more information, see “Your Choices” below. +

    +
    +
    +
    + 14 + We will not share, sell, or give away any of our users’ personal information to third parties, unless one of the following + circumstances applies: +
      +
    • Except as it relates to advertisers and our ad partners, we may share information with vendors, consultants, and other + service providers who need access to such information to carry out work for us; +
    • +
    • If you participate in contests, sweepstakes, promotions, special offers, or other events or activities in connection with + our Services, we may share information with entities that partner with us to provide these offerings; +
    • +
    • We may share information (and will attempt to provide you with prior notice, to the extent legally permissible) in response + to a request for information if we believe disclosure is in accordance with, or required by, any applicable law, regulation, + legal process or governmental request; +
    • +
    • We may share information in response to an emergency if we believe it's necessary to prevent imminent and serious bodily + harm to a person; +
    • +
    • We may share information if we believe your actions are inconsistent with our user agreements, or other Steemit policies, or + to protect the rights, property, and safety of ourselves and others; +
    • +
    • We may share information between and among Steemit, and its current and future parents, affiliates, subsidiaries, and other + companies under common control and ownership; and +
    • +
    • We may share information with your consent or at your direction.
    • +
    +
    +
    +
    +

    + 15 + We may share aggregated or de-identified information, which cannot reasonably be used to identify you. +

    +
    +
    +

    Ads and Analytics Partners

    +

    + 16 + We may partner with third-party advertisers, ad networks, and analytics providers to deliver advertising and content targeted to + your interests and to better understand your use of the Services. These third parties may collect information sent by your computer, + browser, or mobile device in response to a request for content, such as unique identifiers, your IP address, or other information + about your computer or device. For example: +

    +
    +
    +

    Advertisers and Ad Networks

    +

    + 17 + Our ad partners and network may use cookies and use related technologies to collect information when ads are delivered to you on our + Services, but Steemit does not link to or provide your actual Steemit account details to these advertising partners. This means that + Steemit does not share your individual account browsing habits with advertisers. Steemit cannot see advertisers’ cookies and + advertisers will not see Steemit cookies. +

    +
    +
    +

    Analytics Partners

    +

    + 18 + We use analytics partners (such as Google Analytics) to help analyze usage and traffic for our Services. As an example, we may use + analytics partners to analyze and measure, in the aggregate, the number of unique visitors to our Services. +

    +
    +
    +

    + 19 + For more information about how you may control the collection and/or use of information for advertising and analytics purposes, + please see “Your Choices.” +

    +
    +
    +

    Security

    +

    + 20 + We take reasonable measures to help protect information about you from loss, theft, misuse and unauthorized access, disclosure, + alteration, and destruction. +

    +
    +
    +

    Children under 13

    +

    + 21 + Although we welcome users from all walks of life, Steemit is not intended or directed at individuals under the age of 13. Therefore, + individuals under the age of 13 may not create an account or otherwise access or use the Services. +

    +
    +
    +

    Your Choices

    +

    + 22 + As a Steemit user, you have choices about how to protect and limit the collection, use, and disclosure of, information about you. + This includes: +

    +
    +
    +

    Preferences

    +

    + 23 + We may provide you with tools and preference settings that allow you to access, correct, delete, and modify information associated + with your account. +

    +
    +
    +

    Account Information

    +

    + 24 + You may delete your account information at any time by logging into your account and following the steps under “Preferences.” When + you delete your account, your profile is no longer visible to other users and disassociated from content you posted under that + account. Please note, however, that the posts, comments, and messages you submitted prior to deleting your account will still be + visible to others, unless you delete such content. We may also retain certain information about you as required by law or for + legitimate business purposes after you delete your account. Any information that has been published to the Steem blockchain will + also remain indefinitely. +

    +
    +
    +

    Cookies

    +

    + 25 + Most web browsers are set to accept cookies by default. If you prefer, you can usually choose to set your browser to remove or + reject first- and third-party cookies. Please note that if you choose to remove or reject cookies, this could affect the + availability and functionality of our Services. +

    +
    +
    +

    Third-Party Advertising and Analytics

    +

    + 26 + Some analytics providers may provide specific opt-out mechanisms (e.g. Google Analytics Opt-out), and we will provide, as needed, + additional tools and third-party services that allow you to better understand cookies and how you can opt-out. You have the ability + to opt out of having your web browsing information used for + behavioral advertising purposes. For more information about behavioral advertising, or to opt out, please visit + www.aboutads.info/choices. +

    +
    +
    +

    Do Not Track

    +

    + 27 + Do Not Track (“DNT”) is a privacy preference that you can set in most web browsers. We support DNT in our Services. There is no + accepted standard on how to respond to web browsers’ DNT signals. When you have DNT enabled, we may still use information collected + for analytics and measurement purposes or to otherwise provide our Services (e.g., Steemit.com buttons), but we will not load any + third-party trackers. You may, however, opt out of having information about you collected and used for behavioral advertising + purposes, as described above. +

    +
    +
    +

    Promotional Communications

    +

    + 28 + You may opt out of receiving promotional communications from us by following the instructions in those communications. If you opt + out, we may still send you non-promotional communications, such as information about your account or your use of our Services. +

    +
    +
    +

    Mobile Notifications

    +

    + 29 + With your consent, we may send promotional and non-promotional push notifications or alerts to your mobile device. You can + deactivate these messages at any time by changing the notification settings on your mobile device. +

    +
    +
    +

    International Data Transfers

    +

    + 30 + We are based in the United States and the information we collect is governed by U.S. law. By accessing or using the Services or + otherwise providing information to us, you consent to the processing, transfer and storage of information in and to the U.S. and + other countries, where you may not have the same rights as you do under local law. +

    +
    +
    +

    + 31 + Steemit complies with the U.S.-E.U. and U.S.-Swiss Safe Harbor Privacy Principles of notice, choice, onward transfer, security, data + integrity, access, and enforcement. Despite an adverse judgment by the European Court of Justice on October 6, 2015, the U.S. + Department of Commerce has advised that it continues to administer the Safe Harbor program until further notice. To learn more about + the Safe Harbor program, and to view our certification, please visit the U.S. Department of Commerce website. If you have concerns + about our compliance with the Safe Harbor program, you should contact us. If you are unable to resolve your concern or dispute with + us, you may submit complaints to JAMS for mediation pursuant to the JAMS International Mediation Rules. +

    +
    +
    +

    Changes

    +

    + 32 + We may change this Privacy Policy from time to time. If we do, we will let you know by revising the date at the top of the policy. + If we make a change to this policy that, in our sole discretion, is material, we will provide you with additional notice (such as + adding a statement to steemouncements, the front page of the Services or sending you a notification). We encourage you to review the + Privacy Policy whenever you access or use our Services or otherwise interact with us to stay informed about our information + practices and the ways you can help protect your privacy. If you continue to use our Services after Privacy Policy changes go into + effect, you consent to the revised policy. +

    +
    +
    +

    Contact Us

    +

    + 33 + If you have any questions about this Privacy Policy, please email contact@steemit.com. +

    +
    +
    +
    + ); + } +} + +module.exports = { + path: 'privacy.html', + component: Privacy +}; diff --git a/src/app/components/pages/Privacy.scss b/src/app/components/pages/Privacy.scss new file mode 100644 index 0000000..38b6c27 --- /dev/null +++ b/src/app/components/pages/Privacy.scss @@ -0,0 +1,11 @@ +.Privacy { + max-width: 800px; + padding: 1.5em 0 3em; + .section { + font-size: 100%; + @include themify($themes) { + fill: themed('textColorPrimary'); + } + padding-right: 0.5rem; + } +} diff --git a/src/app/components/pages/RecoverAccountStep1.jsx b/src/app/components/pages/RecoverAccountStep1.jsx new file mode 100644 index 0000000..8e89b9b --- /dev/null +++ b/src/app/components/pages/RecoverAccountStep1.jsx @@ -0,0 +1,258 @@ +import React from 'react'; +import SvgImage from 'app/components/elements/SvgImage'; +import LoadingIndicator from 'app/components/elements/LoadingIndicator'; +import PasswordInput from 'app/components/elements/PasswordInput'; +import constants from 'app/redux/constants'; +import tt from 'counterpart'; +import { FormattedHTMLMessage } from 'app/Translator'; +import { APP_DOMAIN, APP_NAME, SUPPORT_EMAIL } from 'app/client_config'; +import {PrivateKey} from 'steem/lib/auth/ecc'; +import {api} from 'steem'; + +const email_regex = /^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/; + +function passwordToOwnerPubKey(account_name, password) { + let pub_key; + try { + pub_key = PrivateKey.fromWif(password); + } catch(e) { + pub_key = PrivateKey.fromSeed(account_name + 'owner' + password); + } + return pub_key.toPublicKey().toString(); +} + +class RecoverAccountStep1 extends React.Component { + + static propTypes = { + }; + + constructor(props) { + super(props); + this.state = { + name: '', + name_error: '', + email: '', + email_error: '', + error: '', + progress_status: '', + password: {value: '', valid: false}, + show_social_login: '', + email_submitted: false + }; + this.onNameChange = this.onNameChange.bind(this); + this.onEmailChange = this.onEmailChange.bind(this); + this.onPasswordsChange = this.onPasswordsChange.bind(this); + this.onSubmit = this.onSubmit.bind(this); + this.onSubmitEmail = this.onSubmitEmail.bind(this); + } + + onNameChange(e) { + const name = e.target.value.trim().toLowerCase(); + this.validateAccountName(name); + this.setState({name, error: ''}); + } + + onEmailChange(e) { + const email = e.target.value.trim().toLowerCase(); + let email_error = ''; + if (!email_regex.test(email.toLowerCase())) email_error = tt('recoveraccountstep1_jsx.not_valid'); + this.setState({email, email_error}); + } + + validateAccountName(name) { + if (!name) return; + api.getAccountsAsync([name]).then(res => { + this.setState({name_error: !res || res.length === 0 ? tt('recoveraccountstep1_jsx.account_name_is_not_found') : ''}); + if(res.length) { + const [account] = res + // if your last owner key update is prior to July 14th then the old key will not be able to recover + const ownerUpdate = /Z$/.test(account.last_owner_update) ? account.last_owner_update : account.last_owner_update + 'Z' + const ownerUpdateTime = new Date(ownerUpdate).getTime() + const THIRTY_DAYS_AGO = new Date(Date.now() - (30 * 24 * 60 * 60 * 1000)).getTime() + if(ownerUpdateTime < Math.max(THIRTY_DAYS_AGO, constants.JULY_14_HACK)) + this.setState({name_error: tt('recoveraccountstep1_jsx.unable_to_recover_account_not_change_ownership_recently')}) + } + }) + } + + validateAccountOwner(name) { + const oldOwner = passwordToOwnerPubKey(name, this.state.password.value); + return api.getOwnerHistoryAsync(name).then(history => { + const res = history.filter(a => { + const owner = a.previous_owner_authority.key_auths[0][0]; + return owner === oldOwner; + }); + return res.length > 0; + }); + } + + getAccountIdentityProviders(name, owner_key) { + return fetch('/api/v1/account_identity_providers', { + method: 'post', + mode: 'no-cors', + credentials: 'same-origin', + headers: { + Accept: 'application/json', + 'Content-type': 'application/json' + }, + body: JSON.stringify({csrf: $STM_csrf, name, owner_key}) + }).then(r => r.json()).then(res => { + console.log('-- validateOffchainAccount -->', res); + return res.error ? 'email' : res.provider; + }); + } + + onPasswordsChange({oldPassword, valid}) { + this.setState({password: {value: oldPassword, valid}, error: ''}); + } + + onSubmit(e) { + e.preventDefault(); + const owner_key = passwordToOwnerPubKey(this.state.name, this.state.password.value); + this.validateAccountOwner(this.state.name).then(result => { + if (result) { + this.getAccountIdentityProviders(this.state.name, owner_key).then(provider => { + this.setState({show_social_login: provider}); + }); + } + else this.setState({error: tt('recoveraccountstep1_jsx.password_not_used_in_last_days')}); + }); + } + + onSubmitEmail(e) { + e.preventDefault(); + const {name, password} = this.state; + const owner_key = passwordToOwnerPubKey(name, password.value); + fetch('/api/v1/initiate_account_recovery_with_email', { + method: 'post', + mode: 'no-cors', + credentials: 'same-origin', + headers: { + Accept: 'application/json', + 'Content-type': 'application/json' + }, + body: JSON.stringify({ + csrf: $STM_csrf, + contact_email: this.state.email, + account_name: name, + owner_key + }) + }).then(r => r.json()).then(res => { + if (res.error) { + this.setState({email_error: res.error || 'Unknown'}); + } else { + if (res.status === 'ok') { + this.setState({email_submitted: true}); + } + if (res.status === 'duplicate') { + this.setState({email_error: tt('recoveraccountstep1_jsx.request_already_submitted_contact_support', {SUPPORT_EMAIL})}); + } + } + }).catch(error => { + console.error('request_account_recovery server error (2)', error); + this.setState({email_error: (error.message ? error.message : error)}); + }); + } + + render() { + const {name, name_error, email, email_error, error, progress_status, password, show_social_login, email_submitted} = this.state; + const owner_key = passwordToOwnerPubKey(name, password.value); + const valid = name && !name_error && password.valid; + const submit_btn_class = 'button action' + (!valid ? ' disabled' : ''); + const show_account_and_passwords = !email_submitted && !show_social_login; + return ( +
    + {show_account_and_passwords &&
    +
    +

    {tt('navigation.stolen_account_recovery')}

    +

    + {tt('recoveraccountstep1_jsx.recover_account_intro', {APP_URL: APP_DOMAIN, APP_NAME})} +

    +
    +
    + +

    {name_error}

    +
    + +
    +
    {error}
    + {progress_status ? {progress_status} + : } + +
    +
    } + + {show_social_login && show_social_login !== 'email' && +
    + + + +
    +
    + {show_social_login === 'both' ?

    {tt('recoveraccountstep1_jsx.login_with_facebook_or_reddit_media_to_verify_identity')}.

    + :

    {tt('recoveraccountstep1_jsx.login_with_social_media_to_verify_identity', { + provider: show_social_login.charAt(0).toUpperCase() + show_social_login.slice(1) + })}.

    } +
    +
    +
    +   +
    + {(show_social_login === 'both' || show_social_login === 'facebook') &&
    +
    + +
    +
    + +
    +
    } +
    +   +
    + {(show_social_login === 'both' || show_social_login === 'reddit') &&
    +
    + +
    +
    + +
    +
    } +
    +
     
    +
    +
    + } + {show_social_login && show_social_login === 'email' && +
    +
    + { + email_submitted + ? tt('recoveraccountstep1_jsx.thanks_for_submitting_request_for_account_recovery', {APP_NAME}) + :
    +

    {tt('recoveraccountstep1_jsx.enter_email_toverify_identity')}

    +
    + +

    {email_error}

    + +
    +
    + } +
    +
    + } +
    + ); + } +} + +module.exports = { + path: 'recover_account_step_1', + component: RecoverAccountStep1 +}; + diff --git a/src/app/components/pages/RecoverAccountStep1.scss b/src/app/components/pages/RecoverAccountStep1.scss new file mode 100644 index 0000000..6cf573e --- /dev/null +++ b/src/app/components/pages/RecoverAccountStep1.scss @@ -0,0 +1,41 @@ + +.RestoreAccount { + + .button { + text-decoration: none; + font-weight: bold; + transition: 0.2s all ease-in-out; + text-transform: capitalize; + border-radius: 0; + @include font-size(18px); + @include themify($themes) { + background-color: themed('buttonBackground'); + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0), 5px 5px 0 0 themed('buttonBoxShadow'); + color: themed('buttonText'); + } + &:hover, &:focus { + @include themify($themes) { + background-color: themed('buttonBackgroundHover'); + box-shadow: 2px 2px 2px 0 rgba(0,0,0,0.1), 7px 7px 0 0 themed('buttonBoxShadowHover'); + color: themed('buttonTextHover'); + } + } + &:visited, &:active { + @include themify($themes) { + color: themed('ButtonText'); + } + } + } + .button.disabled, .button[disabled] { + opacity: 0.25; + cursor: not-allowed; + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0); + &:hover { + @include themify($themes) { + background-color: themed('buttonBackground'); + box-shadow: 0px 0px 0px 0 rgba(0,0,0,0); + color: themed('buttonText'); + } + } + } +} \ No newline at end of file diff --git a/src/app/components/pages/RecoverAccountStep2.jsx b/src/app/components/pages/RecoverAccountStep2.jsx new file mode 100644 index 0000000..c12d3e8 --- /dev/null +++ b/src/app/components/pages/RecoverAccountStep2.jsx @@ -0,0 +1,199 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import GeneratedPasswordInput from 'app/components/elements/GeneratedPasswordInput'; +import LoadingIndicator from 'app/components/elements/LoadingIndicator'; +import tt from 'counterpart'; +import Callout from 'app/components/elements/Callout'; +import {PrivateKey} from 'steem/lib/auth/ecc'; +import {api} from 'steem'; + +function passwordToOwnerPubKey(account_name, password) { + let pub_key; + try { + pub_key = PrivateKey.fromWif(password); + } catch(e) { + pub_key = PrivateKey.fromSeed(account_name + 'owner' + password); + } + return pub_key.toPublicKey().toString(); +} + +class RecoverAccountStep2 extends React.Component { + + static propTypes = { + account_to_recover: React.PropTypes.string, + recoverAccount: React.PropTypes.func.isRequired + }; + + constructor(props) { + super(props); + this.state = { + name_error: '', + oldPassword: '', + newPassword: '', + valid: false, + error: '', + progress_status: '', + success: false, + }; + this.onPasswordChange = this.onPasswordChange.bind(this); + this.oldPasswordChange = this.oldPasswordChange.bind(this); + this.onSubmit = this.onSubmit.bind(this); + this.onRecoverFailed = this.onRecoverFailed.bind(this); + this.onRecoverSuccess = this.onRecoverSuccess.bind(this); + } + + oldPasswordChange(e) { + const oldPassword = e.target.value.trim(); + this.setState({oldPassword}); + } + + onPasswordChange(newPassword, valid) { + this.setState({newPassword, valid}); + } + + onRecoverFailed(error) { + this.setState({error: error.msg || error.toString(), progress_status: ''}); + } + + onRecoverSuccess() { + this.setState({success: true, progress_status: ''}); + } + + checkOldOwner(name, oldOwner) { + return api.getOwnerHistoryAsync(name).then(history => { + const res = history.filter(a => { + const owner = a.previous_owner_authority.key_auths[0][0]; + return owner === oldOwner; + }); + return res.length > 0; + }); + } + + requestAccountRecovery(name, oldPassword, newPassword) { + const old_owner_key = passwordToOwnerPubKey(name, oldPassword); + const new_owner_key = passwordToOwnerPubKey(name, newPassword); + const new_owner_authority = {weight_threshold: 1, account_auths: [], key_auths: [[new_owner_key, 1]]} + fetch('/api/v1/request_account_recovery', { + method: 'post', + mode: 'no-cors', + credentials: 'same-origin', + headers: { + Accept: 'application/json', + 'Content-type': 'application/json' + }, + body: JSON.stringify({csrf: $STM_csrf, name, old_owner_key, new_owner_key, new_owner_authority}) + }).then(r => r.json()).then(res => { + if (res.error) { + console.error('request_account_recovery server error (1)', res.error); + this.setState({error: res.error || tt('g.unknown'), progress_status: ''}); + } else { + this.setState({error: '', progress_status: tt('recoveraccountstep1_jsx.recovering_account') + '..'}); + this.props.recoverAccount(name, oldPassword, newPassword, this.onRecoverFailed, this.onRecoverSuccess); + } + }).catch(error => { + console.error('request_account_recovery server error (2)', error); + this.setState({error: (error.message ? error.message : error), progress_status: ''}); + }); + } + + onSubmit(e) { + e.preventDefault(); + const {oldPassword, newPassword} = this.state; + const name = this.props.account_to_recover; + const oldOwner = passwordToOwnerPubKey(name, oldPassword); + this.setState({progress_status: tt('recoveraccountstep1_jsx.checking_account_owner') + '..'}); + this.checkOldOwner(name, oldOwner).then(res => { + if (res) { + this.setState({progress_status: tt('recoveraccountstep1_jsx.sending_recovery_request') + '..'}); + this.requestAccountRecovery(name, oldPassword, newPassword); + } else { + this.setState({error: tt('recoveraccountstep1_jsx.cant_confirm_account_ownership'), progress_status: ''}); + } + }); + } + + render() { + if (!process.env.BROWSER) { // don't render this page on the server + return
    +
    + {tt('g.loading')}.. +
    +
    ; + } + const {account_to_recover} = this.props; + if (!account_to_recover) { + return + {tt('recoveraccountstep1_jsx.account_recovery_request_not_confirmed')} + ; + } + const {oldPassword, valid, error, progress_status, name_error, success} = this.state; + const submit_btn_class = 'button action' + (!valid || !oldPassword ? ' disabled' : ''); + + let submit = null; + if (progress_status) { + submit = {progress_status}; + } else { + if (success) { + // submit =

    Congratulations! Your account has been recovered. Please login using your new password.

    ; + window.location = `/login.html#account=${account_to_recover}&msg=accountrecovered`; + } else { + submit = ; + } + } + const disable_password_input = success || progress_status !== ''; + + return ( +
    +
    +
    +

    {tt('recoveraccountstep1_jsx.recover_account')}

    +
    +
    + +

    {name_error}

    +
    +
    +
    + +
    +
    + 0} /> +
    {error}
    +
    + {submit} + +
    +
    +
    + ); + } +} + +module.exports = { + path: 'recover_account_step_2', + component: connect( + state => { + return { + account_to_recover: state.offchain.get('recover_account'), + }; + }, + dispatch => ({ + recoverAccount: ( + account_to_recover, old_password, new_password, onError, onSuccess + ) => { + dispatch({type: 'transaction/RECOVER_ACCOUNT', + payload: {account_to_recover, old_password, new_password, onError, onSuccess} + }) + dispatch({type: 'user/LOGOUT'}) + }, + }) + )(RecoverAccountStep2) +}; diff --git a/src/app/components/pages/SubmitPost.jsx b/src/app/components/pages/SubmitPost.jsx new file mode 100644 index 0000000..7380e36 --- /dev/null +++ b/src/app/components/pages/SubmitPost.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { browserHistory } from 'react-router'; +import ReplyEditor from 'app/components/elements/ReplyEditor' + +const formId = 'submitStory'; +// const richTextEditor = process.env.BROWSER ? require('react-rte-image').default : null; +// const SubmitReplyEditor = ReplyEditor(formId, richTextEditor); +const SubmitReplyEditor = ReplyEditor(formId); + +class SubmitPost extends React.Component { + // static propTypes = { + // routeParams: React.PropTypes.object.isRequired, + // } + constructor() { + super() + this.success = (/*operation*/) => { + // const { category } = operation + localStorage.removeItem('replyEditorData-' + formId) + browserHistory.push('/created')//'/category/' + category) + } + } + render() { + const {success} = this + return ( +
    + +
    + ); + } +} + +module.exports = { + path: 'submit.html', + component: SubmitPost // connect(state => ({ global: state.global }))(SubmitPost) +}; diff --git a/src/app/components/pages/SubmitPostServerRender.jsx b/src/app/components/pages/SubmitPostServerRender.jsx new file mode 100644 index 0000000..1cc4952 --- /dev/null +++ b/src/app/components/pages/SubmitPostServerRender.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import tt from 'counterpart'; + +class SubmitPostServerRender extends React.Component { + render() { + return ( +
    + {tt('g.loading')}... +
    + ); + } +} + +module.exports = { + path: 'submit.html', + component: SubmitPostServerRender +}; diff --git a/src/app/components/pages/Support.jsx b/src/app/components/pages/Support.jsx new file mode 100644 index 0000000..93abe84 --- /dev/null +++ b/src/app/components/pages/Support.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import tt from 'counterpart'; +import { APP_NAME } from 'app/client_config'; + +class Support extends React.Component { + render() { + return ( +
    +
    +

    {tt('g.APP_NAME_support', {APP_NAME})}

    +

    + {tt('g.please_email_questions_to')} contact@steemit.com. +

    +
    +
    + ); + } +} + +module.exports = { + path: 'support.html', + component: Support +}; diff --git a/src/app/components/pages/TagsIndex.jsx b/src/app/components/pages/TagsIndex.jsx new file mode 100644 index 0000000..f0887a8 --- /dev/null +++ b/src/app/components/pages/TagsIndex.jsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { Link } from 'react-router'; +import {connect} from 'react-redux'; +import { browserHistory } from 'react-router'; +import { numberWithCommas } from 'app/utils/StateFunctions'; +import tt from 'counterpart'; + +export default class TagsIndex extends React.Component { + static propTypes = { + tagsAll: React.PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + this.state = {order: props.order || 'name'}; + this.onChangeSort = this.onChangeSort.bind(this) + } + + shouldComponentUpdate(nextProps, nextState) { + const res = this.props.tagsAll !== nextProps.tagsAll || + this.state !== nextState; + return res; + } + + onChangeSort = (e, order) => { + e.preventDefault() + this.setState({order}) + } + + compareTags = (a, b, type) => { + switch(type) { + case 'name': return a.get('name').localeCompare(b.get('name')); + case 'posts': return parseInt(a.get('top_posts')) <= parseInt(b.get('top_posts')) ? 1 : -1; + case 'comments': return parseInt(a.get('comments')) <= parseInt(b.get('comments')) ? 1 : -1; + case 'payouts': return parseInt(a.get('total_payouts')) <= parseInt(b.get('total_payouts')) ? 1 : -1; + } + } + + render() { + const {tagsAll} = this.props; + //console.log('-- TagsIndex.render -->', tagsAll.toJS()); + const {order} = this.state; + let tags = tagsAll; + + const rows = tags.filter( + // there is a blank tag present, as well as some starting with #. filter them out. + tag => /^[a-z]/.test(tag.get('name')) + ).sort((a,b) => { + return this.compareTags(a, b, order) + }).map(tag => { + const name = tag.get('name'); + const link = `/trending/${name}`; + return ( + + {name} + + {numberWithCommas(tag.get('top_posts').toString())} + {numberWithCommas(tag.get('comments').toString())} + {numberWithCommas(tag.get('total_payouts'))} + ); + }).toArray(); + + const cols = [ + ['name', tt('g.tag')], + ['posts', tt('g.posts')], + ['comments', tt('g.comments')], + ['payouts', tt('g.payouts')] + ].map( col => { + return + {order === col[0] + ? {col[1]} + : this.onChangeSort(e, col[0])}>{col[1]}} + + }) + + return ( +
    +
    +
    +

    {tt('g.trending_topics')}

    + + + + {cols} + + + + {rows} + +
    +
    +
    + ); + } +} + +module.exports = { + path: 'tags(/:order)', + component: connect(state => ({ + tagsAll: state.global.get('tags') + }))(TagsIndex) +}; diff --git a/src/app/components/pages/TagsIndex.scss b/src/app/components/pages/TagsIndex.scss new file mode 100644 index 0000000..9518a8c --- /dev/null +++ b/src/app/components/pages/TagsIndex.scss @@ -0,0 +1,26 @@ +.TagsIndex { + input { + margin-bottom: 0.5rem!important; + } + + table tr { + th a { + position: relative; + transition: 0.3s all ease-in-out; + @include font-size(17px); + } + th a:hover::after { + content: '\2193'; + position: absolute; + left: 100%; + padding-left: 4px; + } + + td, th { + padding-right: 20px; + text-align: right; + &:first-child {text-align: left;} + } + } +} + diff --git a/src/app/components/pages/Topics.jsx b/src/app/components/pages/Topics.jsx new file mode 100644 index 0000000..bd8160f --- /dev/null +++ b/src/app/components/pages/Topics.jsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { Link } from 'react-router'; +import {connect} from 'react-redux'; +import { browserHistory } from 'react-router'; +import tt from 'counterpart'; + +class Topics extends React.Component { + static propTypes = { + categories: React.PropTypes.object.isRequired, + order: React.PropTypes.string, + current: React.PropTypes.string, + className: React.PropTypes.string, + compact: React.PropTypes.bool + }; + + constructor(props) { + super(props); + this.state = {}; + } + + shouldComponentUpdate(nextProps, nextState) { + const res = this.props.categories !== nextProps.categories || + this.props.current !== nextProps.current || + this.props.order !== nextProps.order || this.state !== nextState; + return res; + } + + render() { + // console.log('Topics'); + const { + props: {order, current, compact, className}, + } = this; + let categories = this.props.categories.get('trending'); + categories = categories.take(50); + + const cn = 'Topics' + (className ? ` ${className}` : ''); + const currentValue = `/${order}/${current}`; + + if (compact) { + return ; + } + + categories = categories.map(cat => { + const link = order ? `/${order}/${cat}` : `/hot/${cat}`; + return (
  • + {cat} +
  • ); + }); + return ( +
    +
    +

    {tt('g.tags_and_topics')}

    +
    +
    +
      + {categories} +
    • + {tt('g.show_more_topics')}.. +
    • +
    +
    +
    + ); + } +} + +export default connect(state => ({ + categories: state.global.get('tag_idx') +}))(Topics); + diff --git a/src/app/components/pages/Topics.scss b/src/app/components/pages/Topics.scss new file mode 100644 index 0000000..73ed610 --- /dev/null +++ b/src/app/components/pages/Topics.scss @@ -0,0 +1,33 @@ +ul.Topics { + max-width: 10rem; + > li { + list-style-type: none; + } + > li > a.active { + font-weight: bold; + overflow: hidden; + } + .show-more { + font-size: 0.9rem; + font-weight: bold; + color: $dark-gray; + } +} + +select.Topics { + margin-bottom: 1rem !important; +} + +.Topics__title { + font-weight: bold; + color: $dark-gray; +} + +.Topics__filter { + margin: 0.5rem 0; + input { + padding-top: 0.1rem; + padding-bottom: 0.1rem; + border-color: $light-gray; + } +} diff --git a/src/app/components/pages/Tos.jsx b/src/app/components/pages/Tos.jsx new file mode 100644 index 0000000..7c2fc62 --- /dev/null +++ b/src/app/components/pages/Tos.jsx @@ -0,0 +1,134 @@ +import React from 'react'; + +class Tos extends React.Component { + render() { + return ( +
    +
    +

    Steemit Terms of Service

    +

    Last Updated April 28, 2016

    Welcome to Steemit! These Terms of Service (“Terms”) apply to your access to and use of Steemit.com and any other products or services that link to these Terms (“Steemit”). Steemit is provided by Steemit, Inc. (“Steemit”, “we” + or “us”). By accessing or using Steemit, you agree to be bound by these Terms. If you do not agree to these Terms, including the mandatory arbitration provision and class action waiver in Section 14, do not access or use Steemit. If we make changes to these Terms, we will provide notice of those changes by updating the “Last Updated” date above or posting notice on Steemit. Your continued use of Steemit will confirm your acceptance of the changes. +

    You understand and agree that these Terms apply solely to your access to, and use of, Steemit and that, when you use other Steemit services such as Steemit.com or Steemitgifts.com, the terms and policies particular to those services apply. +

    1. Privacy Policy

    Please refer to our Privacy Policy for information about how we collect, use and disclose information about you. +

    2. Eligibility

    Steemit is not targeted towards, nor intended for use by, anyone under the age of 13. You must be at least 13 years of age to access or use Steemit. If you are between 13 and 18 years of age (or the age of legal majority where you reside), you may only access or use Steemit under the supervision of a parent or legal guardian who agrees to be bound by these Terms. +

    3. Copyright and Limited License

    Steemit contains data, text, photographs, images, video, audio, graphics, articles, comments, software, code, scripts and other content supplied by us, the Steem blockchain or our licensors, which we call “Steemit Content.” Steemit Content is protected by intellectual property laws, including copyright and other proprietary rights of the United States and foreign countries. Except as explicitly stated in these Terms, Steemit does not grant any express or implied rights to use Steemit Content. +

    You are granted a limited, nonexclusive, non-transferable, and non-sublicensable license to access and use Steemit and Steemit Content for your personal, non-commercial use. This license is subject to these Terms and does not include any right to: (a) distribute, publicly perform or publicly display any Steemit Content; (b) modify or otherwise make any derivative uses of Steemit or Steemit Content, or any portion thereof; (c) use any data mining, robots or similar data gathering or extraction methods; and (d) use Steemit or Steemit Content other than for their intended purposes. Any use of Steemit or Steemit Content other than as authorized in these Terms is strictly prohibited and will terminate the license granted herein. This license is revocable at any time. +

    4. Adult-Oriented Content

    Steemit is intended for a general audience and, as a result, some Steemit Content may discuss or depict adult-oriented topics. We realize that this content may not be appropriate or desirable for some of our readers depending on their current location, age, background or personal views. As a result, we mark this content as Not Safe For Work (“NSFW”).

    Marking Steemit Content as NSFW does not prevent you from being able to access this content but, instead, helps you make informed decisions about the type of content you view on Steemit. You understand and agree that you access content marked as NSFW at your own risk. +

    5. Trademarks

    “Steem,” “ + Steemit,” the Steemit logo and any other product or service names, logos or slogans that may appear on Steemit are trademarks of Steemit and may not be copied, imitated or used, in whole or in part, without our prior written permission. You may not use any metatags or other “ + hidden text” utilizing “Steemit” or any other name, trademark or product or service name of Steemit without our prior written permission. In addition, the look and feel of Steemit, including, without limitation, all page headers, custom graphics, button icons and scripts, constitute the service mark, trademark or trade dress of Steemit and may not be copied, imitated or used, in whole or in part, without our prior written permission. All other trademarks, registered trademarks, product names and company names or logos mentioned or used on Steemit are the property of their respective owners and may not be copied, imitated or used, in whole or in part, without the permission of the applicable trademark holder. Reference to any products, services, processes or other information by name, trademark, manufacturer, supplier or otherwise does not constitute or imply endorsement, sponsorship or recommendation by Steemit.                                         +

    6. Assumption of Risk, Limitations on Liability & Indemnity

    6.1.   + You accept and acknowledge that there are risks associated with utilizing an Internet- based STEEM account service including, but not limited to, the risk of failure of hardware, software and Internet connections, the risk of malicious software introduction, and the risk that third parties may obtain unauthorized access to information stored within your Account, including, but not limited to your Private Key (as defined below at 10.2.). You accept and acknowledge that Steemit will not be responsible for any communication failures, disruptions, errors, distortions or delays you may experience when using the Services, however caused.         +

    6.2.  We will use reasonable endeavours to verify the accuracy of any information on the Service but we make no representation or warranty of any kind, express or implied, statutory or otherwise, regarding the contents of the Service, information and functions made accessible through the Service, any hyperlinks to third party websites, nor for any breach of security associated with the transmission of information through the Service or any website linked to by the Service.                                                 +

    6.3.  We will not be responsible or liable to you for any loss and take no responsibility for and will not be liable to you for any use of our Services, including but not limited to any losses, damages or claims arising from: (a) User error such as forgotten passwords, incorrectly constructed transactions, or mistyped STEEM addresses; (b) Server failure or data loss; (c) Corrupted Account files; (d) Unauthorized access to applications; (e) Any unauthorized third party activities, including without limitation the use of viruses, phishing, bruteforcing or other means of attack against the Service or Services.                                                                 +

    6.4.  We make no warranty that the Service or the server that makes it available, are free of viruses or errors, that its content is accurate, that it will be uninterrupted, or that defects will be corrected. We will not be responsible or liable to you for any loss of any kind, from action taken, or taken in reliance on material, or information, contained on the Service.                                         +

    6.5.  Subject to 7.1 below, any and all indemnities, warranties, terms and conditions (whether express or implied) are hereby excluded to the fullest extent permitted under Luxembourg law. +

    6.6.  We will not be liable, in contract, or tort (including, without limitation, negligence), other than where we have been fraudulent or made negligent misrepresentations. +

    6.7.  Nothing in these Terms excludes or limits liability for death or personal injury caused by negligence, fraudulent misrepresentation, or any other liability which may not otherwise be limited or excluded under United States law. +

    7. Agreement to Hold Steemit Harmless

    7.1.  You agree to hold harmless Steemit (and each of our officers, directors, members, employees, agents and affiliates) from any claim, demand, action, damage, loss, cost or expense, including without limitation reasonable legal fees, arising out or relating to:         +

    7.1.1.  Your use of, or conduct in connection with, our Services;

    7.1.2.  Any feedback or submissions you provide (see 19 below);

    7.1.3.   + Your violation of these Terms; or

    7.1.4.  Violation of any rights of any other person or entity. +

    7.2.  If you are obligated to indemnify us, we will have the right, in our sole discretion, to control any action or proceeding (at our expense) and determine whether we wish to settle it.                                                                                  +

    8. No Liability For Third Party Services And Content

    8.1. In using our Services, you may view content or utilize services provided by third parties, including links to web pages and services of such parties (“ + Third Party Content”). We do not control, endorse or adopt any Third-Party Content and will have no responsibility for Third Party Content including, without limitation, material that may be misleading, incomplete, erroneous, offensive, indecent or otherwise objectionable in your jurisdiction. In addition, your dealings or correspondence with such third parties are solely between you and the third parties. We are not responsible or liable for any loss or damage of any sort incurred as a result of any such dealings and you understand that your use of Third Party Content, and your interactions with third parties, is at your own risk.                                 +

    9. Account Registration        

    9.1.  You need not use a Steemit Account. If you wish to use an Account, you must create an Account with Steemit to access the Services (“ + Account”). When you create an Account, you are strongly advised to take the following precautions, as failure to do so may result in loss of access to, and/or control over, your Wallet: (a) Create a strong password that you do not use for any other website or online service; (b) Provide accurate and truthful information; (c) Maintain and promptly update your Account information; (d) maintain the security of your Account by protecting your Account password and access to your computer and your Account; (e) Promptly notify us if you discover or otherwise suspect any security breaches related to your Account.                 +

    9.2.  You hereby accept and acknowledge that you take responsibility for all activities that occur under your Account and accept all risks of any authorized or unauthorized access to your Account, to the maximum extent permitted by law.                                                                                                  +

    10. The Steemit Services                 +

    10.1.  As described in more detail below, the Services, among other things, provide in-browser (or otherwise local) software that (a) generates and stores STEEM ACCOUNT Addresses and encrypted Private Keys (defined below), and (b) Facilitates the submission of STEEM transaction data to the Steem network without requiring you to access the STEEM command line interface.         +

    10.2.  Account Names and Private Keys. When you create an Account, the Services generate and store a cryptographic private and public key pair that you may use to send and receive STEEM and Steem Dollars via the Steem network.. The Private Key uniquely matches the Account Name and must be used in connection with the Account Name to authorize the transfer of STEEM and Steem Dollars from that Account. You are solely responsible for maintaining the security of your Private Key and any password phrase associated with your wallet. You must keep your Account, password phrase and Private Key access information secure. Failure to do so may result in the loss of control of Steem, Steem Power and Steem Dollars associated with the Wallet. +

    10.3.  No Password Retrieval. Steemit does not receive or store your Account password, nor the unencrypted keys and addresses. Therefore, we cannot assist you with Account password retrieval. Our Services provide you with tools to help you remember or recover your password, including by allowing you to set password hints, but the Services cannot generate a new password for your Account. You are solely responsible for remembering your Account password. If you have not safely stored a backup of any Account Names and password pairs maintained in your Account, you accept and acknowledge that any STEEM, Steem Dollars and Steem Power you have associated with such Account will become inaccessible if you do not have your Account password. +

    10.4.  Transactions. In order to be completed, all proposed Steem transactions must be confirmed and recorded in the Steem public ledger via the Steem distributed consensus network (a peer-to-peer economic network that operates on a cryptographic protocol), which is not owned, controlled or operated by Steemit. The Steem Network is operated by a decentralized network of independent third parties. Blockchain has no control over the Steem network and therefore cannot and does not ensure that any transaction details you submit via the Services will be confirmed via the Steem network. You acknowledge and agree that the transaction details you submit via the Services may not be completed, or may be substantially delayed, by the Steem network. You may use the Services to submit these details to the network.                                                          +

    10.5.  No Storage or Transmission of STEEM, Steem Dollars or Steem Power. Steem, in any of its forms (STEEM, Steem Dollars and Steem Power) is an intangible, digital asset. They exist only by virtue of the ownership record maintained in the Steem network. The Service does not store, send or receive Steem. Any transfer of title that might occur in any STEEM, Steem Dollars or Steem Power occurs on the decentralized ledger within the Steem network and not within the Services. We do not guarantee that the Service can effect the transfer of title or right in any Steem, Steem Dollars or Steem Power. +

    10.6.  Relationship. Nothing in these Terms is intended to nor shall create any partnership, joint venture, agency, consultancy or trusteeship, you and Steemit being with respect to one another independent contractors. +

    10.7.  Accuracy of Information. You represent and warrant that any information you provide via the Services is accurate and complete. You accept and acknowledge that Steemit is not responsible for any errors or omissions that you make in connection with any Steem transaction initiated via the Services, for instance, if you mistype an Account Name or otherwise provide incorrect information. We strongly encourage you to review your transaction details carefully before completing them via the Services. +

    10.8.  No Cancellations or Modifications. Once transaction details have been submitted to the Steem network via the Services, The Services cannot assist you to cancel or otherwise modify your transaction details. Steemit has no control over the Steem Network and does not have the ability to facilitate any cancellation or modification requests. +

    10.9.  Taxes. It is your responsibility to determine what, if any, taxes apply to the transactions you for which you have submitted transaction details via the Services, and it is your responsibility to report and remit the correct tax to the appropriate tax authority. You agree that Steemit is not responsible for determining whether taxes apply to your Steem transactions or for collecting, reporting, withholding or remitting any taxes arising from any Steem transactions.                                                                                                  +

    11. Fees for Using the Steemit Services

    11.1. Company Fees Creating an Account. Steemit does not currently charge fees for any Services, however we reserve the right to do so in future, and in such case any applicable fees will be displayed prior to you using any Service to which a fee applies. +

    12. No Right To Cancel And/Or Reverse Steem Transactions

    14.1. If you use a Service to which Steem, Steem Dollars or Steem Power is transacted, you will not be able to change your mind once you have confirmed that you wish to proceed with the Service or transaction. +

    15. Discontinuation of Services                                                          +

    15.1.  We may, in our sole discretion and without cost to you, with or without prior notice and at any time, modify or discontinue, temporarily or permanently, any portion of our Services. You are solely responsible for storing, outside of the Services, a backup of any Account and Private Key pair that you maintain in your Wallet.                                 +

    15.2.  If you do not maintain a backup of your Account data outside of the Services, you will be may not be able to access Steem, Steem Dollars and Steem Power associated with any Account Name maintained in your Account in the event that we discontinue or deprecate the Services. +

    16. Suspension or Termination of Service.

    16.1. We may suspend or terminate your access to the Services in our sole discretion, immediately and without prior notice, and delete or deactivate your Account and all related information and files in such without cost to you, including, for instance, in the event that you breach any term of these Terms. In the event of termination, your access to funds will depend on your access to your backup of your Account data including your Account Name and Private Keys. +

    17. User Conduct

    17.1. When accessing or using the Services, you agree that you will not commit any unlawful act, and that you are solely responsible for your conduct while using our Services. Without limiting the generality of the foregoing, you agree that you will not: +

    17.1.1. Use our Services in any manner that could interfere with, disrupt, negatively affect or inhibit other users from fully enjoying our Services, or that could damage, disable, overburden or impair the functioning of our Services in any manner;         +

    17.1.2.  Use our Services to pay for, support or otherwise engage in any illegal activities, including, but not limited to illegal gambling, fraud, money- laundering, or terrorist activities. +

    17.1.3.  Use any robot, spider, crawler, scraper or other automated means or interface not provided by us to access our Services or to extract data;                                 +

    17.1.4.  Use or attempt to use another user’s Wallet without authorization;

    17.1.5.  Attempt to circumvent any content filtering techniques we employ, or attempt to access any service or area of our Services that you are not authorized to access; +

    17.1.6.   + Introduce to the Services any virus, Trojan, worms, logic bombs or other harmful material;

    17.1.7.   + Develop any third-party applications that interact with our Services without our prior written consent;                                 +

    17.1.8.   + Provide false, inaccurate, or misleading information; or                 +

    17.1.9.  Encourage or induce any third party to engage in any of the activities prohibited under this Section.          +

    17.1.10.  Reverse engineer any aspect of Steemit or do anything that might discover source code or bypass or circumvent measures employed to prevent or limit access to any Steemit Content, area or code of Steemit. +

    18. Third-Party Content and Sites

    + Steemit may include links and other content owned or operated by third parties, including advertisements and social “ + widgets” (we call these “Third-Party Content“). You agree that Steemit is not responsible or liable for Third-Party Content and that you access and use Third-Party Content at your own risk. Your interactions with Third-Party Content are solely between you and the third party providing the content. When you leave Steemit, you should understand that these Terms no longer govern and that the terms and policies of those third-party sites or services will then apply. +

    19. Feedback

    You may submit questions, comments, feedback, suggestions, and other information regarding Steemit (we call this “Feedback“). You acknowledge and agree that Feedback is non-confidential and will become the sole property of Steemit. Steemit shall own exclusive rights, including, without limitation, all intellectual property rights, in and to such Feedback and is entitled to the unrestricted use and dissemination of this Feedback for any purpose, without acknowledgment or compensation to you. You agree to execute any documentation required by Steemit to confirm such assignment to Steemit. +

    20. Copyright Complaints

    Steemit respects the intellectual property of others by not reading infringed content from the Steem blockchain. If you believe that your work has been copied in a way that constitutes copyright infringement, you may notify Steemit’ + s Designated Agent by contacting:

    contact@steemit.com

    Please see 17 + U.S.C. §512(c)(3) for the requirements of a proper notification. You should note that if you knowingly misrepresent in your notification that the material or activity is infringing, you may be liable for any damages, including costs and attorneys’ + fees, incurred by Steemit or the alleged infringer, as the result of Steemit’s relying upon such misrepresentation in removing or disabling access to the material or activity claimed to be infringing. +

    21. Disclaimers

    TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, STEEMIT AND THE STEEMIT CONTENT ARE PROVIDED ON AN “ + AS IS” AND “AS AVAILABLE” BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT AND ANY WARRANTIES IMPLIED BY ANY COURSE OF PERFORMANCE OR USAGE OF TRADE. STEEMIT DOES NOT REPRESENT OR WARRANT THAT STEEMIT AND THE STEEMIT CONTENT: (A) WILL BE SECURE OR AVAILABLE AT ANY PARTICULAR TIME OR LOCATION; (B) ARE ACCURATE, COMPLETE, RELIABLE, CURRENT OR ERROR-FREE OR THAT ANY DEFECTS OR ERRORS WILL BE CORRECTED; AND (C) ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS. YOUR USE OF STEEMIT AND THE STEEMIT CONTENT IS SOLELY AT YOUR OWN RISK. SOME JURISDICTIONS DO NOT ALLOW THE DISCLAIMER OF IMPLIED TERMS IN CONTRACTS WITH CONSUMERS, SO SOME OR ALL OF THE DISCLAIMERS IN THIS SECTION MAY NOT APPLY TO YOU. +

    22. Limitation of Liability

    TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL STEEMIT OR THE STEEMIT PARTIES BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, EXEMPLARY OR PUNITIVE DAMAGES, OR ANY OTHER DAMAGES OF ANY KIND, INCLUDING, BUT NOT LIMITED TO, LOSS OF USE, LOSS OF PROFITS OR LOSS OF DATA, WHETHER IN AN ACTION IN CONTRACT, TORT (INCLUDING, BUT NOT LIMITED TO, NEGLIGENCE) OR OTHERWISE, ARISING OUT OF, OR IN ANY WAY CONNECTED WITH, THE USE OF, OR INABILITY TO USE, STEEMIT OR THE STEEMIT CONTENT. TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE AGGREGATE LIABILITY OF STEEMIT OR THE STEEMIT PARTIES, WHETHER IN CONTRACT, WARRANTY, TORT (INCLUDING NEGLIGENCE, WHETHER ACTIVE, PASSIVE OR IMPUTED), PRODUCT LIABILITY, STRICT LIABILITY OR OTHER THEORY, ARISING OUT OF OR RELATING TO: (A) THE USE OF OR INABILITY TO USE STEEMIT OR THE STEEMIT CONTENT; OR (B) THESE TERMS EXCEED ANY COMPENSATION YOU PAY, IF ANY, TO STEEMIT FOR ACCESS TO OR USE OF STEEMIT. +

    SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES, SO SOME OR ALL OF THE EXCLUSIONS AND LIMITATIONS IN THIS SECTION MAY NOT APPLY TO YOU. +

    23. Modifications to Steemit

    Steemit reserves the right to modify or discontinue, temporarily or permanently, Steemit, or any features or portions of Steemit, without prior notice. You agree that Steemit will not be liable for any modification, suspension or discontinuance of Steemit, or any part of Steemit. +

    24. Arbitration

    PLEASE READ THE FOLLOWING SECTION CAREFULLY BECAUSE IT REQUIRES YOU TO ARBITRATE CERTAIN DISPUTES WITH STEEMIT AND LIMITS THE MANNER IN WHICH YOU CAN SEEK RELIEF FROM STEEMIT. +

    24.1. Binding Arbitration

    Except for disputes in which either party seeks to bring an individual action in small claims court or seeks injunctive or other equitable relief for the alleged unlawful use of copyrights, trademarks, trade names, logos, trade secrets or patents, you and Steemit: (a) waive your right to have any and all disputes or Claims arising from these Terms or Steemit (collectively, “Disputes“) resolved in a court; and (b) waive your right to a jury trial. Instead, you and Steemit will arbitrate Disputes through binding arbitration (which is the referral of a Dispute to one or more persons charged with reviewing the Dispute and making a final and binding determination to resolve it, instead of having the Dispute decided by a judge or jury in court). +

    24.2. No Class Arbitrations, Class Actions or Representative Actions

    YOU AND STEEMIT AGREE THAT ANY DISPUTE IS PERSONAL TO YOU AND STEEMIT AND THAT ANY SUCH DISPUTE WILL BE RESOLVED SOLELY THROUGH INDIVIDUAL ARBITRATION AND WILL NOT BE BROUGHT AS A CLASS ARBITRATION, CLASS ACTION OR ANY OTHER TYPE OF REPRESENTATIVE PROCEEDING. NEITHER PARTY AGREES TO CLASS ARBITRATION OR TO AN ARBITRATION IN WHICH AN INDIVIDUAL ATTEMPTS TO RESOLVE A DISPUTE AS A REPRESENTATIVE OF ANOTHER INDIVIDUAL OR GROUP OF INDIVIDUALS. FURTHER, YOU AND STEEMIT AGREE THAT A DISPUTE CANNOT BE BROUGHT AS A CLASS, OR OTHER TYPE OF REPRESENTATIVE ACTION, WHETHER WITHIN OR OUTSIDE OF ARBITRATION, OR ON BEHALF OF ANY OTHER INDIVIDUAL OR GROUP OF INDIVIDUALS. +

    24.3. Federal Arbitration Act

    You and Steemit agree that these Terms affect interstate commerce and that the enforceability of this Section 14 shall be governed by, construed and enforced, both substantively and procedurally, by the Federal Arbitration Act, 9 U.S.C. § + 1 et seq. (the “FAA”) to the maximum extent permitted by applicable law.

    24.4. Process +

    You and Steemit agree that you will notify each other in writing of any Dispute within thirty (30) days of when it arises so that the parties can attempt, in good faith, to resolve the Dispute informally. Notice to Steemit shall be provided by filling out the form available at https://steemit.com/contact/ or sending an email to contact@Steemit.com. Your notice must include: (1) your name, postal address and email address; (2) a description of the nature or basis of the Dispute; and (3) the specific relief that you are seeking. If you and Steemit cannot agree how to resolve the Dispute within thirty (30) days of Steemit receiving the notice, either you or Steemit may, as appropriate pursuant to this Section 14, commence an arbitration proceeding or file a claim in court. You and Steemit agree that any arbitration or claim must be commenced or filed within one (1) year after the Dispute arose; otherwise, you and Steemit agree that the claim is permanently barred (which means that you will no longer have the right to assert a claim regarding the Dispute). You and Steemit agree that: (a) any arbitration will occur in San Francisco County, California; (b) arbitration will be conducted confidentially by a single arbitrator in accordance with the rules of JAMS; and (c) the state or federal courts in California will have exclusive jurisdiction over the enforcement of an arbitration award and over any Dispute between the parties that is not subject to arbitration. You may also litigate a Dispute in small claims court located in the county where you reside if the Dispute meets the requirements to be heard in small claims court. +

    24.5. Authority of Arbitrator

    As limited by the FAA, these Terms and applicable JAMS rules, the arbitrator will have: (a) the exclusive authority and jurisdiction to make all procedural and substantive decisions regarding a Dispute; and (b) the authority to grant any remedy that would otherwise be available in court. The arbitrator may only conduct an individual arbitration and may not consolidate more than one individual’ + s claims, preside over any type of class or representative proceeding or preside over any proceeding involving more than one individual. +

    24.6. Rules of JAMS

    The rules of, and additional information about, JAMS are available on the JAMS website at http://www.jamsadr.com/, as may be updated from time to time. By agreeing to be bound by these Terms, you either: (a) acknowledge and agree that you have read and understand the rules of JAMS; or (b) waive your opportunity to read the rules of JAMS and any claim that the rules of JAMS are unfair or should not apply for any reason. +

    24.7. Severability

    If any term, clause or provision of this Section 14 is held invalid or unenforceable, it will be so held to the minimum extent required by law and all other terms, clauses or provisions will remain valid and enforceable. Further, the waivers set forth in Section 24.2 are severable from the other provisions of these Terms and will remain valid and enforceable, except as prohibited by applicable law. +

    25. Applicable Law and Venue

    These Terms and your access to and use of Steemit and the Steemit Content will be governed by, and construed in accordance with, the laws of California, without resort to its conflict of law provisions. To the extent the arbitration provision in Section 14 does not apply and the Dispute cannot be heard in small claims court, you agree that any action at law or in equity arising out of, or relating to, these Terms shall be filed only in the state and federal courts located in San Francisco County, California and you hereby irrevocably and unconditionally consent and submit to the exclusive jurisdiction of such courts over any suit, action or proceeding arising out of these Terms. +

    26. Termination

    Steemit reserves the right, without notice and in our sole discretion, to terminate your license to access and use Steemit and to block or prevent your future access to, and use of, Steemit. +

    27. Severability

    If any term, clause or provision of these Terms is deemed to be unlawful, void or for any reason unenforceable, then that term, clause or provision shall be deemed severable from these Terms and shall not affect the validity and enforceability of any remaining provisions. +

    28. Questions & Contact Information

    Questions or comments about Steemit may be directed to contact@steemit.com

    +
    +
    + ); + } +} + +module.exports = { + path: 'tos.html', + component: Tos +}; diff --git a/src/app/components/pages/Tos.scss b/src/app/components/pages/Tos.scss new file mode 100644 index 0000000..10fc2d2 --- /dev/null +++ b/src/app/components/pages/Tos.scss @@ -0,0 +1,7 @@ +.Tos { + max-width: 800px; + padding: 1.5em 0 3em; + .c1.h { + font-weight: 600; + } +} diff --git a/src/app/components/pages/UserProfile.jsx b/src/app/components/pages/UserProfile.jsx new file mode 100644 index 0000000..7e80c2a --- /dev/null +++ b/src/app/components/pages/UserProfile.jsx @@ -0,0 +1,495 @@ +/* eslint react/prop-types: 0 */ +import React from 'react'; +import { Link } from 'react-router'; +import {connect} from 'react-redux'; +import { browserHistory } from 'react-router'; +import transaction from 'app/redux/Transaction'; +import user from 'app/redux/User'; +import Icon from 'app/components/elements/Icon' +import UserKeys from 'app/components/elements/UserKeys'; +import PasswordReset from 'app/components/elements/PasswordReset'; +import UserWallet from 'app/components/modules/UserWallet'; +import Settings from 'app/components/modules/Settings'; +import CurationRewards from 'app/components/modules/CurationRewards'; +import AuthorRewards from 'app/components/modules/AuthorRewards'; +import UserList from 'app/components/elements/UserList'; +import Follow from 'app/components/elements/Follow'; +import LoadingIndicator from 'app/components/elements/LoadingIndicator'; +import PostsList from 'app/components/cards/PostsList'; +import {isFetchingOrRecentlyUpdated} from 'app/utils/StateFunctions'; +import {repLog10} from 'app/utils/ParsersAndFormatters.js'; +import Tooltip from 'app/components/elements/Tooltip'; +import { LinkWithDropdown } from 'react-foundation-components/lib/global/dropdown'; +import VerticalMenu from 'app/components/elements/VerticalMenu'; +import MarkNotificationRead from 'app/components/elements/MarkNotificationRead'; +import NotifiCounter from 'app/components/elements/NotifiCounter'; +import DateJoinWrapper from 'app/components/elements/DateJoinWrapper'; +import tt from 'counterpart'; +import WalletSubMenu from 'app/components/elements/WalletSubMenu'; +import Userpic from 'app/components/elements/Userpic'; +import Callout from 'app/components/elements/Callout'; +import normalizeProfile from 'app/utils/NormalizeProfile'; +import userIllegalContent from 'app/utils/userIllegalContent'; +import proxifyImageUrl from 'app/utils/ProxifyUrl'; + +export default class UserProfile extends React.Component { + constructor() { + super() + this.state = {} + this.onPrint = () => {window.print()} + this.loadMore = this.loadMore.bind(this); + } + + shouldComponentUpdate(np) { + const {follow} = this.props; + const {follow_count} = this.props; + + let followersLoading = false, npFollowersLoading = false; + let followingLoading = false, npFollowingLoading = false; + + const account = np.routeParams.accountname.toLowerCase(); + if (follow) { + followersLoading = follow.getIn(['getFollowersAsync', account, 'blog_loading'], false); + followingLoading = follow.getIn(['getFollowingAsync', account, 'blog_loading'], false); + } + if (np.follow) { + npFollowersLoading = np.follow.getIn(['getFollowersAsync', account, 'blog_loading'], false); + npFollowingLoading = np.follow.getIn(['getFollowingAsync', account, 'blog_loading'], false); + } + + return ( + np.current_user !== this.props.current_user || + np.accounts.get(account) !== this.props.accounts.get(account) || + np.wifShown !== this.props.wifShown || + np.global_status !== this.props.global_status || + ((npFollowersLoading !== followersLoading) && !npFollowersLoading) || + ((npFollowingLoading !== followingLoading) && !npFollowingLoading) || + np.loading !== this.props.loading || + np.location.pathname !== this.props.location.pathname || + np.routeParams.accountname !== this.props.routeParams.accountname || + np.follow_count !== this.props.follow_count + ) + } + + componentWillUnmount() { + this.props.clearTransferDefaults() + this.props.clearPowerdownDefaults() + } + + loadMore(last_post, category) { + const {accountname} = this.props.routeParams + if (!last_post) return; + + let order; + switch(category) { + case 'feed': order = 'by_feed'; break; + case 'blog': order = 'by_author'; break; + case 'comments': order = 'by_comments'; break; + case 'recent_replies': order = 'by_replies'; break; + default: console.log('unhandled category:', category); + } + + if (isFetchingOrRecentlyUpdated(this.props.global_status, order, category)) return; + const [author, permlink] = last_post.split('/'); + this.props.requestData({author, permlink, order, category, accountname}); + } + + render() { + const { + props: {current_user, wifShown, global_status, follow}, + onPrint + } = this; + let { accountname, section } = this.props.routeParams; + // normalize account from cased params + accountname = accountname.toLowerCase(); + const username = current_user ? current_user.get('username') : null + // const gprops = this.props.global.getIn( ['props'] ).toJS(); + if( !section ) section = 'blog'; + + // @user/"posts" is deprecated in favor of "comments" as of oct-2016 (#443) + if( section == 'posts' ) section = 'comments'; + + // const isMyAccount = current_user ? current_user.get('username') === accountname : false; + + // Loading status + const status = global_status ? global_status.getIn([section, 'by_author']) : null; + const fetching = (status && status.fetching) || this.props.loading; + + let account + let accountImm = this.props.accounts.get(accountname); + if( accountImm ) { + account = accountImm.toJS(); + } else if (fetching) { + return
    ; + } else { + return
    {tt('user_profile.unknown_account')}
    + } + const followers = follow && follow.getIn(['getFollowersAsync', accountname]); + const following = follow && follow.getIn(['getFollowingAsync', accountname]); + + // instantiate following items + let totalCounts = this.props.follow_count; + let followerCount = 0; + let followingCount = 0; + + if (totalCounts && accountname) { + totalCounts = totalCounts.get(accountname); + if (totalCounts) { + totalCounts = totalCounts.toJS(); + followerCount = totalCounts.follower_count; + followingCount = totalCounts.following_count; + } + } + + const rep = repLog10(account.reputation); + + const isMyAccount = username === account.name + let tab_content = null; + + // const global_status = this.props.global.get('status'); + + + // let balance_steem = parseFloat(account.balance.split(' ')[0]); + // let vesting_steem = vestingSteem(account, gprops).toFixed(2); + // const steem_balance_str = numberWithCommas(balance_steem.toFixed(2)) + " STEEM"; + // const power_balance_str = numberWithCommas(vesting_steem) + " STEEM POWER"; + // const sbd_balance = parseFloat(account.sbd_balance) + // const sbd_balance_str = numberWithCommas('$' + sbd_balance.toFixed(2)); + + let rewardsClass = "", walletClass = ""; + if( section === 'transfers' ) { + walletClass = 'active' + tab_content =
    + + {isMyAccount &&
    } +
    ; + } + else if( section === 'curation-rewards' ) { + rewardsClass = "active"; + tab_content = + } + else if( section === 'author-rewards' ) { + rewardsClass = "active"; + tab_content = + } + else if( section === 'followers' ) { + if (followers && followers.has('blog_result')) { + tab_content =
    + + {isMyAccount && } +
    + } + } + else if( section === 'followed' ) { + if (following && following.has('blog_result')) { + tab_content = + } + } + else if( section === 'settings' ) { + tab_content = + } + else if( section === 'comments' && account.post_history ) { + if( account.comments ) + { + let posts = accountImm.get('posts') || accountImm.get('comments'); + if (!fetching && (posts && !posts.size)) { + tab_content = {tt('user_profile.user_hasnt_made_any_posts_yet', {name: accountname})}; + } else { + tab_content = ( + + ); + } + } + else { + tab_content = (
    ); + } + } else if(!section || section === 'blog') { + if (account.blog) { + let posts = accountImm.get('blog'); + const emptyText = isMyAccount ?
    + {tt('user_profile.looks_like_you_havent_posted_anything_yet')}

    + {tt('user_profile.create_a_post')}
    + {tt('user_profile.explore_trending_articles')}
    + {tt('user_profile.read_the_quick_start_guide')}
    + {tt('user_profile.browse_the_faq')}
    +
    : + tt('user_profile.user_hasnt_started_bloggin_yet', {name: accountname}); + + if (!fetching && (posts && !posts.size)) { + tab_content = {emptyText}; + } else { + tab_content = ( + + ); + } + } else { + tab_content = (
    ); + } + } + else if( (section === 'recent-replies')) { + if (account.recent_replies) { + let posts = accountImm.get('recent_replies'); + if (!fetching && (posts && !posts.size)) { + tab_content = {tt('user_profile.user_hasnt_had_any_replies_yet', {name: accountname}) + '.'}; + } else { + tab_content = ( +
    + + {isMyAccount && } +
    + ); + } + } else { + tab_content = (
    ); + } + } + else if( section === 'permissions' && isMyAccount ) { + walletClass = 'active' + tab_content =
    +
    +
    + +
    +
    +
    + + {isMyAccount && } +
    ; + } else if( section === 'password' ) { + walletClass = 'active' + tab_content =
    +
    +
    + +
    +
    +
    + +
    + } else { + // console.log( "no matches" ); + } + + // detect illegal users + if (userIllegalContent.includes(accountname)) { + tab_content =
    Unavailable For Legal Reasons.
    ; + } + + if (!(section === 'transfers' || section === 'permissions' || section === 'password')) { + tab_content =
    +
    +
    + {tab_content} +
    +
    +
    ; + } + + let printLink = null; + if( section === 'permissions' ) { + if(isMyAccount && wifShown) { + printLink = + } + } + + // const wallet_tab_active = section === 'transfers' || section === 'password' || section === 'permissions' ? 'active' : ''; // className={wallet_tab_active} + + let rewardsMenu = [ + {link: `/@${accountname}/curation-rewards`, label: tt('g.curation_rewards'), value: tt('g.curation_rewards')}, + {link: `/@${accountname}/author-rewards`, label: tt('g.author_rewards'), value: tt('g.author_rewards')} + ]; + + // set account join date + let accountjoin = account.created; + + const top_menu =
    +
    +
      +
    • {tt('g.blog')}
    • +
    • {tt('g.comments')}
    • +
    • + {tt('g.replies')} {isMyAccount && } +
    • + {/*
    • Feed
    • */} +
    • + + } + > + + {tt('g.rewards')} + + + +
    • +
    +
    + +
    ; + + const {name, location, about, website, cover_image} = normalizeProfile(account); + const website_label = website ? website.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '') : null + + let cover_image_style = {} + if(cover_image) { + cover_image_style = {backgroundImage: "url(" + proxifyImageUrl(cover_image, '2048x512') + ")"} + } + + return ( +
    + +
    + +
    +
    +
    + +
    +
    + +

    + + {name || account.name}{' '} + + ({rep}) + +

    + +
    + {about &&

    {about}

    } +
    + + {tt('user_profile.follower_count', {count: followerCount})} + {isMyAccount && } + + {tt('user_profile.post_count', {count: account.post_count || 0})} + {tt('user_profile.followed_count', {count: followingCount})} +
    +

    + {location && {location}} + {website && {website_label}} + +

    +
    +
    + +
    +
    +
    +
    + {top_menu} +
    +
    + {printLink} +
    +
    + {tab_content} +
    +
    + ); + } +} + +module.exports = { + path: '@:accountname(/:section)', + component: connect( + state => { + const wifShown = state.global.get('UserKeys_wifShown') + const current_user = state.user.get('current') + // const current_account = current_user && state.global.getIn(['accounts', current_user.get('username')]) + + return { + discussions: state.global.get('discussion_idx'), + current_user, + // current_account, + wifShown, + loading: state.app.get('loading'), + global_status: state.global.get('status'), + accounts: state.global.get('accounts'), + follow: state.global.get('follow'), + follow_count: state.global.get('follow_count') + }; + }, + dispatch => ({ + login: () => {dispatch(user.actions.showLogin())}, + clearTransferDefaults: () => {dispatch(user.actions.clearTransferDefaults())}, + showTransfer: (transferDefaults) => { + dispatch(user.actions.setTransferDefaults(transferDefaults)) + dispatch(user.actions.showTransfer()) + }, + clearPowerdownDefaults: () => {dispatch(user.actions.clearPowerdownDefaults())}, + showPowerdown: (powerdownDefaults) => { + console.log('power down defaults:', powerdownDefaults) + dispatch(user.actions.setPowerdownDefaults(powerdownDefaults)) + dispatch(user.actions.showPowerdown()) + }, + withdrawVesting: ({account, vesting_shares, errorCallback, successCallback}) => { + const successCallbackWrapper = (...args) => { + dispatch({type: 'global/GET_STATE', payload: {url: `@${account}/transfers`}}) + return successCallback(...args) + } + dispatch(transaction.actions.broadcastOperation({ + type: 'withdraw_vesting', + operation: {account, vesting_shares}, + errorCallback, + successCallback: successCallbackWrapper, + })) + }, + requestData: (args) => dispatch({type: 'REQUEST_DATA', payload: args}), + }) + )(UserProfile) +}; diff --git a/src/app/components/pages/UserProfile.scss b/src/app/components/pages/UserProfile.scss new file mode 100644 index 0000000..d560b18 --- /dev/null +++ b/src/app/components/pages/UserProfile.scss @@ -0,0 +1,194 @@ +.UserProfile { + margin-top: -1.5rem; +} + +.UserProfile__tab_content { + margin-top: 1.5rem; +} + +.UserProfile__top-nav { + background-color: $color-blue-dark; + padding: 0; + .row { + .columns { + overflow-x: auto; + } + } + .menu { + background-color: transparent; + } + .menu > li > a { + transition: all 200ms ease-in; + transform: translate3d( 0, 0, 0); + padding-left: 0.7rem; + padding-right: 0.7rem; + background-color: transparent; + color: $color-white; + &:hover, &:focus { + background-color: $color-blue-black; + } + &.active { + @include themify($themes) { + background-color: themed('backgroundColor'); + color: themed('textColorPrimary'); + } + z-index: 2; + font-weight: bold; + } + } + + div.UserProfile__top-menu { + max-width: 71.42857rem; + margin-left: auto; + margin-right: auto; + display: flex; + flex-flow: row wrap; + width: 100%; + // Override default svg vertical alignment + .Icon > svg, .Icon span.icon { + vertical-align: middle!important; + } + } +} + +.UserProfile__section-title { + margin-bottom: 1.5rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #EEE; +} + +.UserProfile__banner { + text-align: center; + color: $white; + a { + color: $white; + } + > div.column { + background: $color-blue-black; + background-size: cover; + background-repeat: no-repeat; + background-position: 50% 50%; + text-shadow: 1px 1px 4px black; + .button {text-shadow: none;} + + min-height: 155px; + } + h1 { + padding-top: 20px; + font-weight: 600; + font-size: 1.84524rem!important; + @media screen and (max-width: 39.9375em) { + font-size: 1.13095rem!important; + } + } + .Icon { + margin-left: 1rem; + svg {fill: #def;} + } + + .Userpic { + margin-right: 0.75rem; + vertical-align: middle; + } + + .UserProfile__rep { + font-size: 80%; + font-weight: 200; + } + + .UserProfile__buttons { + position: absolute; + top: 15px; + right: 5px; + + label.button { + color: black; + border-radius: 3px; + background-color: white; + } + } + + .UserProfile__bio { + margin: -0.4rem auto 0.5rem; + font-size: 95%; + max-width: 420px; + line-height: 1.4; + } + .UserProfile__info { + font-size: 90%; + } + + .UserProfile__stats { + margin-bottom: 5px; + padding-bottom: 5px; + font-size: 90%; + + a { + @include hoverUnderline; + vertical-align: middle; + } + + > span { + padding: 0px 10px; + border-left: 1px solid #CCC; + &:first-child {border-left: none;} + } + + .NotifiCounter { + position: relative; + top: -5px; + } + } +} + +.UserWallet__balance { + > div:nth-child(2) { + text-align: right; + } +} + +@media screen and (max-width: 39.9375em) { + + div.UserProfile__top-nav .menu li>a { + padding: 8px; + } + + .UserProfile__top-menu > div.columns { + padding-left: 0; + padding-right: 0; + } + + .UserProfile__banner .Userpic { + width: 36px !important; + height: 36px !important; + } + + .UserProfile__banner .UserProfile__buttons { + text-align: right; + + label.button { + display: block; + } + } + + .UserProfile__banner .UserProfile__buttons_mobile { + position: inherit; + margin-bottom: .5rem; + .button { + background-color: $white; + color: $black; + } + } + + .UserWallet__balance { + > div:last-of-type { + text-align: left; + } + } + + .UserReward__row { + > div:last-of-type { + padding-left: 20px; + } + } +} diff --git a/src/app/components/pages/WaitingList.jsx b/src/app/components/pages/WaitingList.jsx new file mode 100644 index 0000000..af1ebec --- /dev/null +++ b/src/app/components/pages/WaitingList.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import AddToWaitingList from 'app/components/modules/AddToWaitingList'; +import tt from 'counterpart'; + +class WaitingList extends React.Component { + render() { + return ( +
    +
    +

    + {tt('g.sorry_your_reddit_account_doesnt_have_enough_karma')} +

    + +
    +
    +

    {tt('g.or_click_the_button_below_to_register_with_facebook')}

    + {tt('g.register_with_facebook')} +
    +
    + ); + } +} + +module.exports = { + path: 'waiting_list.html', + component: WaitingList +}; diff --git a/src/app/components/pages/Welcome.jsx b/src/app/components/pages/Welcome.jsx new file mode 100644 index 0000000..cb2d95f --- /dev/null +++ b/src/app/components/pages/Welcome.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import HelpContent from 'app/components/elements/HelpContent'; + +class Welcome extends React.Component { + render() { + return ( +
    +
    +
    +
    Welcome to
    + +
    Come for the rewards. Stay for the community.
    +
    +
    + +
    +
    + ); + } +} + +module.exports = { + path: 'welcome', + component: Welcome +}; diff --git a/src/app/components/pages/Welcome.scss b/src/app/components/pages/Welcome.scss new file mode 100644 index 0000000..2bd20c9 --- /dev/null +++ b/src/app/components/pages/Welcome.scss @@ -0,0 +1,39 @@ +.Welcome__banner { + + position: relative; + + .Welcome__welcome { + position: absolute; + width: 100%; + bottom: 70%; + color: white; + font-size: 160%; + padding-left: 38%; + } + + .Welcome__caption { + position: absolute; + width: 100%; + top: 65%; + color: white; + font-size: 115%; + padding-left: 44%; + padding-right: 2%; + } + +} + +.HelpContent { + + height: inherit; + + p { + position: relative; + } + h2 { + margin-top: 8px; + } + h3 { + margin-top: 0; + } +} diff --git a/src/app/components/pages/Witnesses.jsx b/src/app/components/pages/Witnesses.jsx new file mode 100644 index 0000000..9b2ca71 --- /dev/null +++ b/src/app/components/pages/Witnesses.jsx @@ -0,0 +1,250 @@ +import React, {PropTypes} from 'react'; +import {connect} from 'react-redux'; +import { Link } from 'react-router'; +import links from 'app/utils/Links' +import Icon from 'app/components/elements/Icon'; +import transaction from 'app/redux/Transaction' +import ByteBuffer from 'bytebuffer' +import {is} from 'immutable' +import g from 'app/redux/GlobalReducer'; +import tt from 'counterpart'; + +const Long = ByteBuffer.Long +const {string, func, object} = PropTypes + +class Witnesses extends React.Component { + static propTypes = { + // HTML properties + + // Redux connect properties + witnesses: object.isRequired, + accountWitnessVote: func.isRequired, + username: string, + witness_votes: object, + } + constructor() { + super() + this.state = {customUsername: "", proxy: "", proxyFailed: false} + this.accountWitnessVote = (accountName, approve, e) => { + e.preventDefault(); + const {username, accountWitnessVote} = this.props + this.setState({customUsername: ''}); + accountWitnessVote(username, accountName, approve) + } + this.onWitnessChange = e => { + const customUsername = e.target.value; + this.setState({customUsername}); + } + this.accountWitnessProxy = (e) => { + e.preventDefault(); + const {username, accountWitnessProxy} = this.props; + accountWitnessProxy(username, this.state.proxy, (state) => { + this.setState(state); + }); + } + } + + shouldComponentUpdate(np, ns) { + return ( + !is(np.witness_votes, this.props.witness_votes) || + np.witnesses !== this.props.witnesses || + np.current_proxy !== this.props.current_proxy || + np.username !== this.props.username || + ns.customUsername !== this.state.customUsername || + ns.proxy !== this.state.proxy || + ns.proxyFailed !== this.state.proxyFailed + ); + } + + render() { + const {props: {witness_votes, current_proxy}, state: {customUsername, proxy}, accountWitnessVote, + accountWitnessProxy, onWitnessChange} = this + const sorted_witnesses = this.props.witnesses + .sort((a, b) => Long.fromString(String(b.get('votes'))).subtract(Long.fromString(String(a.get('votes'))).toString())); + const up = ; + let witness_vote_count = 30 + let rank = 1 + const witnesses = sorted_witnesses.map(item => { + const owner = item.get('owner') + const thread = item.get('url') + const myVote = witness_votes ? witness_votes.has(owner) : null + const classUp = 'Voting__button Voting__button-up' + + (myVote === true ? ' Voting__button--upvoted' : ''); + let witness_thread = "" + if(thread) { + if(links.remote.test(thread)) { + witness_thread = {tt('witnesses_jsx.witness_thread')}  + } else { + witness_thread = {tt('witnesses_jsx.witness_thread')} + } + } + return ( + + + {(rank < 10) && '0'}{rank++} +    + + {up} + + + + {owner} + + + {witness_thread} + + + ) + }); + + let addl_witnesses = false; + if(witness_votes) { + witness_vote_count -= witness_votes.size + addl_witnesses = witness_votes.filter(item => { + return !sorted_witnesses.has(item) + }).map(item => { + return ( +
    +
    + {/*className="Voting"*/} + + {up} +   + + + {item} +
    +
    + ) + }).toArray(); + } + + + return ( +
    +
    +
    +

    {tt('witnesses_jsx.top_witnesses')}

    + {current_proxy && current_proxy.length ? null : +

    + {tt('witnesses_jsx.you_have_votes_remaining', {count: witness_vote_count})}.{' '} + {tt('witnesses_jsx.you_can_vote_for_maximum_of_witnesses')}. +

    } +
    +
    + {current_proxy ? null : +
    +
    + + + + + + + + + + {witnesses.toArray()} + +
    {tt('witnesses_jsx.witness')}{tt('witnesses_jsx.information')}
    +
    +
    } + + {current_proxy ? null : +
    +
    +

    {tt('witnesses_jsx.if_you_want_to_vote_outside_of_top_enter_account_name')}.

    +
    +
    + @ + +
    + +
    +
    +
    +
    + {addl_witnesses} +

    +
    +
    } + +
    +
    +

    {current_proxy ? tt('witnesses_jsx.witness_set') : tt('witnesses_jsx.set_witness_proxy')}

    + {current_proxy ? +
    +
    {tt('witnesses_jsx.witness_proxy_current')}: {current_proxy}
    + +
    +
    + +
    + +
    +
    +
    +
    : +
    +
    + @ + {this.setState({proxy: e.target.value});}} /> +
    + +
    +
    +
    } + {this.state.proxyFailed &&

    {tt('witnesses_jsx.proxy_update_error')}.

    } +
    +
    +
    +
    + ); + } +} + + +module.exports = { + path: '/~witnesses(/:witness)', + component: connect( + (state) => { + const current_user = state.user.get('current'); + const username = current_user && current_user.get('username') + const current_account = current_user && state.global.getIn(['accounts', username]) + const witness_votes = current_account && current_account.get('witness_votes').toSet(); + const current_proxy = current_account && current_account.get('proxy'); + return { + witnesses: state.global.get('witnesses'), + username, + witness_votes, + current_proxy + }; + }, + (dispatch) => { + return { + accountWitnessVote: (username, witness, approve) => { + dispatch(transaction.actions.broadcastOperation({ + type: 'account_witness_vote', + operation: {account: username, witness, approve}, + })) + }, + accountWitnessProxy: (account, proxy, stateCallback) => { + dispatch(transaction.actions.broadcastOperation({ + type: 'account_witness_proxy', + operation: {account, proxy}, + confirm: proxy.length ? "Set proxy to: " + proxy : "You are about to remove your proxy.", + successCallback: () => { + dispatch(g.actions.updateAccountWitnessProxy({account, proxy})); + stateCallback({proxyFailed: false, proxy: ""}); + }, + errorCallback: (e) => { + console.log('error:', e); + stateCallback({proxyFailed: true}); + } + })) + } + } + } + )(Witnesses) +}; diff --git a/src/app/components/pages/Witnesses.scss b/src/app/components/pages/Witnesses.scss new file mode 100644 index 0000000..915b439 --- /dev/null +++ b/src/app/components/pages/Witnesses.scss @@ -0,0 +1,30 @@ + +.Witnesses { + .extlink path { + transition: 0.2s all ease-in-out; + @include themify($themes) { + fill: themed('textColorAccent'); + } + } + a:hover .extlink path { + transition: 0.2s all ease-in-out; + @include themify($themes) { + fill: themed('textColorAccentHover'); + } + } + td > a { + @extend .link; + @extend .link--primary; + } + + .button { + background-color: $color-text-teal; + text-shadow: 0 1px 0 rgba(0,0,0,0.20); + transition: 0.2s all ease-in-out; + &:hover { + background-color: $color-teal; + } + } +} + + diff --git a/src/app/components/pages/XSS.jsx b/src/app/components/pages/XSS.jsx new file mode 100644 index 0000000..7aefbfd --- /dev/null +++ b/src/app/components/pages/XSS.jsx @@ -0,0 +1,78 @@ +import React from 'react' +import MarkdownViewer from 'app/components/cards/MarkdownViewer' + +class XSS extends React.Component { + render() { + if(!process.env.NODE_ENV === 'development') return
    + let tests = xss.map( (test, i) =>

    Test {i}


    ) + return
    +
    + {tests} +
    +
    + // return
    + } +} + +module.exports = { + path: '/xss/test', + component: XSS +}; + +// https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Image_XSS_using_the_JavaScript_directive +// July 14 2016 +const xss = [ + +``, + +`
    `, + +`
    `, + +``, + +``, + +``, + +``, + +``, + +`
    • XSS`, + +``, + +``, + +``, + +`
      *
      `, + +`XSS`, + +`';alert(String.fromCharCode(88,83,83))//';alert(String.fromCharCode(88,83,83))//"; +alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//-- +>">'>`, + +`'';!--"=&{()}`, + +``, + +`onnerr w/ clearly invalid img:
      +good image:
      +good url, bad img: +(results will vary if using image proxy -- it rewrites 'src')`, + +`**test**!%3Cimg%20src=%22awsome.jpg%22%20onerror=%22alert(1)%22/%3E`, + +`test!%3Cimg%20src=%22awsome.jpg%22%20onerror=%22alert(1)%22/%3E`, + +'', + +'Hax', + +'Link to a local page with bad rel attr', +'Link to domain (relative protocol) and bad target attr', + +] diff --git a/src/app/help/en/faq.md b/src/app/help/en/faq.md new file mode 100644 index 0000000..a0fde71 --- /dev/null +++ b/src/app/help/en/faq.md @@ -0,0 +1,1428 @@ +# Steemit FAQ + + +## Table of Contents + + +### General +- What is Steemit.com? +- How does Steemit work? +- How does Steemit differ from other social media websites? +- Does it cost anything to post, comment, or vote? +- Can I earn digital tokens on Steemit? How? +- Where do the tokens come from? +- Where does the value come from? +- Why are people getting vastly different rewards? + + +### Accounts +- How do I create an account? +- What information do I need to provide in order to create an account? +- How long does the account approval process take? +- Why do I need to provide my email and phone number? +- Can I create a Steem account without an email and phone number? +- What are other ways to create an account on the blockchain besides using Steemit.com? +- It is not letting me create an account with my phone number. What should I do? +- What happens if my email or phone number changes? +- Am I allowed to create more than one account +- Can I delete or deactivate my account? + +### Community +- Is there an Etiquette Guide for Steemit? +- Am I required to verify my identity? + + +### Site Navigation +- How do I upvote a post or comment? +- What do the Home, New, Hot, Trending, and Promoted links show? +- What information is available in my account menu? +- How do I see my recent rewards? +- What information is shown in my wallet? +- How do I transfer my STEEM or Steem Dollars into savings? +- How do I send money to another user? +- Will I receive notifications when there is activity with my account? +- What is shown in my profile? +- How do I change my avatar image and other profile information? +- What is the recommend size for the cover image? +- How can I control whether I see "Not Safe For Work" (NSFW) content? +- How do I search for content? +- Can I see which users I have muted? +- Can I see which users have muted me? +- Can I see the list of users I am following, and who is following me? +- What languages are supported? + + +### Posting +- What can users post to Steemit? +- What are the different choices for post rewards (50%/50%, Power Up 100%, Decline Payout)? +- How do I add images and photos to my posts? +- How do I set the thumbnail image for my post? +- What is the recommend aspect ratio for thumbnail images? +- How do I add videos to my posts? +- Is there a way I can make my images smaller? +- What are tags? +- What tags should I use? +- How many tags can I use? +- Why is the "Post" button grayed out? +- How do I format text in Markdown? +- How often can I post? +- How long can my post be? +- If posting in a language other than English, how will I get recognized? +- Can I delete something I posted? +- What does "Promoting" a post do? +- How do I promote a post? + + +### Comments +- Can I earn digital tokens for commenting? +- How often can I comment? + + +### Economics +- Where do the new STEEM tokens come from? +- How many new tokens are generated by the blockchain? +- How are the new tokens distributed? +- What is the reward pool? +- How is the reward pool split between authors and curators? +- Will the reward pool pay out more or less depending on who votes? +- Why do the earnings for my post go up or down? +- When can I claim my rewards? +- What is the difference between STEEM, STEEM Power, and Steem Dollars? +- What is delegated STEEM Power? +- What determines the price of STEEM? +- How do I get more STEEM Power? +- How long does it take STEEM or STEEM Power that I purchased to show up in my account? +- What is powering up and down? +- What do the dollar amounts for pending payouts represent? +- Will 1 Steem Dollar always be worth $1.00 USD? +- How do Steem Dollar to STEEM conversions work? +- Is there a way for me to convert my Steem Dollars to STEEM without waiting 3.5 days? +- What can I do with my STEEM tokens? +- What can I do with my SBD tokens? +- What is a MVEST? +- Can I sell goods and services on Steemit? +- How can I withdraw my STEEM or SBD coins? +- Will I get a 1099 from Steemit? +- How much are the transaction fees for sending tokens to other users? +- Are there fees for Powering Up, Powering Down, trading on the internal market, or converting SBD to STEEM? +- How long does it take to transfer STEEM or SBD tokens between users? + + +### Voting and Curating +- What is my voting power? +- How many times can I vote without depleting my voting power? +- Can I vote with less than 100% of my voting strength? +- Where can I check my voting power? +- What determines how much of the curation reward goes to the author versus curators? +- Can I get curation rewards for upvoting comments? +- Do I get curation rewards for downvoting posts or comments? +- What are curation trails? +- Why don't my upvotes have an effect on a post's rewards? +- Is there a way to make my votes count for more? +- What are the valid reasons for downvoting? +- Does a downvote mean that I did something wrong? +- Will a downvote hurt my reputation? +- What is the difference between a downvote and a flag? + + +### Plagiarism, Spam, and Abuse +- What are Steemit’s policies on plagiarism and spam? +- Is it okay to use random pictures from the internet? +- What is Steemcleaners? +- What is @cheetah? +- Where do I report a post or comment that contains plagiarism, spam, or abuse? + + +### Reputation +- What is Reputation? +- How is the Reputation score measured? +- How do I improve my reputation score? +- What causes my reputation score to go down? +- Why does my reputation score matter? + + +### Followers, Feeds, and Resteem +- What is Resteeming? +- Can I share on other social media? + + +### Blockchain +- What is a blockchain? +- What is the Steem blockchain? +- What is the difference between Steem and Steemit? +- How is Steem different from Bitcoin? +- What is the difference between Proof of Work, Proof of Stake, and Delegated Proof of Stake? +- How often does the Steem blockchain produce a new block? +- Is there a way to see the raw data that is stored in the blockchain? +- Where can I find the information for the official launch of the blockchain? +- Can I mine STEEM? + + +### Steemit, Inc. +- Who is the CEO of Steemit? +- Can I invest in Steemit? +- What does Steemit’s development roadmap look like? +- Am I allowed to use the Steemit logo? +- Can I purchase official Steemit merchandise? +- Did Steemit "pre-mine" tokens? +- What is the Steemit Privacy Policy? + + +### Security +- How can I keep my Steem account secure? +- Why should I be careful with my master password? +- Why is the master password a long string of gibberish? +- What are my different keys for? +- What do I do if I lost my password/keys? +- Are my STEEM and Steem Dollar tokens insured in the event of a hack or if someone takes over my account? +- What should I do if I discover that someone hacked my account? +- How does the stolen account recovery process work? +- How do I report a security vulnerability? + + +### Developers +- Are the Steem blockchain and Steemit.com code open-source? +- Is there a Github page for Steemit.com? +- Is there a Github page for the Steem blockchain? +- What is available for developers interested in Steem and Steemit? +- How do I use cli_wallet? + + +### Witnesses +- What are Steem witnesses? +- How can I vote for witnesses? +- How many witnesses can I vote for? + + +### Miscellaneous +- What third-party tools are there for Steemit? +- Is there an official Steemit Facebook page? +- Is there an official Steemit Twitter account? +- What is the Steem Whitepaper and what is its purpose? +- Where can I ask for help if my question was not answered here? + + +### Disclaimer +- Third Party References and User Links + +# General + + +## What is Steemit.com? + +Steemit is a social network and content rewards platform that makes the crowd the beneficiaries of the attention economy. It does this be rewarding users with STEEM. + +Steemit has redefined social media by building a living, breathing, and growing social economy; a community where users are getting rewarded for sharing their voice. + +^ + +## How does Steemit work? + +Steemit is a social media platform that works by having the crowd reward the crowd for their content. It does this thanks to the Steem blockchain and cryptocurrency; Steem is 'minted' daily and distributed to content producers according to the votes they get. + +^ + +## How does Steemit differ from other social media websites? + +Most social media sites extract value from their userbase for the benefit of shareholders alone. Steemit is different, it's a new kind of attention economy. By connecting with the Steem blockchain (which is decentralized and controlled by the crowd), Steemit users receive all the benefits and rewards for their attention. + +^ + +## Does it cost anything to post, comment, or vote? + +No. It is free to post, comment, and vote on content on Steemit.com. You might even get paid for it! + +^ + +## Can I earn digital tokens on Steemit? How? + +You can earn digital tokens on Steemit by: + +**Posting** - By sharing your posts, you can earn upvotes from community members. Depending on the upvotes you receive, you will get a portion of the ongoing Steem reward pool. + +**Voting and curating** - If you discover a post and upvote it before it becomes popular, you can earn a curation reward. The reward amount will depend on the amount of STEEM Power you have. + +**Purchasing** - Users can purchase STEEM or Steem Dollar tokens directly through the Steemit wallet using bitcoin, Ether, or BitShares tokens. They are also available from other markets and exchanges including BlockTrades, Poloniex, Bittrex, Shapeshift.io, and Changelly. STEEM tokens that are powered up to STEEM Power earn a small amount of interest for holding. + +^ + +## Where do the tokens come from? + +The Steem network continually creates digital tokens to reward content creators and curators. Some of the newly-created tokens are transferred to users who add value to Steemit by posting, commenting, and voting on other people's posts. The remainder is distributed to holders of STEEM Power and the witnesses that power the blockchain. + +^ + +## Where does the value come from? + +At its root, Steem is simply a points system. However, because this points system is blockchain-based, the points can be traded on markets as tokens. People buy and sell these tokens, and many hold in anticipation of increased purchasing power for various Steem-related services. + +By analogy, Steem is a game system where users compete for attention and rewards by bringing content and adding value to the platform. The rewards people earn are tokens that have market value and are readily tradable. It is similar to how someone playing a video game could obtain a limited item or currency by playing the game. If the currency or items are transferable between users, then they can sell or buy them on game item markets. + +^ + +## Why are people getting vastly different rewards? + +Steemit is not a "get rich quick" scheme. While it is possible to post content that goes viral quickly and earn a lot of rewards on a single post, this is not typical for most users. + +Most of the authors that you see earning high rewards are users that have spent a lot of time in the network building followings, making connections with others, and developing a reputation for bringing high quality content. + +It is best to have realistic expectations, without focusing on rewards when you are first starting out. Work on building a following, making connections, and developing a good reputation. Consistency will pay off in the long run. + +^ +# Accounts + + +## How do I create an account? + +Click on the "Sign Up" link at the top of Steemit.com to get started. + +You will be asked to verify your email address and phone number. After your email address and phone number have been verified, you will be added to the waiting list. You will be notified via email once your account is approved. + +After you receive notification that your account is approved, click on the link in the email to finish the account creation process. Be sure to save and backup your username and password. It is very important that you do not lose your password. There is no way to recover your password or access your account if it is lost. Once your password is saved and backed up, click on the "Create Account" button to create the account. + +^ + +## What information do I need to provide in order to create an account? + +You will need to provide your email address and phone number. + +^ + +## How long does the account approval process take? + +Most accounts are approved within 24 hours. Some may take up to a week. + +If your account has not been approved after one week, please ask for help in the #help channel on steemit.chat. + +^ + +## Why do I need to provide my email and phone number? + +To create an account on the blockchain, it costs STEEM tokens. When you create an account through Steemit.com, Steemit Inc. is supplying the tokens to pay the account creation fee. In order to prevent users from abusing the paid-for signup and creating multiple accounts, we need to be able to verify that each user is only signing up for one account. + +^ + +## Can I create a Steem account without an email and phone number? + +The only way to have an account created via Steemit.com is to supply your email and phone number. Because Steem is an open and permissionless network, there are other ways to create a Steem account. Any Steem blockchain account can be used on Steemit.com + +^ + +## What are other ways to create an account on the blockchain besides using Steemit.com? + +If you are willing to pay your own signup fee, then there are other ways to create a new account on the blockchain. + +There is a third-party tool called AnonSteem that accepts bitcoin, Litecoin, STEEM, or SBD to anonymously create a Steem account. You do not need to have an existing Steem blockchain account to use the service, but there is a charge on top of the blockchain account creation fee for using the service. + +There is also a third-party tool called SteemConnect that allows you to create accounts by paying or delegating the account creation fee. There is no additional fee to use the service, but does require an existing Steem blockchain account to pay the account creation fee to create the account. + +^ + +## It is not letting me create an account with my phone number. What should I do? + +Ask for help in the #help channel on steemit.chat. + +^ + +## What happens if my email or phone number changes? + +Currently there is no way to change the email or phone number that is linked to your account. Though once your account is created, you can continue to use it even if the email or phone number that is linked to the account has changed. + +^ + +## Am I allowed to create more than one account? + +Each user is allowed only one paid-for account created via Steemit.com, however users are allowed to create multiple accounts on the blockchain. Creating additional accounts on the blockchain requires users to pay their own account creation fee for any additional accounts. + +^ + +## Can I delete or deactivate my account? + +Accounts can not be deactivated or deleted. The account along with all of its activity is permanently stored in the blockchain. + +^ +# Community + + +## Is there an Etiquette Guide for Steemit? + +There are no official rules for participating on Steemit.com, but one of the users @thecryptofiend has created an Etiquette Guide for the community. While it is not required to follow the suggestions in the guide, they are standards that many users in the community choose to follow. + +^ + +## Am I required to verify my identity? + +Verification is a process where users give evidence to show that they are the person that they claim to be. This is to reduce fraud and people impersonating known figures. If you would like to remain anonymous, that is perfectly fine. However if you claim to be someone specific, the community may expect that you verify you are who you say you are. + +There are a number of ways to do this. The most common way to verify your identity is by posting a link to your Steemit profile on a website or social media account which you are already known for having such as Twitter, Facebook, LinkedIn, a blog, or photography site. + +Many users also like to post a photo or a video which shows them holding up a sheet of paper with the current date and their Steem account name handwritten on it. This is a great way to add a personal touch to verifying. + +^ +# Site Navigation + + +## How do I upvote a post or comment? + +To upvote a post or comment, click on the "upvote" icon at the bottom of the post/comment. + +^ + +## What do the Home, New, Hot, Trending, and Promoted links show? + +These are various ways to sort Steem posts. + +**Home** - The most recent posts of the accounts you follow (your feed). + +**New** - Posts are sorted by the time posted, with newest first. + +**Hot** - Popular posts at the moment. + +**Trending** - Posts with the most amount of votes, stake-weighted, recently. + +**Promoted** - Listings that are boosted by Steem Dollar payments get promoted for greater visibility. + +^ + +## What information is available in my account menu? + +You can get to your account menu by clicking on the avatar icon in the top-right corner of a Steemit.com page. + +**Feed** - Here is where you go to see the most recent posts from the people you follow. + +**Blog** - Here is where you go to see all of your posts and resteems. It is also where you go to see your profile page that is viewable by other users. + +**Comments** - Here is where you go to see all of the comments and replies you have made. + +**Replies** - Here is where you go to see all replies other users have made to your posts and comments. + +**Wallet** - Here is where you go to see your wallet balances, make transfers, exchange STEEM or Steem Dollars, and Power Up. + +**Change Password** - Here is where you go to change your password. + +**Settings** - Here is where you go to update your settings. + +**Logout** - If you'd like to logout. + +^ + +## How do I see my recent rewards? + +The Rewards drop-down menu is available on your profile/blog page. Click it and there are two links: + +**Curation rewards** - Shows the rewards earned for upvoting posts and comments. + +**Author rewards** - Shows the rewards earned by your own posts and comments. + +You can also view the same information for other users by visiting their profile. + +^ + +## What information is shown in my wallet? + +Your wallet shows how many STEEM and Steem Dollar tokens you have in your account. It shows how much STEEM Power it has, and how much SP is delegated. It also shows how many of your STEEM and Steem Dollar tokens are being held in the savings account, which is a balance that is subject to 3 day withdraw waiting period. The wallet page shows any the progress of any Steem Dollar to STEEM conversions as well as the status of a power down. It also shows an estimated value of all the tokens in your account, based on the recent market prices of STEEM and SBD. + +^ + +## How do I transfer my STEEM or Steem Dollars into savings? + +Your savings balance is STEEM and SBD tokens that are subject to 3 day withdraw waiting period. This is an extra security measure in case your account credentials are compromised. To transfer STEEM or SBD tokens into savings, click on the drop-down arrow next to STEEM or STEEM DOLLARS in your wallet, and select "Transfer to Savings". + +^ + +## How do I send money to another user? + +- From your wallet page, click the STEEM or Steem Dollar balances with the down arrow next to them. +- In the drop-down menu, click 'Transfer'. +- Type the username of the account you want to send the STEEM or Steem Dollars to. Double and triple check spelling. +- Enter the amount of STEEM or Steem Dollars to send. +- Enter a memo to go along with the transaction (optional). +- Click Submit. +- You will be prompted for your password. You will need to enter your master password or active key. + +^ + +## Will I receive notifications when there is activity with my account? + +When there is new activity in your feed, you receive a reply from another user, or there is a new transfer in your wallet, you will receive a notification in your account menu. It will show a little red number showing the number of new notifications. + +Steemit also allows you to subscribe to receive additional notifications when users mention you in a comment or post. + +Currently, there are no options to receive notifications for votes directly on Steemit.com. But, there is a third-party application https://steemstats.com/, developed by @jesta, which has an option to set up additional notifications on your computer. + +^ + +## What is shown in my profile? + +At the top of your profile is your display name and reputation score. Below your display name is the number of followers you have, the number of posts and comments you have written, and the number of people you are following. It also shows the month and year when your account was created. + +You have the option to change your avatar and display name on the Settings page. There, you can set additional information such as "about" information, your location, and add a link to a website of your choosing. You also have the option to set a cover image for your profile. + +You can view your own profile by clicking on the link to your Blog in your account menu. + +^ + +## How do I change my avatar image and other profile information? + +Your profile info, avatar image, and cover image are set in your Settings page. In order to update your avatar picture and cover image, you will need to host the images somewhere. This can be done by uploading it to a Steemit comment or post, or using a third-party image host such as Postimage. Once your image is uploaded, copy its URL and paste it into the "Profile Picture URL" box for the avatar, or the "Cover Image URL" box for the cover image. Then click the Update button and enter your password or active key. + +^ + +## What is the recommend size for the cover image? + +The cover image will be resized/scaled depending on the device being used. Therefore it is recommend to use an image that will still look good when cropped or resized. A 2048x512 image is the optimal size to work for most devices. + +^ + +## How can I control whether I see "Not Safe For Work" (NSFW) content? + +By default, content that users have tagged as "NSFW" will be hidden, but a link will be shown to reveal the content. + +You can update your display preference with the Settings page so that NSFW content is always shown by default, remains hidden until clicked, or is completely hidden with no option to reveal. + +^ + +## How do I search for content? + +In the upper right corner of Steemit, there is a magnifying glass search link where you can find posts using a keyword search. + +There is also an **Explore** link in the main menu, where you can browse through posts based on tags. + +^ + +## Can I see which users I have muted? + +Yes. This can be seen under the Settings page. + +^ + +## Can I see which users have muted me? + +No. This information is not presented on Steemit.com. + +^ + +## Can I see the list of users I am following, and who is following me? + +Yes. You can see the list of followers or people you are following by clicking on the links on your profile page. + +^ + +## What languages are supported? + +English is the most-used language used on the Steemit platform, but communities are forming that speak other languages. + +^ +# Posting + + +## What can users post to Steemit? + +Steem is an open platform meant to host and welcome any legal content. Users can post anything they want, whether it be phrases, quotes, blogs, anecdotes, photos, videos, memes, songs, and more. Be creative! + +^ + +## What are the different choices for post rewards (50%/50%, Power Up 100%, Decline Payout)? + +- **50%/50%** - This rewards in half STEEM Power, and half liquid STEEM / Steem Dollars. The ratio of liquid STEEM to Steem Dollars rewarded is based on network conditions at the time of payout. This is the default payout option. + +- **Power Up 100%** - This option rewards the post in 100% STEEM Power. + +- **Decline Payout** - Use this option to receive no post rewards. Votes will affect the post's position on the trending ranking but no rewards are paid from Steem's reward pool. Replies made to the post are still eligible for rewards. + +^ + +## How do I add images and photos to my posts? + +You can browse your hard drive to add an image by clicking on the "selecting them" link from within the editor. + +If you have an image copied to your clipboard, you can simply paste (`ctrl + v`) while in the post/comment editor, and your image will be uploaded into your post or comment. Due to the file size of these pasted images, this method is only recommended for simple graphics. Photos (.JPG) should be uploaded from your disk. + +Pictures can also be hosted on an external site. Paste the image's web address (URL) into the editor and it will automatically be added. + +^ + +## How do I set the thumbnail image for my post? + +The first image in the post will automatically be set as the thumbnail image. + +^ + +## What is the recommend aspect ratio for thumbnail images? + +The recommend aspect ratio for thumbnail images is 16x9. + +^ + +## How do I add videos to my posts? + +To add a YouTube or Vimeo video to your blog post, simply paste the link to the video into the post. + +You can also read this guide from @algimantas, which has more detailed instructions: + +^ + +## Is there a way I can make my images smaller? + +Yes, but the picture must be resized before it is uploaded into the Steemit.com editor. This can be done in your favorite photo editing software, or online by uploading to a third-party website that features editing such as imgur.com. + +^ + +## What are tags? + +Tags are a way to categorize your content, so that others can find it. The more relevant the tags are to the post, the more like-minded people will come across it. + +^ + +## What tags should I use? + +Try to use tags that are relevant to your post, and that will be popular for other people to browse. For example, "mytriptoalaska" may be relevant to your post, but readers are probably not going to go searching for that. Using "travel" would be a better choice for a tag in this case. + +You can browse through commonly used tags using the "Explore" link, in the main menu. + +Be mindful when choosing tags. If your tags aren’t related to your post, your post may get downvotes for mistagging. + +All tags must be lowercase letters. Spaces aren't allowed, but hyphenated words with a single dash are. + +^ + +## How many tags can I use? + +You can use up to 5 tags per post. + +^ + +## Why is the "Post" button grayed out? + +A post must have a title, body, and at least one valid tag. If any of these are missing, then the "Post" button will be disabled. + +^ + +## How do I format text in Markdown? + +Some common markdown syntax is: +- `**bold**` **bold** +- `_italics_` _italics_ +- `~~cross out~~` ~~cross out~~ + +Text can be sized using headers: +``` +# H1 +## H2 +### H3 +#### H4 +``` +# H1 +## H2 +### H3 +#### H4 + +For more advanced formatting, a guide describing the common markdown formatting syntax can be found here: Markdown Cheatsheet + +^ + +## How often can I post? + +You are allowed to post almost as often as you like. Currently, posts must be spaced 5 minutes apart. However, the community may not find value in users that are posting too frequently. Keep in mind what your audience will be interested in viewing, so that you do not overwhelm your followers with too much content. + +^ + +## How long can my post be? + +Post sizes are limited to about 64,000 characters including formatting. This is ample for most posts. If writing blogs, consider how much people are willing to read at one time. If you make your posts too long, readers may lose interest which may affect the amount of upvotes and rewards you receive. + +^ + +## If posting in a language other than English, how will I get recognized? + +You can use language-specific tags to help you to reach the audience that speaks your language. + +Language-specific groups include: +- Chinese = cn +- German = deutsch +- Spanish = spanish +- Korean = kr +- Russian = ru +- French = fr +- Portuguese = pt + +^ + +## Can I delete something I posted? + +The blockchain will always contain the full edit history of posts and comments, so it can never be completely deleted. If you would like to update a post so that users cannot see the content via Steemit.com, you can edit the post and replace it with blank content for as long as the post is active. After seven days, the post can no longer be edited. + +^ + +## What does "Promoting" a post do? + +When you make a post, there is the option to promote it with Steem Dollars. It will then show up in the “Promoted” tab. The order that it appears in the list depends on how much the post was promoted for. Posts with a higher promoted amount will be higher than posts with less. + +Steem Dollars spent to promote a post are paid to the account @null, which nobody owns or controls. Once a user transfers SBD to @null, the Steem blockchain removes them from the currency supply. Details can be found in this official post. + +You can promote your own posts, or posts that you like from other users. + +^ + +## How do I promote a post? + +At the bottom of each post is a button to "Promote". After clicking the button, type the number of Steem Dollars that you want to spend and click “PROMOTE”. The operation will require your master password or active key. + +^ +# Comments + + +## Can I earn digital tokens for commenting? + +Yes, comments that are upvoted can earn rewards just like posts! + +^ + +## How often can I comment? + +There is a 20 second wait time in between comments to limit spam. + +^ +# Economics + + +## Where do the new STEEM tokens come from? + +Blockchains like Steem and Bitcoin produce new tokens each time a block is produced. Unlike Bitcoin, where all of the new coins go to the block producers (called miners), the Steem blockchain allocates a majority of the new tokens to a reward fund. The reward fund gives users tokens for participating in the platform. + +^ + +## How many new tokens are generated by the blockchain? + +Starting with the network's 16th hard fork in December 2016, Steem began creating new tokens at a yearly inflation rate of 9.5%. The inflation rate decreases at a rate of 0.01% every 250,000 blocks, or about 0.5% per year. The inflation will continue decreasing at this pace until the overall inflation rate reaches 0.95%. This will take about 20.5 years from the time hard fork 16 went into effect. + +^ + +## How are the new tokens distributed? + +Out of the new tokens that are generated: +- 75% go to the reward pool, which is split between authors and curators. +- 15% of the new tokens are awarded to holders of STEEM Power. +- The remaining 10% pays for the witnesses to power the blockchain. + +^ + +## What is the reward pool? + +Every day, a fixed amount of STEEM tokens are allocated to the network reward fund, commonly called the "reward pool." These get distributed to authors and curators for posting and voting on content. + +^ + +## How is the reward pool split between authors and curators? + +Up to 25% of a post's payout is awarded to curators (the people who upvoted the post) as a reward for discovering the content. The other 75% is awarded to the author. If curators vote for a post within the first 30 minutes of it being created, a portion of their curation reward is added to the author payout. This portion is linear to the age of the post between 0 and 30 minutes. Therefore upvoting at 15 minutes old will donate half of your potential curation reward to the author. + +^ + +## Will the reward pool pay out more or less depending on who votes? + +There is a fixed amount of STEEM coins that gets added to the rewards pool each day. In the short term, the amount of coins that get paid out may be higher or lower depending on the amount of voting activity, but over time it will pay out the full amount of rewards regardless of who votes. + +Votes in Steem are stake-weighted. Therefore voters with more STEEM Power have a greater influence over the allocation than voters with less SP, but their votes do not increase the amount of rewards. + +^ + +## Why do the earnings for my post go up or down? + +The amount that is shown next to a post is a "**Potential Payout**". This is an estimated value of how much money the post will make based on the votes that have occurred so far. Depending on various factors, this value can go up or down until the payout window closes: + +- If a post receives more upvotes, the potential payout of the post can go up. +- If a post receives more downvotes, the potential payout of the post can go down. +- If other posts receive more upvotes, the potential payout of the post can go down. +- If other posts receive more downvotes, the potential payout of the post can go up. +- If upvotes are removed from a post, the potential payout of the post can go down. +- If downvotes are removed from a post, the potential payout of the post can go up. +- If the price of STEEM goes up, the potential payout of all posts can go up. +- If the price of STEEM goes down, the potential payout of all posts can go down. + +^ + +## When can I claim my rewards? + +Posts and comments remain active for 7 days. When the period is over, you are able to claim their earned rewards. In your Wallet, click the Claim Rewards button to add the tokens to your account. + +^ + +## What is the difference between STEEM, STEEM Power, and Steem Dollars? + +**STEEM** - STEEM is the base liquid currency token in the platform. STEEM can be powered up into STEEM Power, traded for Steem Dollars, and transferred to other accounts. It is a cryptocurrency token, similar to bitcoin. + +**STEEM Power** - STEEM Power (abbreviated SP) is a measurement of how much influence a user has in the Steem network. The more STEEM Power a user holds, the more they can influence the value of posts and comments. STEEM Power is less liquid. If a user wishes to “Power Down” SP, they will receive equal distributions of the STEEM weekly, over a 13 week period. + +**Steem Dollars** - Steem Dollars (commonly abbreviated SBD) are liquid stable-value currency tokens designed to be pegged to $1 USD. Steem Dollars can be traded with STEEM, and transferred to other accounts for commerce or exchange. Steem Dollars may also be converted into STEEM in a process that takes 3.5 days. Steem Dollars can be used to buy things in marketplaces, such as PeerHub.com. + +^ + +## What is delegated STEEM Power? + +Users have the option to delegate STEEM Power to other users. When a user is delegated STEEM Power, their content votes and curation rewards are calculated as if it were their own STEEM Power. Users are not able to power down or cash out delegated STEEM Power, as it still belongs to the original owner. + +Most users will have a small amount of STEEM Power delegated to them by the Steemit account after creating an account via Steemit.com. + +Delegated STEEM Power shows up in a user's wallet below their actual STEEM Power balance in parentheses. + +^ + +## What determines the price of STEEM? + +The price of STEEM is based on the supply and demand of the token, determined by buyers and sellers on the exchanges. It is similar to how the price of a commodity like gold is determined. + +^ + +## How do I get more STEEM Power? + +With STEEM tokens in your wallet, click "Power Up" to turn them into STEEM Power. If you have Steem Dollars, you can convert them to STEEM from your wallet, and then power up the STEEM. + +If you don’t already have STEEM or Steem Dollars in your wallet, you can purchase them using bitcoin (BTC), ether (ETH), or BitShares (BTS) tokens. You may purchase BTC on various exchanges, such as Coinbase.com or Localbitcoins.com. + +To buy: +- Click "Buy Steem" from the main menu in the top right corner of Steemit.com, or from your wallet. +- Select the currency to deposit, and enter the amount of that currency you wish to use. +- Enter your Steemit account name (without the @) for "Your receive address". +- Click the "Get Deposit Address" button. +- Send the currency to the provided address. + +STEEM purchases made via Steemit.com are facilitated by BlockTrades. + +bitcoin can also be exchanged for STEEM on external markets such as Poloniex, Bittrex, ShapeShift.io, and Changelly. + +^ + +## How long does it take STEEM or STEEM Power that I purchased to show up in my account? + +Transactions on the Steem blockchain typically only take about three seconds to process, but when you are purchasing the STEEM tokens using bitcoin or some other token, then the transaction must wait for the transaction to be confirmed on the other network. This can often take several hours, and sometimes even days. + +If you paid using bitcoin, the third party website bitcoinfees.21.co can estimate the approximate wait time of the transaction based on the fees that were paid. The third party website blockchain.info will lookup the fees that were paid on a specific blockchain transaction. + +^ + +## What is powering up and down? + +**Powering up** - If you have STEEM tokens, you can Power Up to STEEM Power to get more voting influence on posts and comments. Having more STEEM Power also increases the amount of curation rewards and interest that you can earn. More SP also grants more influence on approving Steem witnesses. + +**Powering down** - If you have STEEM Power, you can power down to turn it into liquid STEEM over a period of time. The system will transfer 1/13 of your STEEM Power to STEEM each week for about three months (13 weeks), starting 1 week from the time it is started. However, you will lose your influence in the network proportionally to how much is powered down, so think about it carefully. Power downs can be stopped at any time. + +^ + +## What do the dollar amounts for pending payouts represent? + +The dollar amounts next to posts and comments are estimates of the potential payout that will occur when the payout period ends, based on the current voting activity and price of STEEM. These potential payout amounts may fluctuate up or down until the payout period ends. + +Payouts occur as a combination of STEEM Power and Steem Dollars. Sometimes the blockchain may substitute STEEM in place of the Steem Dollars based on market conditions. + +The blockchain estimates the dollar value of STEEM and STEEM Power based on the 3.5 day average price of STEEM reported by the witnesses. The blockchain assumes Steem Dollars are worth approximately one USD. + +^ + +## Will 1 Steem Dollar always be worth $1.00 USD? + +The market value of a Steem Dollar is dictated by the supply and demand of the token. Therefore it is possible for 1 SBD to be worth more or less than 1 USD depending on market conditions. However, the network's SBD conversion feature serves as a mechanism to hold Steem Dollars within a small margin of the value of USD. + +^ + +## How do Steem Dollar to STEEM conversions work? + +If you convert Steem Dollars to STEEM on the Wallet page, the blockchain will process the transaction over a period of 3.5 days. At the end of the 3.5 days, the SBD will be gone and replaced by approximately $1 USD worth of STEEM tokens. The "approximately 1 USD worth of STEEM tokens" is based on the median STEEM price over the 3.5 days, using the price feeds from the Steem witnesses. Depending on price fluctuations during the 3.5 days it is possible to end up with more or less than $1 USD worth of STEEM per SBD at the end of the conversion. + +^ + +## Is there a way for me to convert my Steem Dollars to STEEM without waiting 3.5 days? + +You can exchange them. Visit the internal Market, found in the main menu. There you can exchange your SBD for STEEM in real-time at whatever the current market price is. + +Depending on market conditions, users may get more STEEM for their SBD by trading them for STEEM on the internal market, rather than using the conversion. + +^ + +## What can I do with my STEEM tokens? + +- "Power Up" to STEEM Power +- Exchange for SBD in the internal market +- Withdraw to an exchange, and trade for BTC or other digital tokens +- Purchase items through third-party stores that accept STEEM tokens + +^ + +## What can I do with my SBD tokens? + +- Hold them as a stable-value token +- Convert to STEEM via your wallet (takes 3.5 days) +- Exchange for STEEM in the internal market +- Withdraw to an exchange, and trade for BTC or other digital tokens +- Purchase items through third-party stores that accept SBD tokens + +^ + +## What is a MVEST? + +A VEST is a unit of measurement for STEEM Power. A MVEST is one million VESTS. The amount of STEEM Power in one MVEST can be found on steemd.com as `steem_per_mvests`. + +^ + +## Can I sell goods and services on Steemit? + +Other than making a post and making sales manually, there is no interface for selling items directly on Steemit.com. You can list goods on the third-party website PeerHub.com. Through PeerHub, you can accept payment in Steem Dollars or STEEM, and you have the option to advertise your items through Steemit posts. + +^ + +## How can I withdraw my STEEM or SBD coins? + +STEEM and SBD tokens are readily tradable to bitcoin, which is readily tradable to the local currency of your choice. There is a link to "Sell" your STEEM and SBD tokens in your wallet, which uses the BlockTrades interface. + +There are several guides that have been posted by users in the community for using various external exchanges to withdraw STEEM and SBD tokens. Please read the disclaimer before using any of these guides to withdraw your coins. The users, guides, and exchanges listed in the guides are not endorsed by Steemit, Inc. Use the guides below at your own risk. + +It is recommended that you withdraw a small amount first, to verify it works before withdrawing a larger amount. + +#### Sell Steem Dollars via Poloniex +https://steemit.com/steemit/@ash/steemit-how-to-sell-steem-dollars-via-poloniex-newbie-friendly + +#### Withdraw Steem Dollars to a Bitcoin address +https://steemit.com/steem-help/@piedpiper/how-to-withdraw-your-steem-dollars-in-less-that-a-minute + +#### Convert Steem Dollars to a country’s currency and withdraw to a bank account +https://steemit.com/tutorial/@beanz/how-to-get-my-usdteemit-money-into-my-bank-account + +#### Convert STEEM to many other cryptocurrencies via ShapeShift +https://steemit.com/steemit/@shapeshiftio/official-announcement-shapeshift-has-added-steem-to-the-exchange + +^ + +## Will I get a 1099 from Steemit? + +No, you are not being paid by Steemit. The Steem network rewards you. It is your responsibility to determine what, if any, taxes apply to the transactions you make. Further, it is your responsibility to report and remit the correct tax to the appropriate tax authority. By creating an account, you agree that Steemit Inc is not responsible for determining whether taxes apply to your Steem transactions or for collecting, reporting, withholding, or remitting any taxes arising from any Steem transactions. + +^ + +## How much are the transaction fees for sending tokens to other users? + +There are never any fees for transfers within the Steem network. However, if you transfer Steem to an exchange and convert it to another currency, you will incur a small fee from the exchange. + +^ + +## Are there fees for Powering Up, Powering Down, trading on the internal market, or converting SBD to STEEM? + +No. None of these actions incur any fees. + +^ + +## How long does it take to transfer STEEM or SBD tokens between users? + +A transfer of tokens between accounts typically takes 3 seconds. This is far faster than most blockchain tokens. + +^ +# Voting and Curating + + +## What is my voting power? + +Voting power is like an "energy bar" in a computer game that goes down a little bit every time you vote. You start out with 100% voting power. Every time you vote, you will use a small amount of your voting power. + +As you use more of your voting power, your votes will carry less influence. A vote with 50% voting power left will be worth 1/2 as much as a vote cast with 100% voting power. Not to worry, the network recharges your voting power by 20% every day. + +^ + +## How many times can I vote without depleting my voting power? + +Every 100% vote you cast will use 2% of your remaining voting power. Your voting power will recharge by 20% each day. You can vote more than 10 times per day, but each vote will be worth less, and it will take longer to reach full voting power again. + +^ + +## Can I vote with less than 100% of my voting strength? + +New users can only upvote and downvote with 100% voting strength. + +Once you reach about 500 STEEM Power, you will see a vote slider appear when you vote. You can use the slider to adjust the weight of your vote, between 1% and 100% voting strength. Voting with less than 100% voting weight will use up less voting power, but it will also have less of an influence on the post or comment's rewards. + + + +Upvotes and downvotes use the same amount of voting power. + +^ + +## Where can I check my voting power? + +You can view your current voting power using third party tools such as https://steemd.com/@youraccount or https://steemstats.com. + +^ + +## What determines how much of the curation reward goes to the author versus curators? + +The rewards are allocated so that 75% of the payout goes to the author of the post/comment, and 25% goes to the curator. + +Of the 25% that goes to the curator, that portion will be split between the author and the curator if the curator votes within the first 30 minutes. The split of the 25% between the author and curator during the first 30 minutes is calculated linearly based on the time the vote is cast. + +- If a post is upvoted the moment of posting, 100% of the curation reward goes to the author. +- At 3 minutes, 90% goes to the author and 10% to the curator. +- At 15 minutes it's a 50/50 split. +- At 27 minutes, 10% goes to the author and 90% to the curator. +- If a post is upvoted 30 min after posting, 100% of the curation reward goes to the curator. + +^ + +## Can I get curation rewards for upvoting comments? + +Yes. You can earn curation rewards from upvoting both posts and comments! + +^ + +## Do I get curation rewards for downvoting posts or comments? + +No. Since downvoting reduces the rewards on a post/comment, it does not earn curation rewards. + +^ + +## What are curation trails? + +Some users decide to use third party applications such as Streemian to automatically cast votes. Users can automatically vote for the same posts and comments that other users does. Typically they will set this up to follow the votes of users who are good at curating. When a user has other users automatically voting for the same content that they do, the people that automatically vote after them are called their "curation trail". + +^ + +## Why don't my upvotes have an effect on a post's rewards? + +A user with more SP is going to have a larger influence on the rewards than users with less SP. One vote from a user with a lot of SP can often have more of an effect than 100 votes from users with a small amount of SP. + +Even though your vote may not have an immediate effect, when it gets added in along with all the other votes at the end of the payout period, it can still affect the payout. It may also cause more users to vote on the post too, because they saw that you upvoted it - so your votes can have an indirect effect on the payout this way. + +^ + +## Is there a way to make my votes count for more? + +Yes. The more STEEM Power you have, the more influence your votes will have. + +The platform does not require that anybody purchase SP in order to participate, and there are many users who have earned a lot of STEEM Power without spending any of their own money. You have the option of purchasing more STEEM Power through your Steemit wallet. + +^ + +## What are the valid reasons for downvoting? + +Users are allowed to downvote for any reason that they want. There are many users in the community who recommend only using the downvote on posts that are abusive. It is up to you if you want to follow this etiquette. + +^ + +## Does a downvote mean that I did something wrong? + +Just because you received a downvote does not mean that you did something wrong. The downvoting person may have just been voting to reallocate the rewards in a way that they felt was more beneficial to the other active posts in the platform. Often users will leave a comment explaining why they downvoted, but sometimes they might not. If they left a reason, it is up to you to determine if you did anything wrong, and if there is anything you want to change. + +^ + +## Will a downvote hurt my reputation? + +Not necessarily. See: What causes my reputation score to go down? + +^ + +## What is the difference between a downvote and a flag? + +With the current implementation, there is no difference between a downvote and a flag. They are treated the same at the blockchain level. + +^ +# Plagiarism, Spam, and Abuse + + +## What are Steemit’s policies on plagiarism and spam? + +If you are posting plagiarized or copied content, you can get in legal trouble for violating copyright laws. Plagiarized posts and spam are seen as abuse and will be downvoted by community members. If you are posting or using someone else’s content, you must ensure that you have the rights to use the content, and properly reference the sources where you got the material from. + +^ + +## Is it okay to use random pictures from the internet? + +If you are using an image that is not your own, make sure you are allowed to use the image, and cite the source of the image. + +Using random pictures from the internet without giving credit is discouraged. You may, however, use photos from “free image” websites such as Pexels.com or Pixabay.com. All photos on Pexels and Pixabay are free for personal and commercial use. + +Here is a post from @mindover that has links to many websites that have images you can use: +https://steemit.com/steem-help/@mindover/don-t-plagiarize-images-here-are-13-free-and-legal-ways-to-find-high-quality-photos-you-can-use-on-steemit + +^ + +## What is Steemcleaners? + +Steemcleaners are a group of Steemians concerned with plagiarism, copy/paste, spam, scams and other forms of abuse on Steemit. +https://steemit.com/steemcleaners/@steemcleaners/announcing-steemcleaners-the-steemit-abuse-fighting-team + +^ + +## What is @cheetah? + +@cheetah is a bot developed by @anyx that scours Steemit for copy/pasted content. Cheetah will not downvote copied content, but it alerts other users to look into it further. + +Abusive accounts (serial plagiarists or identity thieves, for example) will go on Cheetah’s blacklist. These users will get downvoted by @cheetah accounts when they post. + +More information on the @cheetah bot can be found in this post: +https://steemit.com/steemit/@cheetah/faq-about-cheetah + +^ + +## Where do I report a post or comment that contains plagiarism, spam, or abuse? + +You can report any abusive content to the #steemitabuse channel on steemit.chat. + +^ +# Reputation + + +## What is Reputation? + +Every user has a reputation score next to their name. The reputation score is one way Steemit measures the amount of value you have brought to the community. It is also a mechanism that is designed to help reduce abuse of the Steemit platform. + +Your reputation goes up when accounts vote on your content. Getting downvoted by someone with a higher reputation can push your reputation down and make your posts less visible. + +Users with a lower reputation score are unable to affect your reputation. + +^ + +## How is the Reputation score measured? + +Every new user starts off with a reputation score of 25. + +The reputation score is based off of a `log10` system, which means that a score of 40 is about 10x better than a score of 30. + +More information about the calculation of the reputation score can be found in this post from @digitalnotvir: +https://steemit.com/steemit/@digitalnotvir/how-reputation-scores-are-calculated-the-details-explained-with-simple-math + +^ + +## How do I improve my reputation score? + +Every time another user upvotes one of your posts or comments, it increases your reputation score. Users with a higher reputation than you will have more of a positive effect. The more STEEM Power that the voter has, the larger the effect is as well. The best way to earn upvotes is by adding value to the Steemit community. + +^ + +## What causes my reputation score to go down? + +The only way for your reputation score to go down is to be downvoted by another user. Not all downvotes will cause a reputation loss though. + +- Downvotes from users with a lower reputation score than you will not hurt your score. +- If your post or comment that was downvoted still received more upvotes than downvotes (weighted by SP), then the net effect on your reputation score will still be positive. + +^ + +## Why does my reputation score matter? + +A reputation score is one way Steemit measures the amount of value you have brought to the community. In real estate, they say there are three variables of the utmost importance: location, location, location. On Steemit, those things are: reputation, reputation, reputation. It’s not to say other variables aren’t important, but reputation will be an enormous factor in your level of success. + +Many Steemians glance at users’ reputation scores when deciding which articles to read because they know higher reputation scores means it is much more likely quality content. Furthermore, the higher your rep, the more effect your vote will have on the reputation of others. + +It is worth noting that if your reputation score goes below 0, Steemit will hide your posts and comments making it very difficult to gain monetary rewards and followers. This incentivizes online etiquette and respect for your fellow Steemians. + +^ +# Followers, Feeds, and Resteem + + +## What is Resteeming? + +This is like reblogging or sharing posts on other platforms. Once you resteem a post it will appear in your feed and in your followers' feeds as if you had posted it yourself. Use it conservatively and with caution. It is great to want to share content you like and appreciate with people you follow, but you don't want to overwhelm your followers either. + +^ + +## Can I share on other social media? + +Yes you can use the share button to share on Facebook, Twitter or LinkedIn. You are welcome to post your Steemit links on other websites and social media sites. + +^ +# Blockchain + + +## What is a blockchain? + +A blockchain is a public ledger of all transactions ever executed. All of the transactions and data are stored in a distributed database. Each time the database is updated, all of updates are done together in a batch called a 'block'. Each time a new block is produced/added, it is appended on to all of the previous blocks - hence the name "blockchain". + +^ + +## What is the Steem blockchain? + +The Steem blockchain is the publicly accessible distributed database, which records all posts and votes, and distributes the rewards across the network. It is where all of the text content and voting data is stored, and it is where all of the reward calculations and payouts are performed. + +^ + +## What is the difference between Steem and Steemit? + +Steem is the name of the blockchain that stores all of the data and transactions, and processes all of the events that take place. STEEM is also a name for the system’s value token (currency). + +Steemit is a front end web interface to interact with the blockchain, and view the blockchain data. + +^ + +## How is Steem different from Bitcoin? + +On a technical level, the two networks rely on the same model of a blockchain, but are built upon different technologies and codebase. Steem is based on a new state-of-the-art blockchain technology called Graphene, which uses "witnesses" instead of "miners" to produce blocks. + +The "delegated proof of stake" model of using witnesses instead of miners allows for greater efficiency in block production. With BTC, 100% of the new coins that are created are allocated to block producers (miners). With the Steem blockchain, only 10% of the new coins are paid to block producers (witnesses). The other 90% of new STEEM coins are awarded to content producers, curators, and STEEM Power holders. + +^ + +## What is the difference between Proof of Work, Proof of Stake, and Delegated Proof of Stake? + +**Proof of work** - Miners solve a complex mathematical problem. The miner that solves the problem first adds the block to the blockchain. The network rewards the miner for doing so. + +**Proof of stake** - Requires ownership, or stake, in the cryptocurrency. The more tokens you own, the more block creation power you have. Benefits: eliminates the need for expensive mining rigs, runs on a tiny fraction of the power, and it requires block producers to have a stake in the network. + +**Delegated proof of stake** - Block-creating accounts, called witnesses, are collectively approved by Steem stakeholders. Instead of relying on proof of work to find blocks, the Steem network actively schedules these accounts to improve the time between blocks to 3 seconds. + +^ + +## How often does the Steem blockchain produce a new block? + +The Steem blockchain schedules witnesses to produce a new block every 3 seconds. 21 witness nodes produce 21 blocks in each 63-second round. + +^ + +## Is there a way to see the raw data that is stored in the blockchain? + +Yes. The blockchain data can be viewed in different ways with third-party tools such as steemd.com and steemdb.com. + +^ + +## Where can I find the information for the official launch of the blockchain? + +The original launch of Steem was on March 23, 2016, announced on Bitcointalk.org. There was a bug found in the original code though, and a majority of the stakeholders agreed that it would be easier to fix via a re-launch than a hardfork. The blockchain was reset and officially re-launched on March 24, 2016, via Bitcointalk.org. + +^ + +## Can I mine STEEM? + +No. Proof of work mining has been removed from Steem. + +^ +# Steemit, Inc. + + +## Who is the CEO of Steemit? + +Ned Scott, @ned +https://www.linkedin.com/in/nedscott + +^ + +## Can I invest in Steemit? + +Steemit, Inc. is a privately held company and is not available for public investment. + +Though not considered an investment, you can purchase STEEM tokens which can go up or down in value. You can power up these tokens into STEEM Power, which grants more influence in the Steem platform. + +^ + +## What does Steemit’s development roadmap look like? + +You can view the 2017 Roadmap here: +https://steemit.com/steemit/@steemitblog/steemit-2017-roadmap + +^ + +## Am I allowed to use the Steemit logo? + +Currently, the Steem and Steemit logos are the same and is free to use. In the future, Steemit, Inc. will have its own logo so that it can be distinguished from Steem. The Steemit logo will be proprietary while Steem and its three S-shaped squiggles will remain open for public use. + +^ + +## Can I purchase official Steemit merchandise? + +Yes. Official Steemit merchandise can be purchased from [The Steemit Shop](https://thesteemitshop.com/). + +^ + +## Did Steemit "pre-mine" tokens? + +The STEEM tokens mined by Steemit, Inc. were not "pre-mined". All mining took place after the coin was officially and publicly announced on Bitcointalk.org. + +^ + +## What is the Steemit Privacy Policy? + +https://steemit.com/privacy.html + +^ +# Security + + +## How can I keep my Steem account secure? + +Save your master password and keep it somewhere safe. + +Only log into your account using the key with the appropriate permissions for what you are doing: +- Posting key for every day logins +- Active key when necessary for transfers, power ups, etc. +- Master password or owner key when changing the password + +Again, save your master password and keep it safe! If logging in with your post key, make sure you don't overwrite or misplace your original master password. + +It is not recommended to share your password or keys with any third party site. Steemit Inc. is developing a login application that can be used on third party Steem front ends. + +^ + +## Why should I be careful with my master password? + +The master password is used to derive all keys for your account, including the owner key. + +^ + +## Why is the master password a long string of gibberish? + +The password has to be long and random for maximum account security. + +^ + +## What are my different keys for? + +**Posting key** - The posting key allows accounts to post, comment, edit, vote, resteem, and follow or mute other accounts. Most users should be logging into Steemit every day with the posting key. You are more likely to have your password or key compromised the more you use it so a limited posting key exists to restrict the damage that a compromised account key would cause. + +**Active key** - The active key is meant for more sensitive tasks such as transferring funds, power up/down transactions, converting Steem Dollars, voting for witnesses, updating profile details and avatar, and placing a market order. + +**Memo key** - Currently the memo key is not used. + +**Owner key** - The owner key is only meant for use when necessary. It is the most powerful key because it can change any key of an account, including the owner key. Ideally it is meant to be stored offline, and only used to recover a compromised account. + +^ + +## What do I do if I lost my password/keys? + +There is no way to recover your account if you lose your password or owner key! Because your account has real value, it is **very important** that you save your master password somewhere safe where you will not lose it. + +It is strongly recommended that you store an offline copy of your password somewhere safe in case of a hard drive failure or other calamity. Consider digital offline storage, such as an external disk or flash drive, as well as printed paper. Use a safe deposit box for best redundancy. + +^ + +## Are my STEEM and Steem Dollar tokens insured in the event of a hack or if someone takes over my account? + +No, liquid tokens can not be taken back if stolen or sent to the wrong account. If your tokens are in STEEM Power, it is impossible for a hacker to take out more than 1/13 per week. If your tokens are in savings, there is a three-day wait period for them to become transferable. + +^ + +## What should I do if I discover that someone hacked my account? + +If you made your account through Steemit and it is compromised, immediately visit the Stolen Account Recovery page. This link is also available in the main site menu. You will need to provide the email address that you used when you signed up, your account name, and a master password that was used in the last 30 days. + +^ + +## How does the stolen account recovery process work? + +If your password has been changed without your consent, then the account designated as your recovery account can generate a new owner key for the account. The account recovery must be completed within 30 days of the password being changed, and you must supply a recent owner key that was valid within the last 30 days. + +Steemit Inc. owns the default recovery account (@steem) for all users who sign up using Steemit.com. Steemit can identify users by their original email, Facebook, or Reddit logins that were used to signup via Steemit.com. + +If you don't have the master password or owner key that was valid the past 30 days, or are unable to prove that you are the original owner of the account, then your account will be unrecoverable. + +^ + +## How do I report a security vulnerability? + +If you find a security issue please report the details to security@steemit.com. + +^ +# Developers + + +## Are the Steem blockchain and Steemit.com code open-source? + +Yes. Both the Steem blockchain and Steemit.com are open-source projects. + +Developers should however avoid the use of the term "Steemit" in their own products, and instead refer to the Steem Blockchain or Steem Platform. Steemit refers to Steemit.com, which is owned by Steemit, Inc. + +^ + +## Is there a Github page for Steemit.com? + +https://github.com/steemit/condenser + +^ + +## Is there a Github page for the Steem blockchain? + +https://github.com/steemit/steem + +^ + +## What is available for developers interested in Steem and Steemit? + +Many software engineers are currently leveraging the open-source code to build their applications on Steem. There are more than sixty so far. + +This post from the user @fabien has more information about the Steem API: +https://steemit.com/steemjs/@fabien/steem-api-now-released + +^ + +## How do I use cli_wallet? + +Here is a guide from the user @pfunk explaining how to use the cli_wallet: +https://steemit.com/steemhelp/@pfunk/a-learner-s-guide-to-using-steem-s-cliwallet-part-1 + +^ +# Witnesses + + +## What are Steem witnesses? + +The Steem blockchain requires a set of people to create blocks and uses a consensus mechanism called delegated proof of stake, or DPOS. The community elects 'witnesses' to act as the network's block producers and governance body. There are 20 full-time witnesses, producing a block every 63-second round. A 21st position is shared by backup witnesses, who are scheduled proportionally to the amount of stake-weighted community approval they have. Witnesses are compensated with STEEM Power for each block they create. + +Steemit leverages Steem because the founders of Steemit believe Steem’s decentralized text content storage and governance model makes Steem an excellent platform for supporting the long term success of its social network and digital currency tokens. + +^ + +## How can I vote for witnesses? + +Visit https://steemit.com/~witnesses + +^ + +## How many witnesses can I vote for? + +Each account can vote for up to 30 witnesses. + +^ +# Miscellaneous + + +## What third-party tools are there for Steemit? + +http://steemtools.com/ + +^ + +## Is there an official Steemit Facebook page? + +https://www.facebook.com/steemit/ + +^ + +## Is there an official Steemit Twitter account? + +https://twitter.com/steemit + +^ + +## What is the Steem Whitepaper and what is its purpose? + +The Steem Whitepaper was written to describe the mechanics of the token system that makes decentralized content incentives and distribution possible in a way that can improve web technologies across the board. It is also applicable to Steemit, the first website to plug into the Steem blockchain. Users who have read the Steem Whitepaper will better understand how their interactions with Steemit are interactions with Steem, the decentralized network. + +It is worth noting that the Whitepaper hasn’t been updated almost since Steem came into existence. Many changes have been made since then, so much of the Whitepaper is now out of date. It is in the process of being rewritten. + +https://steem.io/SteemWhitePaper.pdf + +^ + +## Where can I ask for help if my question was not answered here? + +If you post your question in the #help channel on steemit.chat, the users there may be able to help. + +You can also create a post on Steemit.com with the tag #help, and someone in the community may be able to answer it. + +^ + +# Disclaimer + + +## Third Party References and User Links + +BlockTrades, Poloniex, Bittrex, Changelly, Shapeshift.io, Coinbase, Localbitcoins, SteemDB, PeerHub, Steemit.chat, SteemTools, AnonSteem, SteemConnect, Streemian, SteemStats, Pixabay, Steemcleaners, Pexels, Postimage, Markdown Cheatsheet, @cheetah, Bitcointalk, bitcoinfees, blockchain.info, and steemd are third party applications/services, and are not owned or maintained by Steemit, Inc. Their listing here, as well as any other third party applications or websites that are listed, does not constitute and endorsement or recommendation on behalf of Steemit, Inc. + +All links to user posts were created by our users and do not necessarily represent the views of Steemit, Inc. or its management. Their listing here does not constitute and endorsement or recommendation on behalf of Steemit, Inc. + +Please use the third party tools and content at your own risk. + +^ diff --git a/src/app/help/en/welcome.md b/src/app/help/en/welcome.md new file mode 100644 index 0000000..a94d1fc --- /dev/null +++ b/src/app/help/en/welcome.md @@ -0,0 +1,378 @@ +## Welcome to Steemit! + +This page is full of information to help you learn about the platform and become a successful Steemian. You can return to this page at any time by clicking on the "Welcome" link in the main menu. There is a table of contents below to help you navigate the page. + +Included on the page is a "Quick Start Guide" with information on how the platform works, and a "To Do List" with recommended steps to get started with your account. + +Below that is a section of "Helpful Posts from Steemit Users", which contains a collection of posts from users in the community that are helpful for new users getting started. + +Below that is a list of recommended users to follow, a collection of other resources including the FAQ Page, and information on where to find live help. + + +## Table of Contents + +### Quick Start Guide + +- No Cost to Participate +- Upvotes +- Comments +- Creating Posts +- Tags +- Followers and Feeds +- Resteem +- Digital Currencies +- Curation +- Payments +- Home, New, Hot, Trending, Promoted, and Active +- Profile +- Reputation +- Cashing out or Spending SBD +- Plagiarism +- Password Security +- Earning on Steemit + +### To Do List + +1. Backup your password +2. Sign Up for Steemit Chat +3. Setup your Profile, Avatar, and Cover Image +4. Choose your "NSFW" (Not Safe for Work) Display Preference +5. Create your "introduceyourself" post + +### Helpful Posts from Steemit Users +### Users to Follow +### Other Resources +### Live Help +### Third Party References + +*** + +## Quick Start Guide + + +### No Cost to Participate + +It is free to post, comment, or upvote all content on Steemit.com. You might even get paid for it! + +^ + +### Upvotes + +Upvotes are Steemit's way of saying you like someone's post or comment. + +To upvote, click on the *Upvote* icon at the bottom of the comment/post. + +^ + +### Comments + +When you are first starting out, commenting on other people's posts can be a great way to get involved and connect with people! + +To comment on a post, or reply to an existing comment, click on the "Reply" link at the bottom of the post/comment. + +^ + +### Creating Posts + +To create a post, click on the "Post" link in the upper right corner. + +Posts have three main parts: Title, Content, Tags. + +You will want to make your title attention grabbing, and relevant to your content. + +To create your content, you can either use "Editor" or "Markdown" mode. + +There are several guides for creating posts in the "Helpful Posts from Steemit Users" section below. + +^ + +### Tags + +Tags will help people find your posts. + +Each post can have up to five tags, separated by spaces. + +The first tag in the list will be the main category that the post is in. + +The tags should all be relevant to the content in the post. + +You can browse content by tags, as well as see a list of popular tags that other users have used in their posts [here](https://steemit.com/tags). + +^ + +### Followers and Feeds + +To follow an author, click on their username and click the "Follow" button. + +Once you follow someone, all of their posts will show up in your "Feed" on the homepage when you login. + +As other Steemians come across your posts and comments, you will start to gain followers. + +You can see all of your followers and the people you are following in your profile page. + +^ + +### Resteem + +If you want to share someone else's post with all of your followers, click on the *resteem* icon. + +^ + +### Digital Currencies + +STEEM, Steem Power and Steem Dollars are the three forms of digital currency used by the Steem Blockchain. + +More information on the three types of tokens can be found in the [Steemit FAQ](https://steemit.com/faq.html). + +^ + +### Curation + +Up to 25% of the reward for posts goes to the people who voted on it. These people are called curators. + +The more Steem Power you have in your account, the more your upvotes will be worth, and the more potential curation rewards you can earn! + +^ + +### Payments + +Payouts are made 7 days after the post/comment is created. You can claim your rewards in your wallet after 7 days. + +The payments may fluctuate (up and down) until the final payment is reached. + +Payments for posts are split between the author (at least 75%) and the curators (up to 25%). + +The author reward is paid 50% in Steem Power, and 50% in liquid STEEM/SBD. + +Authors also have the option to decline payout, or be paid in 100% Steem Power! + +^ + +### Home, New, Hot, Trending, Promoted, and Active + +These are various ways to sort blog posts. + +**Home** - Most recent posts of the people you follow (your feed). + +**New** - Posts are sorted by the time posted, newest first. + +**Hot** - Popular posts at the moment. + +**Trending** - Posts with the highest pending rewards currently. + +**Promoted** - Listings that are boosted by Steem Dollar payments get "Promoted" for greater visibility. + +^ + +### Profile + + + +**Feed** - Here is where you go to see the most recent posts from the people you follow. + +**Blog** - Here is where you go to see all of your posts and resteems. + +**Comments** - Here is where you go to see all of the comments you have made to other's posts and comments. + +**Replies** - Here is where you go to see all replies other users have made to your posts and comments. + +**Wallet** - Here is where you go to see your wallet balances, make transfers, exchange STEEM/SBD, and Power Up. + +**Change Password** - Here is where you go to change your password. + +**Settings** - Here is where you go to update your settings. + +**Logout** - Here is where you go to logout. + +^ + +### Reputation + +A reputation score is one way Steemit measures the amount of value you have brought to the community. + +The higher the number, the more weighted votes an account has earned. + +All new users start at 25. + +Your reputation will go up as you earn upvotes for your posts and comments, but it can come down if they are flagged. + +^ + +### Cashing out or Spending SBD + +You can spend your SBD at the [Peerhub Store](https://www.peerhub.com/). + +You can exchange your STEEM and SBD for bitcoin on an exchange such as [BlockTrades](https://blocktrades.us/) or [Bittrex](https://bittrex.com/). + +You can also "Power Up" and use your STEEM/SBD to gain more Steem Power! + +^ + +### Plagiarism + +The community is looking for you to add your own personal touch to your articles. + +Plagiarizing, that is posting someone else's work as if it were your own, is very frowned upon by the Steemit community. + +If you are using anyone else's material as part of your posts (including images) - please cite your sources. + +Also, make sure that you are not violating any copyright laws if you are using someone else's material/images. Limited, sourced material sharing is OK under fair use and fair dealing doctrines. + +^ + +### Password Security + +Your Steemit account is worth real money. Treat your Steemit password like you would your bank password, and keep it secure! + +Unless your password was recently changed and you possess the old one, **there is no password recovery for Steem accounts**. You are 100% responsible for having it backed up. This means secure digital backups, as well as secured paper backups, off-site if possible. + +^ + +### Earning on Steemit + +The best attitude to have is to expect to make nothing. Have fun. Get engaged. Make friends. If along the way you earn something - bonus! + +It is possible to earn thousands of dollars, but most authors who are doing this have put in a lot of time and work to contribute to the community and build followings. + +^ + +*** + +## To Do List + + +### 1. Backup your password + +Unlike centralized web services, **the Steem Blockchain has no account password recovery**. + +You are entirely responsible for keeping your password, and keeping it secure. + +Save your master key and keep it somewhere safe. + +It is strongly recommended that you store an offline copy of your password somewhere safe in case of a hard drive failure or other calamity. Consider digital offline storage, such as a flash drive or burned CD, as well as printed paper. Use a safe deposit box for best redundancy. + +If your account is valuable, treat it like a valuable! + +^ + +### 2. Sign Up for Steemit Chat + +A lot of users hang out and chat when they are not posting or browsing Steemit. It is a great place to meet people! + +There is a link to sign up in the main menu in the upper right corner. + +Your [steemit.chat](https://steemit.chat/home) account is a separate account from your Steem account. + +Some channels allow you to share links, but others don't. For instance, [general](https://steemit.chat/channel/general) is for discussion without link promotion, while [postpromotion](https://steemit.chat/channel/postpromotion) is for promoting your Steemit posts. + +Each channel will have its rules posted in the "Info" section. + +^ + +### 3. Setup your Profile, Avatar, and Cover Image + +Under your user settings, you can update your profile. This includes your display name, location, about info, and website. + +To set your avatar image, type or paste a link to the URL where the image is located into the "Profile Picture URL" field. + +To set your cover image, type or paste a link to the URL where the image is located into the "Cover Image URL" field. + +Once you have made all your changes, click the "Update" button to save your profile. + +^ + +### 4. Choose your "NSFW" (Not Safe for Work) Display Preference + +By default, content that users have tagged as "NSFW" will be hidden, but a link will be shown to reveal the content. + +You can update your display preference so that NSFW content is always shown by default, or is completely hidden with no option to reveal. + +^ + +### 5. Create your "introduceyourself" post + +While not required, the tradition for new users is to create an "introduceyourself" post, to let the community know who you are. + +You can see some examples of what other people have done [here](https://steemit.com/trending30/introduceyourself). + +It is not required, but a lot of users will take a picture of themselves holding up a piece of paper that says "Steemit" with the current date, so we know you are a real person. + +It is not required either, but if you have other social media accounts (Twitter, Facebook, etc.) you can help the community verify that you are who you say you are, by sharing the link to your Steemit introduceyourself post with those accounts. If you are claiming to be someone famous, this is pretty much expected. + +^ + +*** + + +## Helpful Posts from Steemit Users + +- [Posting and Markdown Basics](https://steemit.com/steemit/@thecryptofiend/markdown-basics-for-beginners) +- [Tons of Ways to Spend Your Hard Earned STEEM/SBD](https://steemit.com/steem/@timcliff/the-steem-economy-tons-of-ways-to-spend-your-hard-earned-steem-sbd) +- [Advice on How To Build a Following](https://steemit.com/steemit/@allasyummyfood/how-i-build-a-following-of-160-k-from-scratch) +- [Steemit Etiquette Guide](https://steemit.com/steemit/@thecryptofiend/the-complete-steemit-etiquette-guide-revision-2-0) +- [Advice on using Tags](https://steemit.com/steemit/@rok-sivante/the-top-3-reasons-to-tag-your-steemit-posts-a-simple-quick-strategy-for-long-term-profits-and-success) +- [The Ultimate Guide To Steemit Payouts](https://steemit.com/steemit/@shenanigator/the-ultimate-guide-to-steemit-payouts) +- [Steemit Succeeds if We Make it Succeed](https://steemit.com/steemit/@krnel/steemit-succeeds-if-we-make-it-succeed) +- [Professional Formatting Tutorial](https://steemit.com/writing/@minion/professional-tutorial-for-post-formatting-both-for-beginners-and-advanced-users) +- [Introduceyourself Example](https://steemit.com/almost-famous/@teamsteem/hello-steemit-coinmaketcap-com-introduced-me) +- [The Steemit Newbie's Comprehensive Guide To Understanding Your Wallet](https://steemit.com/steemit-help/@merej99/the-steemit-newbie-s-comprehensive-guide-to-understanding-your-wallet-by-merej99) +- [Witness Voting Guide](https://steemit.com/witness-category/@timcliff/witness-voting-guide) +- [Steemit for Artists](https://steemit.com/art/@voronoi/steemit-for-artists-a-new-stage-for-craft) +- [Steemit Chat Guide](https://steemit.com/steemit/@firepower/dummies-guide-to-using-steemit-chat-effectively-everyday) +- [How to use Blocktrades to exchange STEEM/SBD for BTC](https://steemit.com/steem/@thecryptofiend/how-to-use-blocktrades-the-fastest-and-easiest-way-to-buy-and-sell-steem-sd-and-my-review-of-the-experience) +- [SBD to bitcoin (BTC) using Blocktrades, then BTC to Euro or USD Using CEX.io](https://steemit.com/howto/@future24/blocktrades-tutorial-part-2-how-to-exchange-sbd-to-btc-and-sell-them-for-euro-or-usd-english-german) +- [A Guide To Cashing Out Your STEEM/SBD Using PayPal](https://steemit.com/tutorial/@son-of-satire/a-minnows-guide-to-cashing-out-your-steem-sbd-using-paypal-without-using-coinbase-and-their-invasive-identity-checks) +- [How to Make Your Presence Known](https://steemit.com/steem-help/@papa-pepper/let-your-presence-be-known-advice-from-papa-for-newer-users) +- [Emojis for Steemit](https://steemit.com/emojis/@blueorgy/steemit-emojis-master-list) +- [5 Markdown Tips](https://steemit.com/steemit/@steemitguide/markdown-and-html-code-guide-for-beginners-that-includes-5-useful-tips-to-edit-posts-and-comments-using-the-raw-editor) +- [How to Get Noticed if You Are a New User on Steemit](https://steemit.com/steemit/@thecryptofiend/how-to-get-noticed-if-you-are-a-new-user-on-steemit) +- [Curation Rewards Explained](https://steemit.com/curation/@liberosist/mind-your-votes-ii-a-guide-to-maximizing-your-curation-rewards) +- [More Information on Curation Rewards](https://steemit.com/steemit/@calamus056/curation-rewards-explained-in-great-detail) +- [Everything you need to know about potential payouts and flagging](https://steemit.com/payout/@timcliff/everything-you-need-to-know-about-potential-payouts-and-flagging-for-new-users) +- [The Ultimate Guide of Tips for New Steem Users](https://steemit.com/steem-help/@timcliff/the-ultimate-guide-of-tips-for-new-steem-users) +- [Ways to Find Free and Legal Images](https://steemit.com/steem-help/@mindover/don-t-plagiarize-images-here-are-13-free-and-legal-ways-to-find-high-quality-photos-you-can-use-on-steemit) +- [Steem Keys and Passwords Guide](https://steemit.com/steemit-guides/@pfunk/a-user-s-guide-to-the-different-steem-keys-or-passwords) +- [Unwritten Rules of Steemit](https://steemit.com/steem-help/@beanz/the-unwritten-rules-of-steemit) +- [Advice on How to Succeed on Steemit: Come to Give, Not to Take](https://steemit.com/psychology/@shenanigator/come-to-give-not-take-the-power-of-reciprocity) +- [Best Practices for Steemit Artists](https://steemit.com/art/@merej99/best-practices-for-steemit-newbie-artists-by-merej99-a-100-power-up-post) +- [STEEM The Super Basic Explanation](https://steemit.com/steem/@thecryptofiend/steem-the-super-basic-explanation-for-new-users) +- [Blogging Tools](https://steemit.com/blogging/@munteanu/blogging-tools) +- [How to Create Different Types of Blog Content](https://steemit.com/writing/@jessicanicklos/how-to-create-different-types-of-blog-content-know-here-total-guide-line) + +^ + +## Users to Follow + +- @steemitblog - Official Steemit Announcements +- @ned - Ned Scott, CEO and Co-Founder of Steemit + +^ + +## Other Resources + +- [FAQ](https://steemit.com/faq.html) +- [Steemit Help](https://www.steemithelp.net/) +- [The Steem Whitepaper](https://steem.io/SteemWhitePaper.pdf) +- [Steem App Center](http://steemtools.com/) +- [Steem Block Explorer](https://steemd.com/) +- [Steem Blockchain Explorer](https://steemdb.com/) + +^ + +## Live Help + +Ask your general questions in the [help](https://steemit.chat/channel/help) channel of [steemit.chat](https://steemit.chat/home). Users in the channel will typically respond to questions within a few hours. + +New Member Support Community is a group of people dedicated to helping new users find their way around Steemit. You can find them in the [New Member Support Community](https://discord.gg/HYj4yvw) channel of Discord Chat. + +^ + +## Third Party References + +Peerhub, BlockTrades, Bittrex, Steemit Chat, Steemit Help, New Member Support Community, and Discord Chat, as well as the tools listed under "Other Resources" are third party applications/services, and are not owned or maintained by Steemit, Inc. Their listing here does not constitute and endorsement or recommendation on behalf of Steemit, Inc. + +All of the links in the "Helpful Posts from Steemit Users" section were created by our users and do not necessarily represent the views of Steemit, Inc. or its management. + +Please use the third party tools and content at your own risk. + +^ diff --git a/src/app/locales/README.md b/src/app/locales/README.md new file mode 100644 index 0000000..a02f660 --- /dev/null +++ b/src/app/locales/README.md @@ -0,0 +1,61 @@ +# internationalization guide + +## how to add your own language + +1. copy ./en.js +2. rename it (for example jp.js) +3. translate it +5. go to server-html.jsx +6. add your locale data as it is done in https://cdn.polyfill.io script (add ',Intl.~locale.XX' at the end of url) +7. add localeData and newly translated strings as it is done in Translator.jsx (read the comments) + +## Notes for hackers and translators + +Use lower-snake-case (e.g. 'keep_syntax_lowercase_with_dashes') for all translation string names. + +Example: show_password: 'Show Password' + +Please keep in mind that none of the strings are bound directly to components to keep them reusable. + +For example: 'messages_count' can be used in one page today, but also can be placed in another tomorrow. + +Strings must be as close to source as possible. + +They must keep proper structure because "change_password" can translate both 'Change Password' and 'Change Account Password'. + +Same with "user hasn't started posting" and "user hasn't started posting yet", 'user_hasnt_followed_anything_yet' and 'user_hasnt_followed_anything' are not the same. + +### About syntax rules + +Do not use anything to style strings unless you are 100% sure what you are doing. + +This is no good: 'LOADING' instead of 'Loading'. + +Avoid whitespace styling: ' Loading' - is no good. + +Also, try to avoid using syntax signs, unless it's a long ass string which will always end with a dot no matter where you put it. Example: 'Loading...', 'Loading!' is no good, use 'Loading' instead. + +If you are not sure which syntax to use and how to write your translations just copy the way the original string has been written. + +### How to use plurals + +Plurals are strings which look different depending on which numbers are used. + +We use formatJs syntax, read the docs http://formatjs.io/guides/message-syntax/ + +Pay special attention to '{plural} Format' section. +[This link](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html) shows how they determine which plural to use in different languages (they explain how string falls under 'few', 'many' and 'other' category. If you are completely lost, just look at the other translation files (en.js or ru.js). + +IMPORTANT: if you use the wrong rules for pluralization, the translation will fail '{postCount, plural, zero {0 постов} one {# пост} few {# поста} many {# постов}}' instead of using a normal string. In that case, check what kind of rules your language needs. For example: Russian needs "zero", "one", "few", "many". But english only needs 'zero', 'one', 'more than one'. + +### How to use special symbols + +`\n` means new line break + +`\'` means `'` (single quote sign) + +this works: `'hasn\'t'`, `"hasn't"` (double quotes != single quotes) + +this does not: `'hasn't'` + +Some languages require certain strings to be empty. For example, Russian in some contexts does not have an equivalent for 'by' (e.g. 'Post created by Misha'). For empty strings use `' '` (empty quotes with single space) instead of `''`, otherwise you will see string name instead of nothing. diff --git a/src/app/locales/counterpart/es.js b/src/app/locales/counterpart/es.js new file mode 100644 index 0000000..a433728 --- /dev/null +++ b/src/app/locales/counterpart/es.js @@ -0,0 +1,30 @@ +// The translations in this file are added by default. + +'use strict'; + +module.exports = { + counterpart: { + names: require('date-names/en'), + pluralize: require('pluralizers/en'), + + formats: { + date: { + 'default': '%a, %e %b %Y', + long: '%A, %B %o, %Y', + short: '%b %e' + }, + + time: { + 'default': '%H:%M', + long: '%H:%M:%S %z', + short: '%H:%M' + }, + + datetime: { + 'default': '%a, %e %b %Y %H:%M', + long: '%A, %B %o, %Y %H:%M:%S %z', + short: '%e %b %H:%M' + } + } + } +}; diff --git a/src/app/locales/counterpart/fr.js b/src/app/locales/counterpart/fr.js new file mode 100644 index 0000000..a433728 --- /dev/null +++ b/src/app/locales/counterpart/fr.js @@ -0,0 +1,30 @@ +// The translations in this file are added by default. + +'use strict'; + +module.exports = { + counterpart: { + names: require('date-names/en'), + pluralize: require('pluralizers/en'), + + formats: { + date: { + 'default': '%a, %e %b %Y', + long: '%A, %B %o, %Y', + short: '%b %e' + }, + + time: { + 'default': '%H:%M', + long: '%H:%M:%S %z', + short: '%H:%M' + }, + + datetime: { + 'default': '%a, %e %b %Y %H:%M', + long: '%A, %B %o, %Y %H:%M:%S %z', + short: '%e %b %H:%M' + } + } + } +}; diff --git a/src/app/locales/counterpart/it.js b/src/app/locales/counterpart/it.js new file mode 100644 index 0000000..88edb1d --- /dev/null +++ b/src/app/locales/counterpart/it.js @@ -0,0 +1,30 @@ +// The translations in this file are added by default. + +'use strict'; + +module.exports = { + counterpart: { + names: require('date-names/en'), + pluralize: require('pluralizers/en'), + + formats: { + date: { + 'default': '%a, %e %b %Y', + long: '%A, %B %o, %Y', + short: '%e %b' + }, + + time: { + 'default': '%H:%M', + long: '%H:%M:%S %z', + short: '%H:%M' + }, + + datetime: { + 'default': '%a, %e %b %Y %H:%M', + long: '%A, %B %o, %Y %H:%M:%S %z', + short: '%e %b %H:%M' + } + } + } +}; diff --git a/src/app/locales/en.json b/src/app/locales/en.json new file mode 100644 index 0000000..fb8f02f --- /dev/null +++ b/src/app/locales/en.json @@ -0,0 +1,650 @@ +{ + "g": { + "age": "age", + "amount": "Amount", + "and": "and", + "are_you_sure": "Are you sure?", + "ask": "Ask", + "balance": "Balance", + "balances": "Balances", + "bid": "Bid", + "blog": "Blog", + "browse": "Browse", + "buy": "Buy", + "buy_or_sell": "Buy or Sell", + "by": "by", + "cancel": "Cancel", + "change_password": "Change Password", + "choose_language": "Choose Language", + "clear": "Clear", + "close": "Close", + "collapse_or_expand": "Collapse/Expand", + "comments": "Comments", + "confirm": "Confirm", + "convert": "Convert", + "date": "Date", + "delete": "Delete", + "dismiss": "Dismiss", + "edit": "Edit", + "email": "Email", + "feed": "Feed", + "follow": "Follow", + "for": " for ", + "from": " from ", + "go_back": "Back", + "hide": "Hide", + "in": "in", + "in_reply_to": "in reply to", + "insufficient_balance": "Insufficient balance", + "invalid_amount": "Invalid amount", + "joined": "Joined", + "loading": "Loading", + "login": "Login", + "logout": "Logout", + "memo": "Memo", + "mute": "Mute", + "new": "new", + "newer": "Newer", + "next": "Next", + "no": "No", + "ok": "Ok", + "older": "Older", + "or": "or", + "order_placed": "Order placed", + "password": "Password", + "payouts": "Payouts", + "permissions": "Permissions", + "phishy_message": "Link expanded to plain text; beware of a potential phishing attempt", + "post": "Post", + "post_as": "Post as", + "posts": "Posts", + "powered_up_100": "Powered Up 100%%", + "preview": "Preview", + "previous": "Previous", + "price": "Price", + "print": "Print", + "promote": "Promote", + "promoted": "promoted", + "re": "RE", + "re_to": "RE: %(topic)s", + "recent_password": "Recent Password", + "receive": "Receive ", + "remove": "Remove", + "remove_vote": "Remove Vote", + "replied_to": "replied to %(account)s", + "replies": "Replies", + "reply": "Reply", + "reply_count": { + "zero": "no replies", + "one": "1 reply", + "other": "%(count)s replies" + }, + "reputation": "Reputation", + "reveal_comment": "Reveal Comment", + "request": "request", + "required": "Required", + "rewards": "Rewards", + "save": "Save", + "saved": "Saved", + "search": "Search", + "sell": "Sell", + "settings": "Settings", + "share_this_post": "Share this post", + "show": "Show", + "sign_in": "Sign in", + "sign_up": "Sign up", + "since": "since", + "submit": "Submit", + "power_up": "Power Up", + "submit_a_story": "Post", + "tag": "Tag", + "to": " to ", + "topics": "Topics", + "toggle_nightmode": "Toggle Night Mode", + "all_tags": "All tags", + "transfer": "Transfer ", + "trending_topics": "Trending Topics", + "type": "Type", + "unfollow": "Unfollow", + "unmute": "Unmute", + "unknown": "Unknown", + "upvote": "Upvote", + "upvote_post": "Upvote post", + "username": "Username", + "version": "Version", + "vote": "Vote", + "votes": "votes", + "wallet": "Wallet", + "warning": "warning", + "yes": "Yes", + "posting": "Posting", + "owner": "Owner", + "active": "Active", + "account_not_found": "Account not found", + "this_is_wrong_password": "This is the wrong password", + "do_you_need_to": "Do you need to", + "account_name": "Account Name", + "recover_your_account": "recover your account", + "reset_usernames_password": "Reset %(username)s's Password", + "this_will_update_usernames_authtype_key": "This will update %(username)s %(authType)s key", + "passwords_do_not_match": "Passwords do not match", + "you_need_private_password_or_key_not_a_public_key": "You need a private password or key (not a public key)", + "the_rules_of_APP_NAME": { + "one": "The first rule of %(APP_NAME)s is: Do not lose your password.", + "second": "The second rule of %(APP_NAME)s is: Do not lose your password.", + "third": "The third rule of %(APP_NAME)s is: We cannot recover your password.", + "fourth": "The fourth rule: If you can remember the password, it's not secure.", + "fifth": "The fifth rule: Use only randomly-generated passwords.", + "sixth": "The sixth rule: Do not tell anyone your password.", + "seventh": "The seventh rule: Always back up your password." + }, + "recover_password": "Recover Account", + "current_password": "Current Password", + "generated_password": "Generated Password", + "backup_password_by_storing_it": "Back it up by storing in your password manager or a text file", + "enter_account_show_password": "Enter a valid account name to show the password", + "click_to_generate_password": "Click to generate password", + "re_enter_generate_password": "Re-enter Generated Password", + "understand_that_APP_NAME_cannot_recover_password": "I understand that %(APP_NAME)s cannot recover lost passwords", + "i_saved_password": "I have securely saved my generated password", + "update_password": "Update Password", + "confirm_password": "Confirm Password", + "account_updated": "Account Updated", + "password_must_be_characters_or_more": "Password must be %(amount)s characters or more", + "need_password_or_key": "You need a private password or key (not a public key)", + "login_to_see_memo": "login to see memo", + "new_password": "New Password", + "incorrect_password": "Incorrect password", + "username_does_not_exist": "Username does not exist", + "account_name_should_start_with_a_letter": "Account name should start with a letter.", + "account_name_should_be_shorter": "Account name should be shorter.", + "account_name_should_be_longer": "Account name should be longer.", + "account_name_should_have_only_letters_digits_or_dashes": "Account name should have only letters, digits, or dashes.", + "cannot_increase_reward_of_post_within_the_last_minute_before_payout": "Cannot increase reward of post within the last minute before payout", + "vote_currently_exists_user_must_be_indicate_a_to_reject_witness": "vote currently exists, user must be indicate a desire to reject witness", + "only_one_APP_NAME_account_allowed_per_ip_address_every_10_minutes": "Only one Steem account allowed per IP address every 10 minutes", + "resteem_this_post": "Resteem This Post", + "reblog": "Resteem", + "write_your_story": "Write your story", + "remember_voting_and_posting_key": "Remember voting & posting key", + "auto_login_question_mark": "Auto login?", + "hide_private_key": "Hide private key", + "show_private_key": "Show private key", + "login_to_show": "Login to show", + "not_valid_email": "Not valid email", + "thank_you_for_being_an_early_visitor_to_APP_NAME": "Thank you for being an early visitor to %(APP_NAME)s. We will get back to you at the earliest possible opportunity.", + "author_rewards": "Author rewards", + "curation_rewards": "Curation rewards", + "sorry_your_reddit_account_doesnt_have_enough_karma": "Sorry, your Reddit account doesn't have enough Reddit Karma to qualify for a free sign up. Please add your email for a place on the waiting list", + "register_with_facebook": "Register with Facebook", + "or_click_the_button_below_to_register_with_facebook": "Or click the button below to register with Facebook", + "server_returned_error": "server returned error", + "APP_NAME_support": "%(APP_NAME)s Support", + "please_email_questions_to": "Please email your questions to", + "next_7_strings_single_block": { + "authors_get_paid_when_people_like_you_upvote_their_post": "Authors get paid when people like you upvote their post", + "if_you_enjoyed_what_you_read_earn_amount": "If you enjoyed what you read here, create your account today and start earning FREE STEEM!", + "free_steem": "FREE STEEM!", + "sign_up_earn_steem": "Sign up now to earn " + }, + "next_3_strings_together": { + "show_more": "Show more", + "show_less": "Show fewer", + "value_posts": "low value posts" + }, + "read_only_mode": "Due to server maintenance we are running in read only mode. We are sorry for the inconvenience.", + "tags_and_topics": "Tags", + "show_more_topics": "View all tags", + "basic": "Basic", + "advanced": "Advanced", + "views": { + "zero": "No Views", + "one": "%(count)s View", + "other": "%(count)s Views" + }, + "responses": { + "zero": "No Responses", + "one": "%(count)s Response", + "other": "%(count)s Responses" + }, + "post_key_warning": { + "confirm": "You are about to publish a STEEM private key or master password. You will probably lose control of the associated account and all its funds.", + "warning": "Legitimate users, including employees of Steemit Inc., will never ask you for a private key or master password.", + "checkbox": "I understand" + } + }, + "navigation": { + "about": "About", + "explore": "Explore", + "APP_NAME_whitepaper": "%(APP_NAME)s Whitepaper", + "buy_LIQUID_TOKEN": "Buy %(LIQUID_TOKEN)s", + "sell_LIQUID_TOKEN": "Sell %(LIQUID_TOKEN)s", + "currency_market": "Currency Market", + "stolen_account_recovery": "Stolen Accounts Recovery", + "change_account_password": "Change Account Password", + "witnesses": "Witnesses", + "vote_for_witnesses": "Vote for Witnesses", + "privacy_policy": "Privacy Policy", + "terms_of_service": "Terms of Service", + "sign_up": "Join", + "learn_more": "Learn More", + "welcome": "Welcome", + "faq": "FAQ", + "shop": "The Steemit Shop", + "chat": "Steemit Chat", + "app_center": "Steemit App Center", + "api_docs": "Steemit API Docs", + "bluepaper": "Steem Bluepaper", + "whitepaper": "Steem Whitepaper", + "intro_tagline": "Money talks", + "intro_paragraph": "Your voice is worth something. Join the community that pays you to post and curate high quality content." + }, + "main_menu": { + "hot": "hot", + "trending": "trending" + }, + "reply_editor": { + "shorten_title": "Shorten title", + "exceeds_maximum_length": "Exceeds maximum length (%(maxKb)sKB)", + "including_the_category": " (including the category '%(rootCategory)s')", + "use_limited_amount_of_tags": "You have %(tagsLength)s tags total%(includingCategory)s. Please use only 5 in your post and category line.", + "are_you_sure_you_want_to_clear_this_form": "Are you sure you want to clear this form?", + "uploading": "Uploading", + "draft_saved": "Draft saved.", + "editor": "Editor", + "insert_images_by_dragging_dropping": "Insert images by dragging & dropping, ", + "pasting_from_the_clipboard": "pasting from the clipboard, ", + "selecting_them": "selecting them", + "image_upload": "Image upload", + "power_up_100": "Power Up 100%%", + "default_50_50": "Default (50%% / 50%%)", + "decline_payout": "Decline Payout", + "check_this_to_auto_upvote_your_post": "Check this to auto-upvote your post", + "markdown_styling_guide": "Markdown Styling Guide", + "or_by": "or by", + "title": "Title", + "update_post": "Update Post", + "markdown_not_supported": "Markdown is not supported here" + }, + "category_selector_jsx": { + "tag_your_story": "Tag (up to 5 tags), the first tag is your main category.", + "select_a_tag": "Select a tag", + "maximum_tag_length_is_24_characters": "Maximum tag length is 24 characters", + "use_limited_amount_of_categories": "Please use only %(amount)s categories", + "use_only_lowercase_letters": "Use only lowercase letters", + "use_one_dash": "Use only one dash", + "use_spaces_to_separate_tags": "Use spaces to separate tags", + "use_only_allowed_characters": "Use only lowercase letters, digits and one dash", + "must_start_with_a_letter": "Must start with a letter", + "must_end_with_a_letter_or_number": "Must end with a letter or number" + }, + "postfull_jsx": { + "this_post_is_not_available_due_to_a_copyright_claim": "This post is not available due to a copyright claim.", + "share_on_facebook": "Share on Facebook", + "share_on_twitter": "Share on Twitter", + "share_on_linkedin": "Share on Linkedin", + "recent_password": "Recent Password", + "in_week_convert_DEBT_TOKEN_to_LIQUID_TOKEN": "In 3.5 days, convert %(amount)s %(DEBT_TOKEN)s into %(LIQUID_TOKEN)s", + "view_the_full_context": "View the full context", + "view_the_direct_parent": "View the direct parent", + "you_are_viewing_a_single_comments_thread_from": "You are viewing a single comment's thread from" + }, + "market_jsx": { + "action": "Action", + "date_created": "Date Created", + "last_price": "Last price", + "24h_volume": "24h volume", + "spread": "Spread", + "total": "Total", + "available": "Available", + "lowest_ask": "Lowest ask", + "highest_bid": "Highest bid", + "buy_orders": "Buy Orders", + "sell_orders": "Sell Orders", + "trade_history": "Trade History", + "open_orders": "Open Orders", + "sell_amount_for_atleast": "Sell %(amount_to_sell)s for at least %(min_to_receive)s (%(effectivePrice)s)", + "buy_atleast_amount_for": "Buy at least %(min_to_receive)s for %(amount_to_sell)s (%(effectivePrice)s)", + "price_warning_above": "This price is well above the current market price of %(marketPrice)s, are you sure?", + "price_warning_below": "This price is well below the current market price of %(marketPrice)s, are you sure?", + "order_cancel_confirm": "Cancel order %(order_id)s from %(user)s?", + "order_cancelled": "Order %(order_id)s cancelled.", + "higher": "Higher", + "lower": "Lower", + "total_DEBT_TOKEN_SHORT_CURRENCY_SIGN": "Total %(DEBT_TOKEN_SHORT)s (%(CURRENCY_SIGN)s)" + }, + "recoveraccountstep1_jsx": { + "begin_recovery": "Begin Recovery", + "not_valid": "Not valid", + "account_name_is_not_found": "Account name is not found", + "unable_to_recover_account_not_change_ownership_recently": "We are unable to recover this account, it has not changed ownership recently.", + "password_not_used_in_last_days": "This password was not used on this account in the last 30 days.", + "request_already_submitted_contact_support": "Your request has been already submitted and we are working on it. Please contact %(SUPPORT_EMAIL)s for the status of your request.", + "recover_account_intro": "From time to time, a Steemian's owner key may be compromised. Stolen Account Recovery gives the rightful account owner 30 days to recover their account from the moment the thief changed their owner key. Stolen Account Recovery can only be used on %(APP_URL)s if the account owner had previously listed '%(APP_NAME)s' as their account trustee and complied with %(APP_NAME)s's Terms of Service.", + "login_with_facebook_or_reddit_media_to_verify_identity": "Please login with Facebook or Reddit to verify your identity", + "login_with_social_media_to_verify_identity": "Please login with %(provider)s to verify your identity", + "enter_email_toverify_identity": "We need to verify your identity. Please enter your email address below to begin the verification.", + "continue_with_email": "Continue with Email", + "thanks_for_submitting_request_for_account_recovery": "Thanks for submitting your request for Account Recovery using %(APP_NAME)s's blockchain-based multi factor authentication. We will respond to you as quickly as possible, however, please expect there may be some delay in response due to high volume of emails. Please be prepared to verify your identity.", + "recovering_account": "Recovering account", + "recover_account": "Recover Account", + "checking_account_owner": "Checking account owner", + "sending_recovery_request": "Sending recovery request", + "cant_confirm_account_ownership": "We can't confirm account ownership. Check your password", + "account_recovery_request_not_confirmed": "Account recovery request is not confirmed yet, please get back later, thank you for your patience." + }, + "user_profile": { + "unknown_account": "Unknown Account", + "user_hasnt_made_any_posts_yet": "Looks like %(name)s hasn't made any posts yet!", + "user_hasnt_started_bloggin_yet": "Looks like %(name)s hasn't started blogging yet!", + "user_hasnt_followed_anything_yet": "Looks like %(name)s might not be following anyone yet! If %(name)s recently added new users to follow, their personalized feed will populate once new content is available.", + "user_hasnt_had_any_replies_yet": "%(name)s hasn't had any replies yet", + "looks_like_you_havent_posted_anything_yet": "Looks like you haven't posted anything yet.", + "create_a_post": "Create a Post", + "explore_trending_articles": "Explore Trending Articles", + "read_the_quick_start_guide": "Read The Quick Start Guide", + "browse_the_faq": "Browse The FAQ", + "followers": "Followers", + "this_is_users_reputations_score_it_is_based_on_history_of_votes": "This is %(name)s's reputation score.\n\nThe reputation score is based on the history of votes received by the account, and is used to hide low quality content.", + "follower_count": { + "zero": "No followers", + "one": "1 follower", + "other": "%(count)s followers" + }, + "followed_count": { + "zero": "Not following anybody", + "one": "1 following", + "other": "%(count)s following" + }, + "post_count": { + "zero": "No posts", + "one": "1 post", + "other": "%(count)s posts" + } + }, + "authorrewards_jsx": { + "estimated_author_rewards_last_week": "Estimated author rewards last week", + "author_rewards_history": "Author Rewards History" + }, + "curationrewards_jsx": { + "estimated_curation_rewards_last_week": "Estimated curation rewards last week", + "curation_rewards_history": "Curation Rewards History" + }, + "post_jsx": { + "now_showing_comments_with_low_ratings": "Now showing comments with low ratings", + "sort_order": "Sort Order", + "comments_were_hidden_due_to_low_ratings": "Comments were hidden due to low ratings" + }, + "voting_jsx": { + "flagging_post_can_remove_rewards_the_flag_should_be_used_for_the_following": "Flagging a post can remove rewards and make this material less visible. Some common reasons to flag", + "disagreement_on_rewards": "Disagreement on rewards", + "fraud_or_plagiarism": "Fraud or Plagiarism", + "hate_speech_or_internet_trolling": "Hate Speech or Internet Trolling", + "intentional_miss_categorized_content_or_spam": "Intentional miss-categorized content or Spam", + "pending_payout": "Pending Payout $%(value)s", + "payout_declined": "Payout Declined", + "max_accepted_payout": "Max Accepted Payout $%(value)s", + "promotion_cost": "Promotion Cost $%(value)s", + "past_payouts": "Past Payouts $%(value)s", + "past_payouts_author": " - Author $%(value)s", + "past_payouts_curators": " - Curators $%(value)s", + "we_will_reset_curation_rewards_for_this_post": "will reset your curation rewards for this post", + "removing_your_vote": "Removing your vote", + "changing_to_an_upvote": "Changing to an Up-Vote", + "changing_to_a_downvote": "Changing to a Down-Vote", + "confirm_flag": "Confirm Flag", + "and_more": "and %(count)s more", + "votes_plural": { + "one": "%(count)s vote", + "other": "%(count)s votes" + } + }, + "witnesses_jsx": { + "witness_thread": "witness thread", + "top_witnesses": "Witness Voting", + "you_have_votes_remaining": { + "zero": "You have no votes remaining", + "one": "You have 1 vote remaining", + "other": "You have %(count)s votes remaining" + }, + "you_can_vote_for_maximum_of_witnesses": "You can vote for a maximum of 30 witnesses", + "witness": "Witness", + "information": "Information", + "if_you_want_to_vote_outside_of_top_enter_account_name": "If you would like to vote for a witness outside of the top 50, enter the account name below to cast a vote", + "set_witness_proxy": "You can also choose a proxy that will vote for witnesses for you. This will reset your current witness selection.", + "witness_set": "You have set a voting proxy. If you would like to re-enable manual voting, please clear your proxy.", + "witness_proxy_current": "Your current proxy is", + "witness_proxy_set": "Set proxy", + "witness_proxy_clear": "Clear proxy", + "proxy_update_error": "Your proxy was not updated" + }, + "votesandcomments_jsx": { + "no_responses_yet_click_to_respond": "No responses yet. Click to respond.", + "response_count_tooltip": { + "zero": "no responses. Click to respond.", + "one": "1 response. Click to respond.", + "other": "%(count)s responses. Click to respond." + }, + "vote_count": { + "zero": "no votes", + "one": "1 vote", + "other": "%(count)s votes" + } + }, + "userkeys_jsx": { + "public": "Public", + "private": "Private", + "public_something_key": "Public %(key)s Key", + "private_something_key": "Private %(key)s Key", + "posting_key_is_required_it_should_be_different": "The posting key is used for posting and voting. It should be different from the active and owner keys.", + "the_active_key_is_used_to_make_transfers_and_place_orders": "The active key is used to make transfers and place orders in the internal market.", + "the_owner_key_is_required_to_change_other_keys": "The owner key is the master key for the account and is required to change the other keys.", + "the_private_key_or_password_should_be_kept_offline": "The private key or password for the owner key should be kept offline as much as possible.", + "the_memo_key_is_used_to_create_and_read_memos": "The memo key is used to create and read memos." + }, + "suggestpassword_jsx": { + "APP_NAME_cannot_recover_passwords_keep_this_page_in_a_secure_location": "%(APP_NAME)s cannot recover passwords. Keep this page in a secure location, such as a fireproof safe or safety deposit box.", + "APP_NAME_password_backup": "%(APP_NAME)s Password Backup", + "APP_NAME_password_backup_required": "%(APP_NAME)s Password Backup (required)", + "after_printing_write_down_your_user_name": "After printing, write down your user name" + }, + "converttosteem_jsx": { + "your_existing_DEBT_TOKEN_are_liquid_and_transferable": "Your existing %(DEBT_TOKEN)s are liquid and transferable. Instead you may wish to trade %(DEBT_TOKEN)s directly in this site under %(link)s or transfer to an external market.", + "this_is_a_price_feed_conversion": "This is a price feed conversion. The 3.5 day delay is necessary to prevent abuse from gaming the price feed average", + "convert_to_LIQUID_TOKEN": "Convert to %(LIQUID_TOKEN)s", + "DEBT_TOKEN_will_be_unavailable": "This action will take place 3.5 days from now and can not be canceled. These %(DEBT_TOKEN)s will immediately become unavailable" + }, + "tips_js": { + "liquid_token": "Tradeable tokens that may be transferred anywhere at anytime.
      %(LIQUID_TOKEN)s can be converted to %(VESTING_TOKEN)s in a process called powering up.", + "influence_token": "Influence tokens which give you more control over post payouts and allow you to earn on curation rewards.", + "estimated_value": "The estimated value is based on an average value of %(LIQUID_TOKEN)s in US dollars.", + "non_transferable": "%(VESTING_TOKEN)s is non-transferable and requires 3 months (13 payments) to convert back to %(LIQUID_TOKEN)s.", + "converted_VESTING_TOKEN_can_be_sent_to_yourself_but_can_not_transfer_again": "Converted %(VESTING_TOKEN)s can be sent to yourself or someone else but can not transfer again without converting back to %(LIQUID_TOKEN)s.", + "part_of_your_steem_power_is_currently_delegated": "Part of your STEEM POWER is currently delegated to you. Delegation is donated for influence or to help new users perform actions on steemit. Your delegation amount can fluctuate." + }, + "promote_post_jsx": { + "promote_post": "Promote Post", + "spend_your_DEBT_TOKEN_to_advertise_this_post": "Spend your %(DEBT_TOKEN)s's to advertise this post in the promoted content section", + "you_successfully_promoted_this_post": "You successfully promoted this post", + "this_post_was_hidden_due_to_low_ratings": "This post was hidden due to low ratings" + }, + "about_jsx": { + "about_app": "About %(APP_NAME)s", + "about_app_details": "%(APP_NAME)s is a social media platform where everyone gets paid for creating and curating content. It leverages a robust digital points system, called Steem, that supports real value for digital rewards through market price discovery and liquidity.", + "learn_more_at_app_url": "Learn more at %(APP_URL)s", + "resources": "Resources" + }, + "markdownviewer_jsx": { + "images_were_hidden_due_to_low_ratings": "Images were hidden due to low ratings." + }, + "postsummary_jsx": { + "resteemed": "Resteemed", + "resteemed_by": "Resteemed by", + "reveal_it": "Reveal this post", + "adjust_your": "adjust your", + "display_preferences": "display preferences", + "create_an_account": "create an account", + "to_save_your_preferences": "to save your preferences" + }, + "posts_index": { + "empty_feed_1": "Looks like you haven't followed anything yet", + "empty_feed_2": "If you recently added new users to follow, your personalized feed will populate once new content is available", + "empty_feed_3": "Explore Trending Articles", + "empty_feed_4": "Read The Quick Start Guide", + "empty_feed_5": "Browse The FAQ" + }, + "transferhistoryrow_jsx": { + "to_savings": "to savings", + "from_savings": "from savings", + "cancel_transfer_from_savings": "Cancel transfer from savings", + "stop_power_down": "Stop power down", + "start_power_down_of": "Start power down of", + "receive_interest_of": "Receive interest of" + }, + "savingswithdrawhistory_jsx": { + "cancel_this_withdraw_request": "Cancel this withdraw request?", + "pending_savings_withdrawals": "PENDING SAVINGS WITHDRAWS", + "withdraw": "Withdraw %(amount)s", + "to": "to %(to)s", + "from_to": "from %(from)s to %(to)s" + }, + "explorepost_jsx": { + "copied": "Copied!", + "copy": "COPY", + "alternative_sources": "Alternative Sources" + }, + "header_jsx": { + "home": "home", + "create_a_post": "Create a post", + "change_account_password": "Change Account Password", + "create_account": "Create Account", + "stolen_account_recovery": "Stolen Account Recovery", + "people_following": "People following", + "people_followed_by": "People followed by", + "curation_rewards_by": "Curation rewards by", + "author_rewards_by": "Author rewards by", + "replies_to": "Replies to", + "comments_by": "Comments by" + }, + "loginform_jsx": { + "you_need_a_private_password_or_key": "You need a private password or key (not a public key)", + "cryptography_test_failed": "Cryptography test failed", + "unable_to_log_you_in": "We will be unable to log you in with this browser.", + "the_latest_versions_of": "The latest versions of ", + "are_well_tested_and_known_to_work_with": "are well tested and known to work with %(APP_URL)s.", + "due_to_server_maintenance": "Due to server maintenance we are running in read only mode. We are sorry for the inconvenience.", + "login_to_vote": "Login to Vote", + "login_to_post": "Login to Post", + "login_to_comment": "Login to Comment", + "posting": "Posting", + "active_or_owner": "Active or Owner", + "this_password_is_bound_to_your_account_owner_key": "This password is bound to your account's owner key and can not be used to login to this site.", + "however_you_can_use_it_to": "However, you can use it to ", + "update_your_password": "update your password", + "to_obtain_a_more_secure_set_of_keys": "to obtain a more secure set of keys.", + "this_password_is_bound_to_your_account_active_key": "This password is bound to your account's active key and can not be used to login to this page.", + "you_may_use_this_active_key_on_other_more": "You may use this active key on other more secure pages like the Wallet or Market pages.", + "you_account_has_been_successfully_created": "You account has been successfully created!", + "you_account_has_been_successfully_recovered": "You account has been successfully recovered!", + "password_update_succes": "The password for %(accountName)s was successfully updated", + "password_info": "This password or private key was entered incorrectly. There is probably a handwriting or data-entry error. Hint: A password or private key generated by Steemit will never contain 0 (zero), O (capital o), I (capital i) and l (lower case L) characters.", + "enter_your_username": "Enter your username", + "password_or_wif": "Password or WIF", + "this_operation_requires_your_key_or_master_password": "This operation requires your %(authType)s key or Master password.", + "keep_me_logged_in": "Keep me logged in", + "amazing_community": "amazing community", + "to_comment_and_reward_others": " to comment and reward others.", + "signup_button": "Sign up now to earn ", + "signup_button_emphasis": "FREE STEEM!", + "sign_up_get_steem": "Sign up. Get STEEM", + "returning_users": "Returning Users: ", + "join_our": "Join our" + }, + "chainvalidation_js": { + "account_name_should": "Account name should ", + "not_be_empty": "not be empty.", + "be_longer": "be longer.", + "be_shorter": "be shorter.", + "each_account_segment_should": "Each account segment should ", + "start_with_a_letter": "start with a letter.", + "have_only_letters_digits_or_dashes": "have only letters, digits, or dashes.", + "have_only_one_dash_in_a_row": "have only one dash in a row.", + "end_with_a_letter_or_digit": "end with a letter or digit.", + "verified_exchange_no_memo": "You must include a memo for your exchange transfer." + }, + "settings_jsx": { + "invalid_url": "Invalid URL", + "name_is_too_long": "Name is too long", + "name_must_not_begin_with": "Name must not begin with @", + "about_is_too_long": "About is too long", + "location_is_too_long": "Location is too long", + "website_url_is_too_long": "Website URL is too long", + "public_profile_settings": "Public Profile Settings", + "private_post_display_settings": "Private Post Display Settings", + "not_safe_for_work_nsfw_content": "Not safe for work (NSFW) content", + "always_hide": "Always hide", + "always_warn": "Always warn", + "always_show": "Always show", + "muted_users": "Muted Users", + "update": "Update", + "profile_image_url": "Profile picture url", + "cover_image_url": "Cover image url", + "profile_name": "Display Name", + "profile_about": "About", + "profile_location": "Location", + "profile_website": "Website" + }, + "transfer_jsx": { + "amount_is_in_form": "Amount is in the form 99999.999", + "insufficient_funds": "Insufficient funds", + "use_only_3_digits_of_precison": "Use only 3 digits of precison", + "send_to_account": "Send to account", + "asset": "Asset", + "this_memo_is_private": "This memo is private", + "this_memo_is_public": "This memo is public", + "convert_to_VESTING_TOKEN": "Convert to %(VESTING_TOKEN)s", + "balance_subject_to_3_day_withdraw_waiting_period": "Balance subject to 3 day withdraw waiting period,", + "move_funds_to_another_account": "Move funds to another %(APP_NAME)s account.", + "protect_funds_by_requiring_a_3_day_withdraw_waiting_period": "Protect funds by requiring a 3 day withdraw waiting period.", + "withdraw_funds_after_the_required_3_day_waiting_period": "Withdraw funds after the required 3 day waiting period.", + "from": "From", + "to": "To", + "asset_currently_collecting": "%(asset)s currently collecting %(interest)s%% APR.", + "beware_of_spam_and_phishing_links": "Beware of spam and phishing links in transfer memos. Do not open links from users you do not trust. Do not provide your private keys to any third party websites." + }, + "userwallet_jsx": { + "conversion_complete_tip": "Will complete on", + "in_conversion": "%(amount)s in conversion", + "transfer_to_savings": "Transfer to Savings", + "power_up": "Power Up", + "power_down": "Power Down", + "market": "Market", + "convert_to_LIQUID_TOKEN": "Convert to %(LIQUID_TOKEN)s", + "withdraw_LIQUID_TOKEN": "Withdraw %(LIQUID_TOKEN)s", + "withdraw_DEBT_TOKENS": "Withdraw %(DEBT_TOKENS)s", + "tokens_worth_about_1_of_LIQUID_TICKER": "Tokens worth about $1.00 of %(LIQUID_TICKER)s, currently collecting %(sbdInterest)s%% APR.", + "savings": "SAVINGS", + "estimated_account_value": "Estimated Account Value", + "next_power_down_is_scheduled_to_happen": "The next power down is scheduled to happen", + "transfers_are_temporary_disabled": "Transfers are temporary disabled.", + "history": "HISTORY", + "redeem_rewards": "Redeem Rewards (Transfer to Balance)", + "buy_steem_or_steem_power": "Buy STEEM or STEEM POWER" + }, + "powerdown_jsx": { + "power_down": "Power Down", + "amount": "Amount", + "already_power_down": "You are already powering down %(AMOUNT)s %(LIQUID_TICKER)s (%(WITHDRAWN)s %(LIQUID_TICKER)s paid out so far). Note that if you change the power down amount the payout schedule will reset.", + "delegating": "You are delegating %(AMOUNT)s %(LIQUID_TICKER)s. That amount is locked up and not available to power down until the delegation is removed and a full reward period has passed.", + "per_week": "That's ~%(AMOUNT)s %(LIQUID_TICKER)s per week.", + "warning": "Leaving less than %(AMOUNT)s %(VESTING_TOKEN)s in your account is not recommended and can leave your account in a unusable state.", + "error": "Unable to power down (ERROR: %(MESSAGE)s)" + }, + "checkloginowner_jsx": { + "your_password_permissions_were_reduced": "Your password permissions were reduced", + "if_you_did_not_make_this_change": "If you did not make this change please", + "ownership_changed_on": "Ownership Changed On ", + "deadline_for_recovery_is": "Deadline for recovery is", + "i_understand_dont_show_again": "I understand, don't show me again" + } +} diff --git a/src/app/locales/es.json b/src/app/locales/es.json new file mode 100644 index 0000000..62e5ec6 --- /dev/null +++ b/src/app/locales/es.json @@ -0,0 +1,648 @@ +{ + "g": { + "age": "edad", + "amount": "Cantidad", + "and": "y", + "are_you_sure": "¿Estás seguro?", + "ask": "Preguntar", + "balance": "Saldo", + "balances": "Saldos", + "bid": "Oferta", + "blog": "Blog", + "browse": "Navegar", + "buy": "Comprar", + "buy_or_sell": "Comprar o Vender", + "by": "por", + "cancel": "Cancelar", + "change_password": "Cambiar Contraseña", + "choose_language": "Elegir Idioma", + "clear": "Limpiar", + "close": "Cerrar", + "collapse_or_expand": "Plegar/Expandir", + "comments": "Comentarios", + "confirm": "Confirmar", + "convert": "Convertir", + "date": "Fecha", + "delete": "Borrar", + "dismiss": "Descartar", + "edit": "Editar", + "email": "Email", + "feed": "Favoritos", + "follow": "Seguir", + "for": "para", + "from": "de", + "go_back": "Volver", + "hide": "Ocultar", + "in": "en", + "in_reply_to": "en respuesta a", + "insufficient_balance": "Saldo insuficiente", + "invalid_amount": "Cantidad inválida", + "joined": "Unió", + "loading": "Cargando", + "login": "Iniciar sesión", + "logout": "Cerrar sesión", + "memo": "Memo", + "mute": "Silenciar", + "new": "nuevo", + "newer": "Nuevo", + "next": "Próximo", + "no": "No", + "ok": "Ok", + "older": "Más viejo", + "or": "o", + "order_placed": "Órden dispuesta", + "password": "Contraseña", + "payouts": "Pagos", + "permissions": "Permisos", + "phishy_message": "Link expanded to plain text; beware of a potential phishing attempt", + "post": "Publicación", + "post_as": "Publicar como", + "posts": "Publicaciones", + "powered_up_100": "Powered Up 100 %%", + "preview": "Vista previa", + "previous": "Anterior", + "price": "Precio", + "print": "Imprimir", + "promote": "Promocionar", + "promoted": "promocionado", + "re": "RE", + "re_to": "RE %(topic)s", + "recent_password": "Contraseña reciente", + "receive": "Recibir", + "remove": "Quitar", + "remove_vote": "Quitar voto", + "replied_to": "Respondido a %(account)s", + "replies": "Respuestas", + "reply": "Respuesta", + "reply_count": { + "zero": "Sin respuestas", + "one": "1 respuesta", + "other": "%(count)s respuestas" + }, + "reputation": "Reputación", + "reveal_comment": "Mostrar Comentario", + "request": "solicitud", + "required": "Requerido", + "rewards": "Recompensa", + "save": "Guardar", + "saved": "Guardado", + "search": "Buscar", + "sell": "Vender", + "settings": "Configuración", + "share_this_post": "Compartir esta publicación", + "show": "Mostrar", + "sign_in": "Registrarse", + "sign_up": "Inscribirse", + "since": "desde", + "submit": "Enviar", + "power_up": "Power Up", + "submit_a_story": "Publicar", + "tag": "Etiqueta", + "to": "hasta", + "all_tags": "All tags", + "transfer": "Transferir", + "trending_topics": "Temas Tendencia", + "type": "Tipo", + "unfollow": "Dejar de seguir", + "unmute": "Desmutear", + "unknown": "Desconocido", + "upvote": "Votar", + "upvote_post": "Votar publicación", + "username": "Nombre de usuario", + "version": "Versión", + "vote": "Voto", + "votes": "votos", + "wallet": "Monedero", + "warning": "advertencia", + "yes": "Sí", + "posting": "Publicando", + "owner": "Propietario", + "active": "Activo", + "account_not_found": "Cuenta no encontrada", + "this_is_wrong_password": "Esta es la contraseña errónea", + "do_you_need_to": "Necesitas", + "account_name": "Nombre de cuenta", + "recover_your_account": "recuperar tu cuenta", + "reset_usernames_password": "Restablecer la contraseña de %(username)s", + "this_will_update_usernames_authtype_key": "Esto actualizará la clave %(authType)s de %(username)s", + "passwords_do_not_match": "Las contraseñas no coinciden", + "you_need_private_password_or_key_not_a_public_key": "Necesitas una contraseña o clave privada (no una clave pública)", + "the_rules_of_APP_NAME": { + "one": "La primera regla de %(APP_NAME)ses: No pierdas tu contraseña.", + "second": "La segunda regla de %(APP_NAME)s es: No pierdas tu contraseña.", + "third": "La tercera regla de %(APP_NAME)s es: No podemos recuperar tu contraseña.", + "fourth": "La cuarta regla: Si puedes recordar tu contraseña, esta no es segura.", + "fifth": "La quinta regla: Usa sólo contraseñas creadas aleatoriamente.", + "sixth": "La sexta regla: No le digas a nadie tu contraseña.", + "seventh": "La séptima regla: Haz siempre una copia de seguridad de tu contraseña." + }, + "recover_password": "Recuperar Cuenta", + "current_password": "Contraseña Actual", + "generated_password": "Contraseña Generada", + "backup_password_by_storing_it": "Haz una copia de seguridad guardándolo en tu monedero de contraseñas o en un archivo de texto", + "enter_account_show_password": "Introduce un nombre de cuenta correcto para mostrar la contraseña", + "click_to_generate_password": "Confirmar Contraseña", + "re_enter_generate_password": "Reintroduce la contraseña generada", + "understand_that_APP_NAME_cannot_recover_password": "Entiendo que (nombre APP) no puede recuperar contraseñas perdidas", + "i_saved_password": "He guardado con seguridad mi contraseña generada", + "update_password": "Actualizar Contraseña", + "confirm_password": "Confirmar Contraseña", + "account_updated": "Cuenta Actualizada", + "password_must_be_characters_or_more": "La contraseña tiene que tener %(amount)s caracteres o más", + "need_password_or_key": "Necesitas una contraseña o clave privada (no una clave pública)", + "login_to_see_memo": "accede para ver el memo", + "new_password": "Nueva Contraseña", + "incorrect_password": "Contraseña Incorrecta", + "username_does_not_exist": "El nombre de usuario no existe", + "account_name_should_start_with_a_letter": "El nombre de usuario debería empezar con una letra.", + "account_name_should_be_shorter": "El nombre de usuario debería ser más corto.", + "account_name_should_be_longer": "El nombre de usuario debería ser más largo.", + "account_name_should_have_only_letters_digits_or_dashes": "El nombre de usuario debería tener sólo letras, dígitos o guiones.", + "cannot_increase_reward_of_post_within_the_last_minute_before_payout": "No se puede aumentar la recompensa de la publicación dentro del último minuto antes del pago.", + "vote_currently_exists_user_must_be_indicate_a_to_reject_witness": "Este voto ya existe, el usuario tiene que quitar el voto al witness", + "only_one_APP_NAME_account_allowed_per_ip_address_every_10_minutes": "Sólo una cuenta Steem permitida por dirección IP cada 10 minutos", + "resteem_this_post": "Reesteemea esta Publicación", + "reblog": "Reesteemear", + "write_your_story": "Escribe tu historia", + "remember_voting_and_posting_key": "Recuerda la clave de voto y de publicación", + "auto_login_question_mark": "¿Acceso automático?", + "hide_private_key": "Ocultar clave privada", + "show_private_key": "Mostrar clave privada", + "login_to_show": "Acceder para mostrar", + "not_valid_email": "Correo electrónico no válido", + "thank_you_for_being_an_early_visitor_to_APP_NAME": "Gracias por ser uno de los primeros en entrar en %(APP_NAME)s. Nos pondremos en contacto tan pronto como sea posible", + "author_rewards": "Recompensas de autor", + "curation_rewards": "Recompensas de curación", + "sorry_your_reddit_account_doesnt_have_enough_karma": "Lo sentimos pero tu cuenta de Reddit no tiene suficiente Karma para un registro gratis. Por favor, añade tu email para entrar en la lista de espera", + "register_with_facebook": "Regístrate con Facebook", + "or_click_the_button_below_to_register_with_facebook": "O haz click en el botón de abajo para registrarte con Facebook", + "server_returned_error": "el servidor devolvió el error", + "APP_NAME_support": "%(APP_NAME)s Soporte", + "please_email_questions_to": "Por favor, envía un mail con tus dudas a", + "next_7_strings_single_block": { + "authors_get_paid_when_people_like_you_upvote_their_post": "Autores cobran cuando gente como tú vota por sus posts", + "if_you_enjoyed_what_you_read_earn_amount": "Si te ha gustado lo que has leído aquí, crea tu cuenta hoy mismo y empieza a ganar STEEM!", + "free_steem": "¡STEEM GRATIS!", + "sign_up_earn_steem": "Regístrate ahora para ganar" + }, + "next_3_strings_together": { + "show_more": "Mostrar más", + "show_less": "Mostrar menos", + "value_posts": "Posts de bajo valor" + }, + "read_only_mode": "Debido a tareas de mantenimiento de los servidores, solo el modo lectura está disponible. Perdonen las molestias", + "tags_and_topics": "Etiquetas y Temas", + "show_more_topics": "Mostrar más temas", + "basic": "Básico", + "advanced": "Avanzado", + "views": { + "zero": "Ninguna visualización", + "one": "%(count)s Visualización", + "other": "%(count)s Visualizaciones" + }, + "responses": { + "zero": "Sin Respuestas", + "one": "%(count)s Respuesta", + "other": "%(count)s Respuestas" + }, + "post_key_warning": { + "confirm": "You are about to publish a STEEM private key or master password. You will probably lose control of the associated account and all its funds.", + "warning": "Legitimate users, including employees of Steemit Inc., will never ask you for a private key or master password.", + "checkbox": "I understand" + } + }, + "navigation": { + "about": "Sobre", + "explore": "Explorar", + "APP_NAME_whitepaper": "%(APP_NAME)s Documentación técnica", + "buy_LIQUID_TOKEN": "Comprar %(LIQUID_TOKEN)s", + "sell_LIQUID_TOKEN": "Vender %(LIQUID_TOKEN)s", + "currency_market": "Mercado de monedas", + "stolen_account_recovery": "Recuperación de cuentas robadas", + "change_account_password": "Cambia la contraseña de la cuenta", + "witnesses": "Testigos", + "vote_for_witnesses": "Vota a los testigos", + "privacy_policy": "Política de Privacidad", + "terms_of_service": "Términos del Servicio", + "sign_up": "Unirse", + "learn_more": "Para saber más", + "welcome": "Bienvenido", + "faq": "Preguntas Frecuentes", + "shop": "The Steemit Shop", + "chat": "Chat de Steemit", + "app_center": "Centro de Aplicaciones Steemit", + "api_docs": "Documentos API de Steemit", + "bluepaper": "Steem Bluepaper", + "whitepaper": "Libro blanco de Steem", + "intro_tagline": "El dinero habla.", + "intro_paragraph": "Tu voz tiene valor. Únete a la comunidad que te paga por publicar y votar contenido de alta calidad" + }, + "main_menu": { + "hot": "en alza", + "trending": "tendencia" + }, + "reply_editor": { + "shorten_title": "Abreviar el título", + "exceeds_maximum_length": "Excede la capacidad máxima (%(maxKb)sKB)", + "including_the_category": "(incluyendo categoría '%(rootCategory)s')", + "use_limited_amount_of_tags": "Tienes %(tagsLength)s tags totales %(includingCategory)s. Por favor usa solo 5 tags en tu post y en la línea de categorías", + "are_you_sure_you_want_to_clear_this_form": "¿Estás seguro de que quieres limpiar este formulario?", + "uploading": "Subiendo", + "draft_saved": "Borrador guardado.", + "editor": "Editor", + "insert_images_by_dragging_dropping": "Inserta imágenes arrastrando y soltando,", + "pasting_from_the_clipboard": "pegando desde el portapapeles,", + "selecting_them": "Seleccionándolos", + "image_upload": "Subir imagen", + "power_up_100": "Power Up 100%%", + "default_50_50": "Por defecto (50 1%% / 50 1%%)", + "decline_payout": "Rechazar Pago", + "check_this_to_auto_upvote_your_post": "Seleciona para autovotarte el post", + "markdown_styling_guide": "Guía para el Markdown", + "or_by": "o por", + "title": "Título", + "update_post": "Actualizar Publicación", + "markdown_not_supported": "Markdown no soportado aquí" + }, + "category_selector_jsx": { + "tag_your_story": "Etiqueta (hasta 5 etiquetas), la primera etiqueta es tu principal categoría.", + "select_a_tag": "Selecciona una etiqueta", + "maximum_tag_length_is_24_characters": "La longitud máxima de la etiqueta es de 24 caracteres", + "use_limited_amount_of_categories": "Por favor usa sólo %(amount)s categorías", + "use_only_lowercase_letters": "Usa sólo letras minúsculas", + "use_one_dash": "Usa sólo un guión", + "use_spaces_to_separate_tags": "Usa espacios para separar las etiquetas", + "use_only_allowed_characters": "Usa sólo letras minúsculas, dígitos y un guión", + "must_start_with_a_letter": "Debe empezar con una letra", + "must_end_with_a_letter_or_number": "Debe terminar con una letra o número" + }, + "postfull_jsx": { + "this_post_is_not_available_due_to_a_copyright_claim": "Este post no está disponible por reclamación de derechos de autor.", + "share_on_facebook": "Compartir en Facebook", + "share_on_twitter": "Compartir en Twitter", + "share_on_linkedin": "Compartir en Linkedin", + "recent_password": "Contraseña reciente", + "in_week_convert_DEBT_TOKEN_to_LIQUID_TOKEN": "En 3.5 días, convertir 1 %(amount)s 2 %(DEBT_TOKEN)s a 3 %(LIQUID_TOKEN)s", + "view_the_full_context": "Ver todo el contexto", + "view_the_direct_parent": "Ver el comentario relacionado", + "you_are_viewing_a_single_comments_thread_from": "Estás viendo los comentarios relacionados de" + }, + "market_jsx": { + "action": "Acción", + "date_created": "Fecha de creación", + "last_price": "Último precio", + "24h_volume": "Volumen de 24 horas", + "spread": "Difundir", + "total": "Total", + "available": "Disponible", + "lowest_ask": "Oferta más baja", + "highest_bid": "Oferta más alta", + "buy_orders": "Comprar órdenes", + "sell_orders": "Vender órdenes", + "trade_history": "Historia de compra venta", + "open_orders": "Órdenes abiertas", + "sell_amount_for_atleast": "Vender %(amount_to_sell)s al menos por %(min_to_receive)s (%(effectivePrice)s)", + "buy_atleast_amount_for": "Comprar al menos %(min_to_receive)s for %(amount_to_sell)s (%(effectivePrice)s)", + "price_warning_above": "Este precio está por debajo del precio de mercado que es %(marketPrice)s, estás seguro?", + "price_warning_below": "Este precio está por debajo del precio de mercado que es %(marketPrice)s, estás seguro?", + "order_cancel_confirm": "Cancelar órden %(order_id)s de %(user)s?", + "order_cancelled": "Órden %(order_id)s cancelada", + "higher": "Más alta", + "lower": "Más baja", + "total_DEBT_TOKEN_SHORT_CURRENCY_SIGN": "Total %(DEBT_TOKEN_SHORT)s (%(CURRENCY_SIGN)s)" + }, + "recoveraccountstep1_jsx": { + "begin_recovery": "Empezar Recuperación", + "not_valid": "No es válido", + "account_name_is_not_found": "Nombre de usuario no encontrado", + "unable_to_recover_account_not_change_ownership_recently": "No podemos recuperar esta cuenta, esta no ha cambiado su propiedad recientemente.", + "password_not_used_in_last_days": "Esta contraseña no ha sido usada en esta cuenta en los últimos 30 días.", + "request_already_submitted_contact_support": "Tu petición ha sido enviada y estamos trabajando en ella. Por favor contacta %(SUPPORT_EMAIL)s para ver el estado de la misma.", + "recover_account_intro": "Puede ocurrir que la owner key de algún usuario se vea comprometida. La funcionalidad de recuperación de contraseñas robadas le da al propietario de la cuenta 30 días para recuperar su cuenta desde el mismo momento que el delincuente ha cambiado la owner key. Esta funcionalidad sólo puede ser usada en %(APP_URL)s si el propietario de la cuenta ha incluído %(APP_NAME)s como su cuenta de confianza y además a comulgado con las condiciones y términos de servicio de %(APP_NAME)s", + "login_with_facebook_or_reddit_media_to_verify_identity": "Por favor accede con Fabebook o Reddit para verificar tu identidad", + "login_with_social_media_to_verify_identity": "Por favor entra con %(provider)s para verificar tu identidad", + "enter_email_toverify_identity": "Necesitamos verificar tu identidad. Por favor introduce tu dirección de correo electrónico debajo para empezar la verificación.", + "continue_with_email": "Continuar con correo electrónico", + "thanks_for_submitting_request_for_account_recovery": "Gracias por enviar tu petición a nuestro sistema de recuperación de contraseñas usando %(APP_NAME)s basado en la autentificación multi factor. Tan pronto como sea posible recibirás una respuesta, sim embargo, puede que haya algún retraso debido al alto volumen de peticiones. Por favor, esté preparado para verificar su identidad.", + "recovering_account": "Recuperando cuenta", + "recover_account": "Recuperar Cuenta", + "checking_account_owner": "Comprobando propietario de la cuenta", + "sending_recovery_request": "Enviando solicitud de recuperación ", + "cant_confirm_account_ownership": "No podemos confirmar la propiedad de la cuenta. Comprueba tu contraseña", + "account_recovery_request_not_confirmed": "La solicitud de recuperación de cuenta no está confirmada todavía, por favor vuelve más tarde, gracias por tu paciencia." + }, + "user_profile": { + "unknown_account": "Cuenta desconocida", + "user_hasnt_made_any_posts_yet": "Parece que %(name)s aún no ha comenzado a postear", + "user_hasnt_started_bloggin_yet": "Parece que %(name)s aún no ha comenzado su blog", + "user_hasnt_followed_anything_yet": "Parece que %(name)s aún no está siguiendo a nadie. Si %(name)s acaba de añadir nuevas cuentas para seguir, el feed personalizado aparecerá cuando el nuevo contenido esté disponible.", + "user_hasnt_had_any_replies_yet": "%(name)s todavía no ha tenido ninguna respuesta", + "looks_like_you_havent_posted_anything_yet": "Looks like you haven't posted anything yet.", + "create_a_post": "Create a Post", + "explore_trending_articles": "Explore Trending Articles", + "read_the_quick_start_guide": "Read The Quick Start Guide", + "browse_the_faq": "Browse The FAQ", + "followers": "Seguidores", + "this_is_users_reputations_score_it_is_based_on_history_of_votes": "Esto es la reputación de %(name)s's. La reputación está basada en la historia de votos recibidos por la cuenta y se usa para esconder contenido de baja calidad.", + "follower_count": { + "zero": "Sin seguidores", + "one": "1 seguidor", + "other": "%(count)s seguidores" + }, + "followed_count": { + "zero": "No sigue a nadie", + "one": "1 siguiendo", + "other": "%(count)s siguiendo" + }, + "post_count": { + "zero": "Sin publicaciones", + "one": "1 publicación", + "other": "%(count)s posts" + } + }, + "authorrewards_jsx": { + "estimated_author_rewards_last_week": "Recompensas de autor estimadas la semana pasada", + "author_rewards_history": "Historial de recompensas de autor" + }, + "curationrewards_jsx": { + "estimated_curation_rewards_last_week": "Recompensas de curación aproximadas de la semana pasada", + "curation_rewards_history": "Historial de recompensas de curación" + }, + "post_jsx": { + "now_showing_comments_with_low_ratings": "Mostrando ahora comentarios con bajas calificaciones", + "sort_order": "Clase de órden", + "comments_were_hidden_due_to_low_ratings": "Los comentarios fueron ocultados debido a las bajas calificaciones" + }, + "voting_jsx": { + "flagging_post_can_remove_rewards_the_flag_should_be_used_for_the_following": "Flagear un posts puede hacer desaparecer las recompensas y hacer el contenido menos visible. Usar el sentido común para flagear", + "disagreement_on_rewards": "Desacuerdo en las recompensas", + "fraud_or_plagiarism": "Fraude o Plagio", + "hate_speech_or_internet_trolling": "Discurso de odio o trolear en internat", + "intentional_miss_categorized_content_or_spam": "Uso erróneo de los tags o Spam", + "pending_payout": "Pago pendiente $ %(value)s", + "payout_declined": "Pago rehusado", + "max_accepted_payout": "Máximo pago aceptado $ %(value)s", + "promotion_cost": "Coste de promoción $ %(value)s", + "past_payouts": "Pagos pasados $ %(value)s", + "past_payouts_author": "Autor $ %(value)s", + "past_payouts_curators": "Curador $ %(value)s", + "we_will_reset_curation_rewards_for_this_post": "Reseteará tus recompensas de curación para este post", + "removing_your_vote": "Quitando tu voto", + "changing_to_an_upvote": "Cambiando a Up-vote", + "changing_to_a_downvote": "Cambiando a Down-Vote", + "confirm_flag": "Confirmar Flag", + "and_more": "y %(count)s más", + "votes_plural": { + "one": "%(count)s voto", + "other": "%(count)s votos" + } + }, + "witnesses_jsx": { + "witness_thread": "tema testigo", + "top_witnesses": "Votación para testigo", + "you_have_votes_remaining": { + "zero": "No tienes votos restantes", + "one": "Tienes 1 voto restante", + "other": "Te quedan %(count)s votos disponibles" + }, + "you_can_vote_for_maximum_of_witnesses": "Puedes votar como máximo a 30 testigos", + "witness": "Testigos", + "information": "Información", + "if_you_want_to_vote_outside_of_top_enter_account_name": "SI quieres votar a un testigo que está fuera de los top 50, por favor introduce el nombre de la cuenta para votarle", + "set_witness_proxy": "También puedes elegir un proxy que votará a los testigos por ti. Esta actividad reseteará tu actual selección de witness", + "witness_set": "Has seleccionado un proxy de votación. Si quieres volver a habilitar el voto manual, por favor borra el proxy.", + "witness_proxy_current": "Tu proxy actual es", + "witness_proxy_set": "Crear proxy", + "witness_proxy_clear": "Borrar proxy", + "proxy_update_error": "Tu proxy no fue actualizado" + }, + "votesandcomments_jsx": { + "no_responses_yet_click_to_respond": "Todavía sin respuestas. Pincha para responder.", + "response_count_tooltip": { + "zero": "sin respuestas. Pincha para responder.", + "one": "1 respuesta. Pincha para responder.", + "other": "%(count)s respuestas. Pincha para responder." + }, + "vote_count": { + "zero": "sin votos", + "one": "1 voto", + "other": "%(count)s votos" + } + }, + "userkeys_jsx": { + "public": "Público", + "private": "Privado", + "public_something_key": "Contraseña %(count)s pública", + "private_something_key": "Contraseña %(count)s privada", + "posting_key_is_required_it_should_be_different": "La contraseña de posteado se usa para postear y para votar. Debería ser diferente a la contraseña activa y a la contraseña de propietario.", + "the_active_key_is_used_to_make_transfers_and_place_orders": "La contraseña activa se usa para hacer transferencias y poner órdenes de compra o venta en el mercado interno.", + "the_owner_key_is_required_to_change_other_keys": "La contraseña propia es la contraseña maestra para la cuenta y se requiere para cambiar el resto de las contraseñas.", + "the_private_key_or_password_should_be_kept_offline": "La contraseña privada para la cuenta propia debería estar fuera del acceso a internet. Se recomienda imprimirla y guardarla en lugar seguro.", + "the_memo_key_is_used_to_create_and_read_memos": "La contraseña memo se usa para crear y leer memos." + }, + "suggestpassword_jsx": { + "APP_NAME_cannot_recover_passwords_keep_this_page_in_a_secure_location": "%(APP_NAME)s no puede recuperar contraseñas. Guarda está página en lugar seguro, como en una caja fuerte o una caja de depósito", + "APP_NAME_password_backup": "%(APP_NAME)s Contraseña de apoyo o seguridad", + "APP_NAME_password_backup_required": "%(APP_NAME)s Contraseña de apoyo o seguridad (requerida)", + "after_printing_write_down_your_user_name": "Después de imprimir, escribe tu nombre de usuario" + }, + "converttosteem_jsx": { + "your_existing_DEBT_TOKEN_are_liquid_and_transferable": "La cantidad de %(DEBT_TOKEN)s son líquidos y transferibles. Si lo deseas también puedes comprar o vender %(DEBT_TOKEN)s directamente en esta página en el %(link)s o transferir a un mercado externo", + "this_is_a_price_feed_conversion": "Esto es la conversión de precio. Los 3.5 días de retraso son necesarios para prevenir el abuso con las medias del precio", + "convert_to_LIQUID_TOKEN": "Convertir a %(LIQUID_TOKEN)s", + "DEBT_TOKEN_will_be_unavailable": "Esta acción tardará 3.5 días en procesarse y no puede ser cancelada. Estos %(DEBT_TOKEN)s dejarán inmediatamente de estar disponibles" + }, + "tips_js": { + "liquid_token": "Tokens canjeables pueden ser transferidos a cualquier sitio en cualquier momento.1%(LIQUID_TOKEN)s y pueden ser convertidos a %(VESTING_TOKEN)s en un proceso llamado power up.", + "influence_token": "Tokens con influencia te dan más control sobre los pagos de los posts y te permiten ganar recompensas de curación", + "estimated_value": "El valor estimado está basado en el valor medio de 1%(LIQUID_TOKEN)s en US dollars.", + "non_transferable": "%(VESTING_TOKEN)s es intransferible y requiere 3 meses (13 pagos) para convertir de vuelta a %(LIQUID_TOKEN)s.", + "converted_VESTING_TOKEN_can_be_sent_to_yourself_but_can_not_transfer_again": "Los %(VESTING_TOKEN)s convertidos pueden ser enviados a ti mismo o a otras personas pero no pueden ser transferidos de nuevo sin convertirlos de vuelta a %(LIQUID_TOKEN)s.", + "part_of_your_steem_power_is_currently_delegated": "Parte de tu STEEM POWER te ha sido delegado. La delegación es influencia donada que ayuda a los nuevos usuarios a interactuar en steemit. La cantidad de tu delegación puede fluctuar." + }, + "promote_post_jsx": { + "promote_post": "Promociona el Post", + "spend_your_DEBT_TOKEN_to_advertise_this_post": "Gasta tu %(DEBT_TOKEN)s para promocionar tu contenido en la sección de promoción", + "you_successfully_promoted_this_post": "Has promocionado satisfactoriamente este post", + "this_post_was_hidden_due_to_low_ratings": "El post ha sido ocultado por bajas calificaciones" + }, + "about_jsx": { + "about_app": "Sobre %(APP_NAME)s", + "about_app_details": "%(APP_NAME)s es una plataforma social online donde todo el mundo genera ingresos creando y votando contenido. Usa un sistema muy sólido de puntos digitales llamado Steem que soporta valor real gracias al precio de mercado y gracias a su liquidez.", + "learn_more_at_app_url": "Aprende más en %(APP_URL)s", + "resources": "Recursos" + }, + "markdownviewer_jsx": { + "images_were_hidden_due_to_low_ratings": "Las imágenes fueron ocultadas por bajas calificaciones." + }, + "postsummary_jsx": { + "resteemed": "Reesteemeado", + "resteemed_by": "Resteemeado por", + "reveal_it": "revelarlo", + "adjust_your": "ajustar tu", + "display_preferences": "Visualizar preferencias", + "create_an_account": "crea una cuenta", + "to_save_your_preferences": "para guardar tus preferencias" + }, + "posts_index": { + "empty_feed_1": "Parece que no has seguido nada todavía", + "empty_feed_2": "Si recientemente has añadido a gente para seguir, tu feed personalizado aparecerá una vez que el contenido esté disponible", + "empty_feed_3": "Explorar artículos en tendencia", + "empty_feed_4": "leer la guía básica para comenzar", + "empty_feed_5": "Explorar la FAQ" + }, + "transferhistoryrow_jsx": { + "to_savings": "a ahorros", + "from_savings": "desde ahorros", + "cancel_transfer_from_savings": "Cancela transferencia desde ahorros", + "stop_power_down": "Para power down", + "start_power_down_of": "Comenzar power down", + "receive_interest_of": "Recibir interés de" + }, + "savingswithdrawhistory_jsx": { + "cancel_this_withdraw_request": "¿Cancelar la petición de retirada?", + "pending_savings_withdrawals": "AHORROS PENDIENTES DE RETIRADA", + "withdraw": "Retirar %(amount)s", + "to": "a %(to)s", + "from_to": "de %(from)s a %(to)s" + }, + "explorepost_jsx": { + "copied": "¡Copiado!", + "copy": "COPIAR", + "alternative_sources": "Fuentes alternativas" + }, + "header_jsx": { + "home": "inicio", + "create_a_post": "Crear una publicación", + "change_account_password": "Cambiar Contraseña de la Cuenta", + "create_account": "Crear cuenta", + "stolen_account_recovery": "Recuperación de cuentas robadas", + "people_following": "Gente que sigues", + "people_followed_by": "Gente seguida por", + "curation_rewards_by": "Recompensas de curación de", + "author_rewards_by": "Recompensas de autor de", + "replies_to": "Respuestas a", + "comments_by": "Comentarios de" + }, + "loginform_jsx": { + "you_need_a_private_password_or_key": "Necesitas la contraseña privada (no la contraseña pública)", + "cryptography_test_failed": "El test cryptográfico no tuvo éxito", + "unable_to_log_you_in": "No puedes acceder con este navegador", + "the_latest_versions_of": "Las últimas versiones de", + "are_well_tested_and_known_to_work_with": "han sido testeados con éxito y están preparados para trabajar con %(APP_URL)s.", + "due_to_server_maintenance": "Debido a tareas de mantenimiento sólo estamos disponibles en modo lectura. Pedimos disculpas por los inconvenientes", + "login_to_vote": "Acceso para Votar", + "login_to_post": "Acceso para postear", + "login_to_comment": "Acceso para comentar", + "posting": "Postear", + "active_or_owner": "Activa o propietaria", + "this_password_is_bound_to_your_account_owner_key": "Esta contraseña está ligada a tu contraseña owner o propia y no puede ser usada para acceder a esta página.", + "however_you_can_use_it_to": "Sin embargo, puedes usarla para", + "update_your_password": "cambiar tu contraseña", + "to_obtain_a_more_secure_set_of_keys": "obtener un grupo más seguro de contraseñas.", + "this_password_is_bound_to_your_account_active_key": "Esta contraseña está ligada tu contraseña activa y no puede ser usada para acceder a esta página.", + "you_may_use_this_active_key_on_other_more": "Puede usar la contraseña activa en otras páginas más seguras como el Monedero.", + "you_account_has_been_successfully_created": "Tu cuenta se ha creado con éxito!", + "you_account_has_been_successfully_recovered": "Tu cuenta ha sido recuperada con éxito!", + "password_update_succes": "La contraseña %(accountName)s ha sido cambiada correctamente", + "password_info": "La contraseña o la contraseña privada fue introducida incorrectamente. Probablemente. Ayuda: Las contraseñas generadas por Steemit nunca contendrán 0 (cero), O (o mayúscula), I (i mayúscula), l (L minúscula).", + "enter_your_username": "Introduce tu nombre de usuario", + "password_or_wif": "Contraseña o WIF", + "this_operation_requires_your_key_or_master_password": "Esta operación requiere %(authType)s contraseña o la contraseña maestra", + "keep_me_logged_in": "Manténme logeado", + "amazing_community": "comunidad increíble", + "to_comment_and_reward_others": "para comentar y recompensar a otros", + "sign_up_get_steem": "Sign up. Get STEEM", + "signup_button": "Sign up now to earn ", + "signup_button_emphasis": "FREE STEEM!", + "returning_users": "Usuarios que vuelven", + "join_our": "Únete a nuestro" + }, + "chainvalidation_js": { + "account_name_should": "El nombre de la cuenta debería", + "not_be_empty": "No estar vacío", + "be_longer": "ser más largo", + "be_shorter": "ser más corto.", + "each_account_segment_should": "Cada segmento de la cuenta debería", + "start_with_a_letter": "comenzar con una letra.", + "have_only_letters_digits_or_dashes": "tener sólo letras, dígitos o guiones.", + "have_only_one_dash_in_a_row": "tener sólo un guión consecutivo.", + "end_with_a_letter_or_digit": "terminar con una legra o dígito.", + "verified_exchange_no_memo": "Tienes que incluir un memo para la transferencia al exchange" + }, + "settings_jsx": { + "invalid_url": "URL inválida", + "name_is_too_long": "El nombre es demasiado largo.", + "name_must_not_begin_with": "El nombre no debe empezar con @", + "about_is_too_long": "Sobre es demasiado largo", + "location_is_too_long": "Localización es demasiado largo", + "website_url_is_too_long": "La URL de la página web es muy larga", + "public_profile_settings": "Herramientas para el perfil público", + "private_post_display_settings": "Herramientas privadas para la visualización del post", + "not_safe_for_work_nsfw_content": "(NSFW) contenido para adultos", + "always_hide": "Ocultar siempre", + "always_warn": "Advertir siempre", + "always_show": "Mostrar siempre", + "muted_users": "Usuarios silenciados", + "update": "Actualizar", + "profile_image_url": "Link para la foto de perfil", + "cover_image_url": "Link para la imagen de fondo", + "profile_name": "Nombre de visualización", + "profile_about": "Sobre", + "profile_location": "Localización", + "profile_website": "Página Web" + }, + "transfer_jsx": { + "amount_is_in_form": "La cantidad está en el formato 99999.999", + "insufficient_funds": "Saldo insuficiente", + "use_only_3_digits_of_precison": "Usa solo 3 dígitos", + "send_to_account": "Mandar a cuenta", + "asset": "Activo", + "this_memo_is_private": "Esta memo es privada", + "this_memo_is_public": "Esta memo es pública", + "convert_to_VESTING_TOKEN": "Convertir a %(VESTING_TOKEN)s", + "balance_subject_to_3_day_withdraw_waiting_period": "El saldo está sujeto a 3 días de espera para la retirada", + "move_funds_to_another_account": "Mover fondos a otra %(APP_NAME)scuenta.", + "protect_funds_by_requiring_a_3_day_withdraw_waiting_period": "Protege los fondos al pedir la retirada con un periodo de 3 días de espera", + "withdraw_funds_after_the_required_3_day_waiting_period": "Retirada de fondos después del periodo de 3 días de espera", + "from": "De", + "to": "Para", + "asset_currently_collecting": "%(asset)s recolectando actualmente %(interest)s%% APR.", + "beware_of_spam_and_phishing_links": "Beware of spam and phishing links in transfer memos. Do not open links from users you do not trust. Do not provide your private keys to any third party websites." + }, + "userwallet_jsx": { + "conversion_complete_tip": "Se completará el", + "in_conversion": "%(amount)s en conversión", + "transfer_to_savings": "Transferir a ahorros", + "power_up": "Power up", + "power_down": "Power down", + "market": "Mercado", + "convert_to_LIQUID_TOKEN": "Convertir a %(LIQUID_TOKEN)s", + "withdraw_LIQUID_TOKEN": "Retirar %(LIQUID_TOKEN)s", + "withdraw_DEBT_TOKENS": "Retirar %(DEBT_TOKENS)s", + "tokens_worth_about_1_of_LIQUID_TICKER": "Los tokens valen alrededor de $1.00 de %(LIQUID_TICKER)s, ahora mismo recolectando %(sbdInterest)s%% APR.", + "savings": "AHORROS", + "estimated_account_value": "Valor de cuenta aproximado", + "next_power_down_is_scheduled_to_happen": "El siguiente power down ocurrirá en ", + "transfers_are_temporary_disabled": "Las transferencias están temporalmente inhabilitadas.", + "history": "HISTORIA", + "redeem_rewards": "Liquidar recompensas (Transferir al saldo)", + "buy_steem_or_steem_power": "Compra STEEM o STEEM POWER." + }, + "powerdown_jsx": { + "power_down": "Power Down", + "amount": "Cantidad", + "already_power_down": "Estás haciendo un power down de %(AMOUNT)s%(LIQUID_TICKER)s (%(WITHDRAWN)s%(LIQUID_TICKER)shan sido pagados hasta ahora). Ten en cuenta que si cambias la cantidad de \"power down\" la fecha de pago se reinicializará.", + "delegating": "Estás delegando %(AMOUNT)s%(LIQUID_TICKER)s. Esa cantidad está \"bloquedada\" y no podrás hacer \"power down\" con ella hasta que hayas cancelado la delegación y el periodo de pagos haya vencido.", + "per_week": "Eso es %(AMOUNT)s%(LIQUID_TICKER)spor semana", + "warning": "Dejar menos de %(AMOUNT)s%(VESTING_TOKEN)s en su cuenta no es recomendable y puede dejarla en un estado inutilizable.", + "error": "Imposible hacer \"power down\" (ERROR: %(MESSAGE)s)" + }, + "checkloginowner_jsx": { + "your_password_permissions_were_reduced": "Sus permisos de identificacion han sido limitados.", + "if_you_did_not_make_this_change": "Si no ha hecho el cambio por favor", + "ownership_changed_on": "Pertenencia cambio el", + "deadline_for_recovery_is": "La fecha límite para la recuperación es", + "i_understand_dont_show_again": "Lo entiendo, no mostrarmelo de nuevo." + } +} diff --git a/src/app/locales/fr.json b/src/app/locales/fr.json new file mode 100644 index 0000000..badd438 --- /dev/null +++ b/src/app/locales/fr.json @@ -0,0 +1,648 @@ +{ + "g": { + "age": "ancienneté", + "amount": "Montant", + "and": "et", + "are_you_sure": "Etes-vous sûr(e) ?", + "ask": "Demande", + "balance": "Solde", + "balances": "Soldes", + "bid": "Offre", + "blog": "Blog", + "browse": "Naviguer", + "buy": "Acheter", + "buy_or_sell": "Acheter ou vendre", + "by": "par", + "cancel": "Annuler", + "change_password": "Changer le mot de passe", + "choose_language": "Choisir la langue", + "clear": "Effacer", + "close": "Fermer", + "collapse_or_expand": "Compacter / Etendre", + "comments": "Commentaires", + "confirm": "Confirmer", + "convert": "Convertir", + "date": "Date", + "delete": "Supprimer", + "dismiss": "Ignorer", + "edit": "Editer", + "email": "Email", + "feed": "Flux", + "follow": "S'abonner", + "for": "pour", + "from": "de", + "go_back": "Retour", + "hide": "Masquer", + "in": "dans", + "in_reply_to": "en réponse à", + "insufficient_balance": "Solde insuffisant", + "invalid_amount": "Montant invalide", + "joined": "Inscrit en", + "loading": "Chargement en cours", + "login": "Se connecter", + "logout": "Déconnexion", + "memo": "Note", + "mute": "Ignorer", + "new": "nouveau", + "newer": "Plus récent", + "next": "Suivant", + "no": "Non", + "ok": "Ok", + "older": "Plus ancien", + "or": "ou", + "order_placed": "Ordre effectué", + "password": "Mot de passe", + "payouts": "Gains", + "permissions": "Permissions", + "phishy_message": "Link expanded to plain text; beware of a potential phishing attempt", + "post": "Poster", + "post_as": "Poster en tant que", + "posts": "Articles", + "powered_up_100": "Converti en influence à 100%%", + "preview": "Prévisualiser", + "previous": "Précédent", + "price": "Prix", + "print": "Imprimer", + "promote": "Promouvoir", + "promoted": "promu", + "re": "RE", + "re_to": "RE :%(topic)s", + "recent_password": "Mot de passe antérieur", + "receive": "Recevoir", + "remove": "Enlever", + "remove_vote": "Enlever son vote", + "replied_to": "répondu à %(account)s", + "replies": "Réponses", + "reply": "Répondre", + "reply_count": { + "zero": "pas de réponse", + "one": "1 réponse", + "other": "%(count)s réponses" + }, + "reputation": "Réputation", + "reveal_comment": "Révéler le commentaire", + "request": "requête", + "required": "Requis", + "rewards": "Gains", + "save": "Sauvegarder", + "saved": "Sauvegardé", + "search": "Rechercher", + "sell": "Vendre", + "settings": "Configuration", + "share_this_post": "Partager cet article", + "show": "Montrer", + "sign_in": "S'inscrire", + "sign_up": "S'inscrire", + "since": "depuis", + "submit": "Soumettre", + "power_up": "Augmenter son influence", + "submit_a_story": "Poster", + "tag": "Mot clé", + "to": "vers", + "all_tags": "All tags", + "transfer": "Transférer", + "trending_topics": "Sujets en vogue", + "type": "Genre", + "unfollow": "Se désabonner", + "unmute": "Ne plus ignorer", + "unknown": "Inconnu", + "upvote": "Voter pour", + "upvote_post": "Voter pour l'article", + "username": "Identifiant", + "version": "Version", + "vote": "Voter", + "votes": "votes", + "wallet": "Portefeuille", + "warning": "alarme", + "yes": "Oui", + "posting": "de publication", + "owner": "propriétaire", + "active": "active", + "account_not_found": "Compte inconnu", + "this_is_wrong_password": "Mot de passe incorrect", + "do_you_need_to": "Avez-vous besoin de", + "account_name": "Nom du compte", + "recover_your_account": "Récupérer son compte", + "reset_usernames_password": "Réinitialiser le mot de passe de %(username)s", + "this_will_update_usernames_authtype_key": "Ceci mettra a jour la clé %(authType)s de %(username)s", + "passwords_do_not_match": "Mots de passe différents", + "you_need_private_password_or_key_not_a_public_key": "La clé ou le mot de passe privé (et non public) est nécessaire ", + "the_rules_of_APP_NAME": { + "one": "La première règle de %(APP_NAME)s est de ne pas perdre son mot de passe.", + "second": "La deuxième règle de %(APP_NAME)s est de ne pas perdre son mot de passe.", + "third": "La troisième règle de %(APP_NAME)s est que nous ne pouvons réinitialiser votre mot de passe.", + "fourth": "La quatrième règle est que si vous pouvez vous rappeler de votre mot de passe, il n'est pas assez sécuritaire.", + "fifth": "La cinquième règle demande d'utiliser uniquement des mots de passe générés aléatoirement.", + "sixth": "La sixième règle est de ne donner à personne son mot de passe.", + "seventh": "La septième règle est de faire une sauvegarde de son mot de passe." + }, + "recover_password": "Récupérer un compte", + "current_password": "Mot de passe actuel", + "generated_password": "Mot de passe généré", + "backup_password_by_storing_it": "A sauvegarder en le copiant dans un fichier texte ou via un logiciel de gestion de mot de passe", + "enter_account_show_password": "Entrer un identifiant valide pour afficher le mot de passe", + "click_to_generate_password": "Cliquer ici pour générer un mot de passe", + "re_enter_generate_password": "Recopier le mot de passe généré", + "understand_that_APP_NAME_cannot_recover_password": "Je comprends que %(APP_NAME)s ne peut récupérer les mots de passe perdus", + "i_saved_password": "J'ai sauvegardé mon mot de passe de façon sécuritaire", + "update_password": "Mettre à jour le mot de passe", + "confirm_password": "Confirmer le mot de passe", + "account_updated": "Compte mis à jour", + "password_must_be_characters_or_more": "Le mot de passe doit comporter au moins %(amount)s caractères supplémentaires", + "need_password_or_key": "Il vous faut une clé ou un mot de passe privé (et non public)", + "login_to_see_memo": "S'identifier pour voir le mémo", + "new_password": "Nouveau mot de passe", + "incorrect_password": "Mot de passe incorrect", + "username_does_not_exist": "Cet identifiant n'existe pas", + "account_name_should_start_with_a_letter": "L'identifiant doit commencer par une lettre.", + "account_name_should_be_shorter": "L'identifiant doit être plus court.", + "account_name_should_be_longer": "L'identifiant doit être plus long.", + "account_name_should_have_only_letters_digits_or_dashes": "L'identifiant ne peut comporter que des lettres, des chiffres ou des traits d'union.", + "cannot_increase_reward_of_post_within_the_last_minute_before_payout": "Les gains d'un article ne peuvent être accrus durant la dernière minute avant rétribution.", + "vote_currently_exists_user_must_be_indicate_a_to_reject_witness": "un vote existe actuellement; l'utilisateur doit indiquer une raison pour rejeter le témoin", + "only_one_APP_NAME_account_allowed_per_ip_address_every_10_minutes": "Un seul compte Steem est permis par adresse IP toutes les 10 minutes", + "resteem_this_post": "Resteemer cet article", + "reblog": "Resteem", + "write_your_story": "Ecris ton histoire", + "remember_voting_and_posting_key": "Se souvenir de la clé de publication et de vote", + "auto_login_question_mark": "Identification automatique", + "hide_private_key": "Masquer la clé privée", + "show_private_key": "Révéler la clé privée", + "login_to_show": "S'identifier pour révéler", + "not_valid_email": "Adresse email invalide", + "thank_you_for_being_an_early_visitor_to_APP_NAME": "Merci pour être l'un des premiers visiteurs de %(APP_NAME)s. Nous reviendrons vers vous aussi tôt que possible.", + "author_rewards": "Gains éditoriaux", + "curation_rewards": "Gains de curation", + "sorry_your_reddit_account_doesnt_have_enough_karma": "Désolé, votre compte Reddit ne possède pas de Karma Reddit suffisant pour vous qualifier pour une inscription gratuite. Veuillez s'il-vous-plaît indiquer votre adresse email pour être placé(e) sur liste d'attente.", + "register_with_facebook": "S'enregistrer par Facebook", + "or_click_the_button_below_to_register_with_facebook": "Ou cliquer sur le bouton ci-dessous pour vous enregistrer par Facebook", + "server_returned_error": "le serveur a renvoyé une erreur", + "APP_NAME_support": "Support pour %(APP_NAME)s", + "please_email_questions_to": "Veuillez s'il-vous-plaît envoyer vos questions par email à", + "next_7_strings_single_block": { + "authors_get_paid_when_people_like_you_upvote_their_post": "Les auteurs sont payés lorsque des personnes comme vous votent pour leur contenu", + "if_you_enjoyed_what_you_read_earn_amount": "Si vous avez apprécié ce que vous avez lu ici, créez votre compte dès à présent et commencer par gagner immédiatement du STEEM", + "free_steem": "STEEM GRATUIT!", + "sign_up_earn_steem": "S'enregistrer pour gagner" + }, + "next_3_strings_together": { + "show_more": "Révéler plus", + "show_less": "Révéler moins", + "value_posts": "Articles de faible valeur" + }, + "read_only_mode": "En raison d'une maintenance des serveurs, le site est en mode lecture seule. Nous sommes désolés pour le désagrément. ", + "tags_and_topics": "Mots clés et sujets", + "show_more_topics": "Révéler plus de sujets", + "basic": "Basique", + "advanced": "Avancé", + "views": { + "zero": "Pas de vue", + "one": "%(count)s vue", + "other": "%(count)s vues" + }, + "responses": { + "zero": "Pas de réponse", + "one": "%(count)s réponse", + "other": "%(count)s réponses" + }, + "post_key_warning": { + "confirm": "You are about to publish a STEEM private key or master password. You will probably lose control of the associated account and all its funds.", + "warning": "Legitimate users, including employees of Steemit Inc., will never ask you for a private key or master password.", + "checkbox": "I understand" + } + }, + "navigation": { + "about": "A propos", + "explore": "Explorer", + "APP_NAME_whitepaper": "%(APP_NAME)s livre blanc", + "buy_LIQUID_TOKEN": "Acheter %(LIQUID_TOKEN)s", + "sell_LIQUID_TOKEN": "Vendre %(LIQUID_TOKEN)s", + "currency_market": "Marché des devises", + "stolen_account_recovery": "Récupération de comptes dérobés", + "change_account_password": "Changement de mot de passe", + "witnesses": "Témoins", + "vote_for_witnesses": "Voter pour les témoins", + "privacy_policy": "Politique de confidentialité", + "terms_of_service": "Conditions d'utilisation", + "sign_up": "Rejoindre", + "learn_more": "En savoir plus", + "welcome": "Bienvenue", + "faq": "FAQ", + "shop": "The Steemit Shop", + "chat": "Salons de discussion Steemit", + "app_center": "Centre d'applications Steemit", + "api_docs": "Documentation API sur Steemit", + "whitepaper": "Livre blanc sur le Steem", + "bluepaper": "Steem Bluepaper", + "intro_tagline": "Présentations sur la monnaie", + "intro_paragraph": "Votre voix vaut quelque chose. Rejoignez la communauté qui vous rémunère pour vos articles et participez à la curation du contenu de qualité." + }, + "main_menu": { + "hot": "chaud", + "trending": "en vogue" + }, + "reply_editor": { + "shorten_title": "Raccourcir le titre", + "exceeds_maximum_length": "Dépassement de la taille maximale ( %(maxKb)s KB)", + "including_the_category": "(incluant la catégorie '%(rootCategory)s')", + "use_limited_amount_of_tags": "Vous avez un total de %(tagsLength)s mots clés %(includingCategory)s. Veuillez en utiliser seulement 5, autant pour l'article que pour le champ dédié aux mots clés.", + "are_you_sure_you_want_to_clear_this_form": "Etes-vous sûr de vouloir effacer ce formulaire ?", + "uploading": "Téléchargement en cours", + "draft_saved": "Brouillon sauvegardé", + "editor": "Editeur", + "insert_images_by_dragging_dropping": "Insertion d'images par 'drag and drop'", + "pasting_from_the_clipboard": "copie depuis le presse-papier,", + "selecting_them": "les sélectionnant", + "image_upload": "Téléchargement d'image", + "power_up_100": "Conversion en influence à 100%%", + "default_50_50": "Par défaut (50 %%/ 50%%)", + "decline_payout": "Refuser les gains", + "check_this_to_auto_upvote_your_post": "Cocher cette case pour auto-voter pour votre article", + "markdown_styling_guide": "Guide stylistique pour le format Markdown", + "or_by": "ou par", + "title": "Titre", + "update_post": "Mettre l'article à jour", + "markdown_not_supported": "Le format Markdown n'est pas supporté ici" + }, + "category_selector_jsx": { + "tag_your_story": "Mots clés (jusqu'à 5 mots clés), le premier d'entre eux étant votre catégorie principale.", + "select_a_tag": "Sélectionner un mot clé", + "maximum_tag_length_is_24_characters": "La longueur d'un mot clé est d'au maximum 24 caractères", + "use_limited_amount_of_categories": "Veuillez s'il-vous-plaît n'utiliser que %(amount)s catégories", + "use_only_lowercase_letters": "Veuillez n'utiliser que des lettres minuscules", + "use_one_dash": "Veuillez n'utiliser qu'un seul trait d'union", + "use_spaces_to_separate_tags": "Veuillez utiliser des espaces pour séparer les mots clés", + "use_only_allowed_characters": "Veuillez n'utilisez que des lettres minuscules, des chiffres et au plus un trait d'union", + "must_start_with_a_letter": "Le premier caractère doit être une lettre", + "must_end_with_a_letter_or_number": "Le dernier caractère doit être une lettre ou un chiffre" + }, + "postfull_jsx": { + "this_post_is_not_available_due_to_a_copyright_claim": "Cet article n'est pas disponible en raison d'une requête de droits d'auteur.", + "share_on_facebook": "Partager sur Facebook", + "share_on_twitter": "Partager sur Twitter", + "share_on_linkedin": "Partager sur Linkedin", + "recent_password": "Mot de passe antérieur", + "in_week_convert_DEBT_TOKEN_to_LIQUID_TOKEN": "Dans 3.5 jours, conversion de %(amount)s %(DEBT_TOKEN)s en %(LIQUID_TOKEN)s", + "view_the_full_context": "Voir le contexte complet", + "view_the_direct_parent": "Voir le message précédent", + "you_are_viewing_a_single_comments_thread_from": "Vous visualisez un commentaire unique de l'article" + }, + "market_jsx": { + "action": "Action", + "date_created": "Date de création", + "last_price": "Dernier cours", + "24h_volume": "Volume des dernières 24h", + "spread": "Ecart", + "total": "Total", + "available": "Disponible", + "lowest_ask": "Demande au taux le plus bas", + "highest_bid": "Offre au taux le plus haut", + "buy_orders": "Ordres d'achat", + "sell_orders": "Ordres de vente", + "trade_history": "Historique des échanges", + "open_orders": "Ordres ouverts", + "sell_amount_for_atleast": "Vendre %(amount_to_sell)s pour au moins %(min_to_receive)s ( %(effectivePrice)s )", + "buy_atleast_amount_for": "Acheter au moins %(min_to_receive)s pour %(amount_to_sell)s ( %(effectivePrice)s )", + "price_warning_above": "Ce cours est bien au-delà des cours du marché actuels %(marketPrice)s, êtes-vous sûr (e) ?", + "price_warning_below": "Ce cours est bien en-deça des cours du marché actuels %(marketPrice)s, êtes-vous sûr (e) ?", + "order_cancel_confirm": "Annuler l'ordre %(order_id)s de %(user)s", + "order_cancelled": "Ordre %(order_id)s annulé.", + "higher": "Plus haut", + "lower": "Plus bas", + "total_DEBT_TOKEN_SHORT_CURRENCY_SIGN": "Total %(DEBT_TOKEN_SHORT)s (%(CURRENCY_SIGN)s)" + }, + "recoveraccountstep1_jsx": { + "begin_recovery": "Démarrer la récupération", + "not_valid": "Invalide", + "account_name_is_not_found": "Compte inexistant", + "unable_to_recover_account_not_change_ownership_recently": "Nous sommes incapables de récupérer ce compte, car il n'a pas changé de propriétaire récemment.", + "password_not_used_in_last_days": "Ce mot de passe n'a pas été utilisé durant les derniers 30 jours.", + "request_already_submitted_contact_support": "Votre requête a déjà été soumise et nous nous en occupons. Veuillez s'il-vous-plaît contacter %(SUPPORT_EMAIL)s pour obtenir le statut de votre requête.", + "recover_account_intro": "Il est possible que la clé propriétaire d'un Steemien soit compromise. Le processus de récupération de comptes dérobés est autorisé pour l'utilisateur de droit pendant 30 jours à partir du moment où le voleur a modifié la clé propriétaire. La récupération de comptes dérobés ne peut être utilisée sur %(APP_URL)s que si le propriétaire du compte a ajouté '%(APP_NAME)s'comme mandataire de confiance et accepté les conditions d'utilisation de %(APP_NAME)s.", + "login_with_facebook_or_reddit_media_to_verify_identity": "Veuillez vous identifier via Facebook ou Reddit pour vérifier votre identité.", + "login_with_social_media_to_verify_identity": "Veuillez vous identifier via %(provider)safin de vérifier votre identité", + "enter_email_toverify_identity": "Nous devons vérifier votre identité. Veuillez s'il-vous-plaît entrer votre adresse email ci-dessous pour démarrer le processus.", + "continue_with_email": "Continuer avec l'email.", + "thanks_for_submitting_request_for_account_recovery": "Merci pour la soumission de votre requête concernant la récupération de votre compte %(APP_NAME)sen utilisant une authentification à multi-facteurs basée sur la blockchain. Nous vous répondrons aussi rapidement que possible. Cependant, il se pourrait que notre réponse prennent du temps en raison du gros volume d'emails à traiter. Veuillez vous préparer à vérifier votre identité.", + "recovering_account": "Récupération du compte", + "recover_account": "Récupérer le compte", + "checking_account_owner": "Vérifier le propriétaire du compte", + "sending_recovery_request": "Envoyer une requête de récupération", + "cant_confirm_account_ownership": "Nous ne pouvons confirmer que vous êtes propriétaire de ce compte. Veuillez s'il-vous-plaît vérifier votre mot de passe.", + "account_recovery_request_not_confirmed": "La requête de la récupération du compte n'est pas encore confirmée. Veuillez réessayer plus tard. Merci de votre patience." + }, + "user_profile": { + "unknown_account": "Compte inconnu", + "user_hasnt_made_any_posts_yet": "Il semblerait que %(name)s n'ait pas encore publié d'article.", + "user_hasnt_started_bloggin_yet": "Il semblerait que %(name)s n'ait pas encore commencer à blogger.", + "user_hasnt_followed_anything_yet": "Il semblerait que %(name)s ne soit fan de personne. Dans le cas où %(name)s s'abonnerait au fil d'autres utilisateurs, son flux sera automatiquement rempli avec du nouveau contenu.", + "user_hasnt_had_any_replies_yet": "%(name)s n'a pas encore reçu de réponse", + "looks_like_you_havent_posted_anything_yet": "Looks like you haven't posted anything yet.", + "create_a_post": "Create a Post", + "explore_trending_articles": "Explore Trending Articles", + "read_the_quick_start_guide": "Read The Quick Start Guide", + "browse_the_faq": "Browse The FAQ", + "followers": "Followers", + "this_is_users_reputations_score_it_is_based_on_history_of_votes": "Ceci est le score de réputation de %(name)s. \n\nLe score de réputation est basé sur l'historique des votes reçus par l'utilisateur, et est utilisé pour masquer le contenu de basse qualité.", + "follower_count": { + "zero": "Pas de fans", + "one": "1 fan", + "other": "%(count)s fans" + }, + "followed_count": { + "zero": "N'est fan d'aucun utilisateur", + "one": "fan d'1 personne", + "other": "%(count)s fans" + }, + "post_count": { + "zero": "Pas d'article", + "one": "1 article", + "other": "%(count)s articles" + } + }, + "authorrewards_jsx": { + "estimated_author_rewards_last_week": "Gains de publication estimés pour la semaine dernière", + "author_rewards_history": "Historique des gains de publication" + }, + "curationrewards_jsx": { + "estimated_curation_rewards_last_week": "Gains de curation estimés pour la semaine dernière", + "curation_rewards_history": "Historique des gains de curation" + }, + "post_jsx": { + "now_showing_comments_with_low_ratings": "Affichant à présent les commentaires de faible évaluation", + "sort_order": "Classer les ordres", + "comments_were_hidden_due_to_low_ratings": "Certains commentaires sont masqués en raison de leur faible évaluation" + }, + "voting_jsx": { + "flagging_post_can_remove_rewards_the_flag_should_be_used_for_the_following": "Signaler un article peut diminuer ses gains potentiels et rendre son contenu moins visible. Quelques raisons usuelles pour signaler un article :", + "disagreement_on_rewards": "Désaccord sur les gains", + "fraud_or_plagiarism": "Fraude ou plagiarisme", + "hate_speech_or_internet_trolling": "Discours haineux ou troll", + "intentional_miss_categorized_content_or_spam": "Contenu catégorisé incorrectement de façon intentionnelle ou spam", + "pending_payout": "%(value)s$ de gains en suspens", + "payout_declined": "Gains refusés", + "max_accepted_payout": "%(value)s$ de gains maximum acceptés", + "promotion_cost": "Cout de la promotion: %(value)s$", + "past_payouts": "Gains antérieurs: %(value)s$", + "past_payouts_author": "- Auteur: %(value)s$", + "past_payouts_curators": "Curateurs: %(value)s$", + "we_will_reset_curation_rewards_for_this_post": "cela annulera vos potentiels gains de curation pour cet article", + "removing_your_vote": "Enlever votre vote", + "changing_to_an_upvote": "Modifier pour un vote positif", + "changing_to_a_downvote": "Modifier pour un vote négatif", + "confirm_flag": "Confirmer la signalisation", + "and_more": "et %(count)s en plus", + "votes_plural": { + "one": "%(count)s vote", + "other": "%(count)s votes" + } + }, + "witnesses_jsx": { + "witness_thread": "article introductif pour le témoin", + "top_witnesses": "Votes pour les témoins", + "you_have_votes_remaining": { + "zero": "Il ne vous reste plus de vote disponible", + "one": "Il vous reste 1 vote disponible", + "other": "Il vous reste %(count)s votes disponibles" + }, + "you_can_vote_for_maximum_of_witnesses": "Vous pouvez voter pour un maximum de 30 témoins", + "witness": "Témoin", + "information": "Information", + "if_you_want_to_vote_outside_of_top_enter_account_name": "Si vous voulez voter pour un témoin en dehors du top 50, veuille entrer le nom du compte concerné ci-dessous afin de voter pour lui", + "set_witness_proxy": "Vous pouvez aussi donner une procuration à quelqu'un qui votera pour les témoins en votre nom. Cela annulera votre choix de témoins actuels.", + "witness_set": "Vous avez donné une procuration pour le vote des témoins. Si vous voulez réactiver le vote manuel, veuillez annuler votre procuration.", + "witness_proxy_current": "Vous avez donné une procuration à", + "witness_proxy_set": "Donner une procuration", + "witness_proxy_clear": "Annuler la procuration", + "proxy_update_error": "Votre procuration n'a pas été mise à jour" + }, + "votesandcomments_jsx": { + "no_responses_yet_click_to_respond": "Pas encore de réponse. Cliquez ici pour répondre.", + "response_count_tooltip": { + "zero": "Pas de réponse. Cliquez ici pour répondre.", + "one": "1 réponse. Cliquez ici pour répondre.", + "other": "%(count)s réponses. Cliquez ici pour répondre. " + }, + "vote_count": { + "zero": "Pas de vote", + "one": "1 vote", + "other": "%(count)svotes" + } + }, + "userkeys_jsx": { + "public": "Public", + "private": "Privé", + "public_something_key": "Clé publique %(key)s", + "private_something_key": "Clé privée %(key)s", + "posting_key_is_required_it_should_be_different": "Le clé de publication est utilisée pour publier et voter. Elle devrait être différente des clés active et propriétaire.", + "the_active_key_is_used_to_make_transfers_and_place_orders": "La clé active est utilisée pour effectuer des transferts et des placer des ordres sur le marché interne.", + "the_owner_key_is_required_to_change_other_keys": "La clé propriétaire est la clé principale du compte et est requise pour changer les autres clés.", + "the_private_key_or_password_should_be_kept_offline": "La clé privée, ou le mot de passe pour la clé propriétaire, devrait être gardée hors ligne autant que possible.", + "the_memo_key_is_used_to_create_and_read_memos": "La clé pour les notes est utilisée pour créer et lire des notes." + }, + "suggestpassword_jsx": { + "APP_NAME_cannot_recover_passwords_keep_this_page_in_a_secure_location": "%(APP_NAME)s ne peut pas récupérer de mots de passe. Veuille garder une trace de cette page en lieu sûr, comme un coffre fort ininflammable et sécuritaire.", + "APP_NAME_password_backup": "Sauvegarde de mot de passe pour %(APP_NAME)s", + "APP_NAME_password_backup_required": "Sauvegarde de mot de passe pour %(APP_NAME)s (requis)", + "after_printing_write_down_your_user_name": "Après impression, veuillez indiquer votre identifiant" + }, + "converttosteem_jsx": { + "your_existing_DEBT_TOKEN_are_liquid_and_transferable": "Vos %(DEBT_TOKEN)s existants sont en liquide et donc transférable. Vous pouvez par exemple ėchanger %(DEBT_TOKEN)s directement sur ce site via le lien %(link)s, ou les transférer vers un marché externe.", + "this_is_a_price_feed_conversion": "Il s'agit d'une conversion au taux du marché. Le délais de 3.5 jours est nécessaire afin d'éviter les abus de ceux qui tenterait de parier sur les modifications à court terme du taux du marché moyen.", + "convert_to_LIQUID_TOKEN": "Convertir en %(LIQUID_TOKEN)s", + "DEBT_TOKEN_will_be_unavailable": "Cette action aura lieu dans 3.5 jours et ne peut être annulée. Ces %(DEBT_TOKEN)s deviendront immédiatement non disponibles." + }, + "tips_js": { + "liquid_token": "Des jetons négociables peuvent être transférés n'importe où à tout moment.
      %(LIQUID_TOKEN)s peuvent être convertis en %(VESTING_TOKEN)s lors d'un processus appelé conversion en influence, ou power up.", + "influence_token": "Jetons d'influence qui vous donnent plus de contrôle sur les gains potentiels des articles et vous permettent d'augmenter vos gains de curation", + "estimated_value": "La valeur estimée est basée sur la valeur moyenne de %(LIQUID_TOKEN)s en dollars américains.", + "non_transferable": "%(VESTING_TOKEN)s est non transférable et demande 3 mois (13 paiements) pour être convertis en %(LIQUID_TOKEN)s. ", + "converted_VESTING_TOKEN_can_be_sent_to_yourself_but_can_not_transfer_again": "Les %(VESTING_TOKEN)s convertis peuvent être envoyés soit à vous, soit à quelqu'un d'autre, mais ne peuvent pas être transférés à nouveau sans être convertis en %(LIQUID_TOKEN)s.", + "part_of_your_steem_power_is_currently_delegated": "Une partie de votre influence, ou STEEM POWER, est actuellement déléguée. La délégation est un don pour augmenter l'influence ou pour encourager les nouveaux utilisateurs à effectuer des actions sur Steemit. Le montant délégué peut varier." + }, + "promote_post_jsx": { + "promote_post": "Promouvoir un article", + "spend_your_DEBT_TOKEN_to_advertise_this_post": "Dépenser vos %(DEBT_TOKEN)s pour afficher cet article dans la section contenant les articles promus. ", + "you_successfully_promoted_this_post": "Vous avez promotionné cet article avec succès", + "this_post_was_hidden_due_to_low_ratings": "Cet article est masqué en raison d'une faible évaluation." + }, + "about_jsx": { + "about_app": "A propos de %(APP_NAME)s", + "about_app_details": "%(APP_NAME)s est une plateforme de réseau social où les utilisateurs sont payés pour créer et effecter la curation du contenu. Elle influence un système de points digitaux, appelé Steem, qui ont une valeur réelle en fonction des prix du marché et des liquidités disponibles.", + "learn_more_at_app_url": "Plus d'informations à %(APP_URL)s", + "resources": "Ressources" + }, + "markdownviewer_jsx": { + "images_were_hidden_due_to_low_ratings": "Les images sont masqués en raison d'une faible évaluation." + }, + "postsummary_jsx": { + "resteemed": "Resteemé", + "resteemed_by": "Resteemé par", + "reveal_it": "Révélez-le", + "adjust_your": "Ajustez vos", + "display_preferences": "préférences visuelles", + "create_an_account": "créer un compte", + "to_save_your_preferences": "Pour enregistrer vos préférences" + }, + "posts_index": { + "empty_feed_1": "Il semblerait que vous ne vous êtes abonné(e) à aucun fil pour le moment", + "empty_feed_2": "Si vous vous êtes récemment abonné(e) au fil de nouveaux utilisateurs, votre flux personnel sera automatiquement rempli lorsque du nouveau contenu apparaîtra", + "empty_feed_3": "Explorer les articles en vogue", + "empty_feed_4": "Lire le guide de prise en main rapide", + "empty_feed_5": "Parcourir la FAQ" + }, + "transferhistoryrow_jsx": { + "to_savings": "vers l'épargne", + "from_savings": "depuis l'épargne", + "cancel_transfer_from_savings": "Annuler le transfert vers l'épargne", + "stop_power_down": "Arrêter la récupération de l'influence, ou le power down", + "start_power_down_of": "Démarrer la récupération de l'influence, ou le power down", + "receive_interest_of": "Recevoir les intérêts de" + }, + "savingswithdrawhistory_jsx": { + "cancel_this_withdraw_request": "Voulez-vous annuler cette demande de retrait ?", + "pending_savings_withdrawals": "RETRAITS D'EPARGNE EN SUSPENS", + "withdraw": "Retirer %(amount)s", + "to": "vers %(to)s", + "from_to": "de %(from)s vers %(to)s" + }, + "explorepost_jsx": { + "copied": "Copié!", + "copy": "COPIER", + "alternative_sources": "Références alternatives" + }, + "header_jsx": { + "home": "répertoire personnel", + "create_a_post": "Créer un article", + "change_account_password": "Changer le mot de passe du compte", + "create_account": "Créer un compte", + "stolen_account_recovery": "Récupération des comptes dérobés", + "people_following": "Personnes fans de", + "people_followed_by": "Personnes suivies par", + "curation_rewards_by": "Gains de curation de", + "author_rewards_by": "Gains de publication de", + "replies_to": "Réponses à", + "comments_by": "Commentaires de" + }, + "loginform_jsx": { + "you_need_a_private_password_or_key": "Il vous faut un mot de passe ou une clé privé (pas une clé publique)", + "cryptography_test_failed": "Le test de cryptographie a échoué", + "unable_to_log_you_in": "Nous sommes incapables de vous identifier avec ce navigateur.", + "the_latest_versions_of": "Les dernières versions de", + "are_well_tested_and_known_to_work_with": "sont bien testées et connues pour fonctionner avec %(APP_URL)s.", + "due_to_server_maintenance": "En raison de la maintenance des serveurs, nous opérons en mode 'lecture seule'. Nous nous excusons des désagréments. ", + "login_to_vote": "S'identifier pour voter", + "login_to_post": "S'identifier pour publier", + "login_to_comment": "S'identifier pour commenter", + "posting": "de publication", + "active_or_owner": "Active ou propriétaire", + "this_password_is_bound_to_your_account_owner_key": "Ce mot de passe est lié à votre clé propriétaire et ne peut pas être utilisé pour vous identifier sur ce site.", + "however_you_can_use_it_to": "Cependant, vous pouvez l'utiliser pour ", + "update_your_password": "Mettre à jour votre mot de passe", + "to_obtain_a_more_secure_set_of_keys": "Pour obtenir un jeu de clés plus sûr.", + "this_password_is_bound_to_your_account_active_key": "Ce mot de passe est lié à la clé active de votre compte et ne peut pas être utilisé pour vous connecter à cette page.", + "you_may_use_this_active_key_on_other_more": "Vous pouvez utiliser cette clé active sur d'autres pages plus sécurisées comme les pages 'Portefeuille' ou 'Marché'.", + "you_account_has_been_successfully_created": "Votre compte a été créé avec succès!", + "you_account_has_been_successfully_recovered": "Votre compte a été récupéré avec succès!", + "password_update_succes": "Le mot de passe pour%(accountName)sa été mis à jour avec succès.", + "password_info": "Ce mot de passe ou clé privée est incorrect. Il y a probablement une erreur d'écriture ou d'entrée des données. Indication : un mot de passe ou une clé privée générée par Steemit ne contiendra jamais aucun 0 (zéro), O (o majuscule), I (i majuscule) ou l (l minuscule).", + "enter_your_username": "Entrez votre nom d'utilisateur", + "password_or_wif": "Mot de passe ou WIF", + "this_operation_requires_your_key_or_master_password": "Cette opération nécessite votre clé %(authType)s ou votre mot de passe principal.", + "keep_me_logged_in": "Gardez-moi connecté", + "amazing_community": "communauté extraordinaire", + "to_comment_and_reward_others": " pour commenter et récompenser les autres.", + "sign_up_get_steem": "Sign up. Get STEEM", + "signup_button": "Sign up now to earn ", + "signup_button_emphasis": "FREE STEEM!", + "returning_users": "Utilisateurs de retour :", + "join_our": "Rejoignez notre" + }, + "chainvalidation_js": { + "account_name_should": "Le nom du compte doit ", + "not_be_empty": "ne peut pas être vide.", + "be_longer": "être plus long.", + "be_shorter": "être plus court.", + "each_account_segment_should": "Chaque segment de compte devrait", + "start_with_a_letter": "commencer par une lettre.", + "have_only_letters_digits_or_dashes": "ne comporter que des lettres, des chiffres ou des traits d'union.", + "have_only_one_dash_in_a_row": "n'avoir qu'un seul trait d'union.", + "end_with_a_letter_or_digit": "terminer par une lettre ou un chiffre.", + "verified_exchange_no_memo": "Vous devez inclure un mémo pour votre transfert vers un marché d'échanges." + }, + "settings_jsx": { + "invalid_url": "L'URL est invalide", + "name_is_too_long": "Le nom est trop long", + "name_must_not_begin_with": "Le nom ne doit pas commencer avec un @", + "about_is_too_long": "Le \"A propos\" est trop long", + "location_is_too_long": "Le lieu est trop long", + "website_url_is_too_long": "L'URL du site web est trop longue", + "public_profile_settings": "Paramètres du profil public", + "private_post_display_settings": "Paramètres de l'affichage des messages privés", + "not_safe_for_work_nsfw_content": "Peu approprié pour le travail (NSFW)", + "always_hide": "Toujours masquer", + "always_warn": "Toujours avertir", + "always_show": "Toujours montrer", + "muted_users": "Utilisateurs ignorés", + "update": "Mise à jour", + "profile_image_url": "URL de la photo de profil", + "cover_image_url": "URL de l'image de couverture", + "profile_name": "Nom à afficher", + "profile_about": "A propos", + "profile_location": "Lieu", + "profile_website": "Site internet" + }, + "transfer_jsx": { + "amount_is_in_form": "Montant dans le format 99999.999", + "insufficient_funds": "Fonds insuffisants", + "use_only_3_digits_of_precison": "Utiliser au maximum 3 décimales pour la précision", + "send_to_account": "Envoyer au compte", + "asset": "Actif", + "this_memo_is_private": "Ce mémo est privé", + "this_memo_is_public": "Ce mémo est public", + "convert_to_VESTING_TOKEN": "Convertir en %(VESTING_TOKEN)s", + "balance_subject_to_3_day_withdraw_waiting_period": "Le solde dépend de la période de 3 jours d'attente requise pour les retraits", + "move_funds_to_another_account": "Transférer des fonds vers un autre compte %(APP_NAME)s ", + "protect_funds_by_requiring_a_3_day_withdraw_waiting_period": "Protéger les fonds en imposant une période de latence de 3 jours pour les retraits.", + "withdraw_funds_after_the_required_3_day_waiting_period": "Retirer les fonds après la période de 3 jours de latence requise", + "from": "De", + "to": "A", + "asset_currently_collecting": "%(asset)s collectant actuellement %(interest)s %% d'APR.", + "beware_of_spam_and_phishing_links": "Beware of spam and phishing links in transfer memos. Do not open links from users you do not trust. Do not provide your private keys to any third party websites." + }, + "userwallet_jsx": { + "conversion_complete_tip": "S'achèvera le", + "in_conversion": "%(amount)s en conversion", + "transfer_to_savings": "Transfert vers l'épargne", + "power_up": "Augmenter son influence", + "power_down": "Diminuer son influence", + "market": "Marché", + "convert_to_LIQUID_TOKEN": "Convertir en %(LIQUID_TOKEN)s", + "withdraw_LIQUID_TOKEN": "Retirer %(LIQUID_TOKEN)s", + "withdraw_DEBT_TOKENS": "Retirer %(DEBT_TOKENS)s", + "tokens_worth_about_1_of_LIQUID_TICKER": "Des jetons d'environ 1.00$ de %(LIQUID_TICKER)s, actuellement donnant lieu à %(sbdInterest)s %% d'APR.", + "savings": "EPARGNE", + "estimated_account_value": "Valeur estimée du compte", + "next_power_down_is_scheduled_to_happen": "Le prochain retrait d'influence, ou power down, est prévu dans ", + "transfers_are_temporary_disabled": "Les transferts sont temporairement désactivés.", + "history": "HISTORIQUE", + "redeem_rewards": "Réclamer les gains (transférer vers le solde)", + "buy_steem_or_steem_power": "Acheter du STEEM ou du STEEM POWER" + }, + "powerdown_jsx": { + "power_down": "Diminuer son influence", + "amount": "Montant", + "already_power_down": "Vous êtes actuellement en train de retirer %(AMOUNT)s %(LIQUID_TICKER)s d'influence (%(WITHDRAWN)s %(LIQUID_TICKER)s déjà payé). Veuillez noter que si le montant retiré est modifié, le compteur pour le prochain retrait sera remis à zéro également.", + "delegating": "Vous déléguez actuellement %(AMOUNT)s %(LIQUID_TICKER)s. Ce montant est bloqué et est indisponible pour tout retrait d'influence, ou power down, tant que la délégation n'est pas annulée et qu'une période complète de gains soit passée.", + "per_week": "C'est environ %(AMOUNT)s%(LIQUID_TICKER)s par semaine.", + "warning": "Laisser moins de%(AMOUNT)s%(VESTING_TOKEN)s sur votre compte n'est pas recommandé et peut rendre votre compte inutilisable.", + "error": "Impossible d'effectuer un retrait d'influence (ERREUR : %(MESSAGE)s)" + }, + "checkloginowner_jsx": { + "your_password_permissions_were_reduced": "Les permissions de votre mot de passe ont été réduites", + "if_you_did_not_make_this_change": "Si vous n'avez pas fait ce changement, veuillez s'il-vous-plaît", + "ownership_changed_on": "Changement de propriétaire à compter du ", + "deadline_for_recovery_is": "La date limite de récupération est", + "i_understand_dont_show_again": "J'ai compris, ne me le montrez plus jamais" + } +} diff --git a/src/app/locales/it.json b/src/app/locales/it.json new file mode 100644 index 0000000..e76a8b1 --- /dev/null +++ b/src/app/locales/it.json @@ -0,0 +1,648 @@ +{ + "g": { + "age": "età", + "amount": "Quantità", + "and": "e", + "are_you_sure": "Sei sicuro?", + "ask": "Ask", + "balance": "Saldo", + "balances": "Saldi", + "bid": "Bid", + "blog": "Blog", + "browse": "Naviga", + "buy": "Acquista", + "buy_or_sell": "Acquista o vendi", + "by": "da", + "cancel": "Cancella", + "change_password": "Cambia Password", + "choose_language": "Scegli la lingua", + "clear": "Pulisci", + "close": "Chiudi", + "collapse_or_expand": "Riduci/Espandi", + "comments": "Commenti", + "confirm": "Conferma", + "convert": "Converti", + "date": "Data", + "delete": "Elimina", + "dismiss": "Abbandona", + "edit": "Modifica", + "email": "Email", + "feed": "Feed", + "follow": "Segui", + "for": "per", + "from": "da", + "go_back": "Indietro", + "hide": "Nascondi", + "in": "in", + "in_reply_to": "in risposta a", + "insufficient_balance": "Credito insufficiente", + "invalid_amount": "Importo errato", + "joined": "Connesso", + "loading": "Caricamento", + "login": "Login", + "logout": "Logout", + "memo": "Memo", + "mute": "Silenzia", + "new": "Nuovo", + "newer": "Più recente", + "next": "Prossimo", + "no": "No", + "ok": "Ok", + "older": "Meno recente", + "or": "o", + "order_placed": "Ordine piazzato", + "password": "Password", + "payouts": "Pagamenti", + "permissions": "Permessi", + "phishy_message": "Link expanded to plain text; beware of a potential phishing attempt", + "post": "Post", + "post_as": "Pubblica come", + "posts": "Posts", + "powered_up_100": "Powered Up 100%%", + "preview": "Anteprima", + "previous": "Precedente", + "price": "Prezzo", + "print": "Stampa", + "promote": "Promuovi", + "promoted": "promosso", + "re": "RE", + "re_to": "RE: %(topic)s", + "recent_password": "Password Recente", + "receive": "Ricevi", + "remove": "Rimuovi", + "remove_vote": "Rimuovi il voto", + "replied_to": "risposto a %(account)s", + "replies": "Risponde", + "reply": "Risposta", + "reply_count": { + "zero": "Nessuna risposta", + "one": "1 risposta", + "other": "%(count)s risposte" + }, + "reputation": "Reputazione", + "reveal_comment": "Mostra Commento", + "request": "richiesta", + "required": "Richiesto", + "rewards": "Ricompense", + "save": "Salva", + "saved": "Salvato", + "search": "Cerca", + "sell": "Vendi", + "settings": "Impostazioni", + "share_this_post": "Condividi questo post", + "show": "Mostra", + "sign_in": "Accedi", + "sign_up": "Registrati", + "since": "da", + "submit": "Sottoscrivi", + "power_up": "Power Up", + "submit_a_story": "Post", + "tag": "Tag", + "to": "a", + "all_tags": "All tags", + "transfer": "Trasferisci", + "trending_topics": "Trending Topics", + "type": "Scrivi", + "unfollow": "Non seguire più", + "unmute": "Unmute", + "unknown": "Sconosciuto", + "upvote": "Upvote", + "upvote_post": "Upvote post", + "username": "Username", + "version": "Versione", + "vote": "Vota", + "votes": "voti", + "wallet": "Wallet", + "warning": "avviso", + "yes": "Sì", + "posting": "Postare", + "owner": "Proprietario", + "active": "Attivo", + "account_not_found": "Account non trovato", + "this_is_wrong_password": "Password errata", + "do_you_need_to": "Hai bisogno di", + "account_name": "Nome Account", + "recover_your_account": "recupera il tuo account", + "reset_usernames_password": "Resetta la password di %(username)s", + "this_will_update_usernames_authtype_key": "Questo aggiornerà la %(authType)s key di %(username)s ", + "passwords_do_not_match": "Le password non corrispondono", + "you_need_private_password_or_key_not_a_public_key": "Hai bisogno di una password privata o di una chiave (non di una chiave pubblica)", + "the_rules_of_APP_NAME": { + "one": "La prima regola di %(APP_NAME)sè: Non perdere la tua password.", + "second": "La seconda regola di %(APP_NAME)s è: Non perdere la tua password.", + "third": "La terza regola di %(APP_NAME)s è: Non possiamo recuperare la tua password.", + "fourth": "La quarta regola è: Se puoi ricordare la password, non è sicura.", + "fifth": "La quinta regola è: Usa solo password generate in modo casuale.", + "sixth": "La sesta regola è: Non dire a nessuno la tua password.", + "seventh": "La settima regola è: Salva sempre la tua password." + }, + "recover_password": "Recupera Account", + "current_password": "Password attuale", + "generated_password": "Password generata", + "backup_password_by_storing_it": "Effettua il backup copiando la password in un file di testo o in un gestore di password. ", + "enter_account_show_password": "Inserisci un nome account valido per visualizzare la password", + "click_to_generate_password": "Clicca per generare una password", + "re_enter_generate_password": "Inserisci di nuovo la Password Generata", + "understand_that_APP_NAME_cannot_recover_password": "Ho capito che %(APP_NAME)s non può recuperare le password perse", + "i_saved_password": "Ho salvato in modo sicuro la mia password generata automaticamente", + "update_password": "Aggiorna la password", + "confirm_password": "Conferma la password", + "account_updated": "Account aggiornato", + "password_must_be_characters_or_more": "La password deve essere lunga %(amount)s caratteri o più", + "need_password_or_key": "Hai bisogno di una password o di una chiave privata (non una chiave pubblica)", + "login_to_see_memo": "Esegui il login per visualizzare la memo", + "new_password": "Nuova password", + "incorrect_password": "Password errata", + "username_does_not_exist": "Il nome utente non esiste", + "account_name_should_start_with_a_letter": "Il nome dell'account deve iniziare con una lettera.", + "account_name_should_be_shorter": "Il nome dell'account deve essere più corto.", + "account_name_should_be_longer": "Il nome dell'account deve essere più lungo.", + "account_name_should_have_only_letters_digits_or_dashes": "Il nome account deve essere composto solo da lettere, cifre e trattini.", + "cannot_increase_reward_of_post_within_the_last_minute_before_payout": "Non è possibile aumentare le ricompense del post durante l'ultimo minuto prima del pagamento", + "vote_currently_exists_user_must_be_indicate_a_to_reject_witness": "Attualmente il voto esiste, l'utente deve indicare la volonta di rigettare il witness ", + "only_one_APP_NAME_account_allowed_per_ip_address_every_10_minutes": "è possibile utilizzare un solo Steem account per indirizzo IP ogni 10 minuti", + "resteem_this_post": "Fai il resteem di questo post", + "reblog": "Resteem", + "write_your_story": "Scrivi la tua storia", + "remember_voting_and_posting_key": "Ricorda la chiave di voto e di pubblicazione", + "auto_login_question_mark": "Accesso automatico?", + "hide_private_key": "Nascondi la chiave privata", + "show_private_key": "Mostra la chiave privata", + "login_to_show": "Esegui il login per mostrare", + "not_valid_email": "Email non valida", + "thank_you_for_being_an_early_visitor_to_APP_NAME": "Ti ringraziamo per essere uno dei primi visitatori di %(APP_NAME)s. Sarai ricontattato il prima possibile. ", + "author_rewards": "Ricompense da autore", + "curation_rewards": "Ricompense da curatore", + "sorry_your_reddit_account_doesnt_have_enough_karma": "Purtroppo il tuo account Reddit non possiede abbastanza Reddit Karma per essere usato per la registrazione gratuita. Aggiungi per favore la tua email per ottenere un posto in lista d'attesa.", + "register_with_facebook": "Registrati con Facebook", + "or_click_the_button_below_to_register_with_facebook": "O premi il pulsante sottostante per registrarti con Facebook", + "server_returned_error": "Errore del server", + "APP_NAME_support": "Supporto %(APP_NAME)s", + "please_email_questions_to": "Per piacere invia le tue domande tramite email a", + "next_7_strings_single_block": { + "authors_get_paid_when_people_like_you_upvote_their_post": "Gli autori ricevono un compenso quando gli utenti come te votano il loro post. ", + "if_you_enjoyed_what_you_read_earn_amount": "Se ti piace quello che hai letto, crea il tuo account oggi e inizia ricevendo STEEM GRATIS!", + "free_steem": "STEEM GRATIS!", + "sign_up_earn_steem": "Registrati adesso per guadagnare" + }, + "next_3_strings_together": { + "show_more": "Mostra di più", + "show_less": "Mostra di meno", + "value_posts": "post di basso valore" + }, + "read_only_mode": "Server in manutenzione, modalità sola lettura attiva. Ci scusiamo per l'inconveniente.", + "tags_and_topics": "Tag e argomenti", + "show_more_topics": "Vedi più argomenti", + "basic": "Base", + "advanced": "Avanzato", + "views": { + "zero": "Nessuna Visualizzazione", + "one": "%(count)s Visualizzazione", + "other": "%(count)s Visualizzazioni" + }, + "responses": { + "zero": "Nessuna Risposta", + "one": "%(count)s Risposta", + "other": "%(count)s Risposte" + }, + "post_key_warning": { + "confirm": "You are about to publish a STEEM private key or master password. You will probably lose control of the associated account and all its funds.", + "warning": "Legitimate users, including employees of Steemit Inc., will never ask you for a private key or master password.", + "checkbox": "I understand" + } + }, + "navigation": { + "about": "Circa", + "explore": "Esplora", + "APP_NAME_whitepaper": "%(APP_NAME)s Whitepaper", + "buy_LIQUID_TOKEN": "Compra %(LIQUID_TOKEN)s", + "sell_LIQUID_TOKEN": "Vendi %(LIQUID_TOKEN)s", + "currency_market": "Mercato della valuta", + "stolen_account_recovery": "Recupero degli Account Rubati", + "change_account_password": "Cambia la password dell'account", + "witnesses": "Witness", + "vote_for_witnesses": "Vota per i Witness", + "privacy_policy": "Privacy Policy", + "terms_of_service": "Condizioni di servizio", + "sign_up": "Entra", + "learn_more": "Scopri di più", + "welcome": "Benvenuto", + "faq": "FAQ", + "shop": "The Steemit Shop", + "chat": "Steemit Chat", + "app_center": "Steemit App Center", + "api_docs": "Steemit API Docs", + "bluepaper": "Steem Bluepaper", + "whitepaper": "Steem Whitepaper", + "intro_tagline": "Il denaro parla", + "intro_paragraph": "La tua voce vale qualcosa. Entra nella comunità che ti pagare per pubblicare e curare contenuti di alta qualità." + }, + "main_menu": { + "hot": "hot", + "trending": "trending" + }, + "reply_editor": { + "shorten_title": "Titolo breve", + "exceeds_maximum_length": "Supera la grandezza massima (%(maxKb)sKB)", + "including_the_category": " (inclusa la categoria '%(rootCategory)s')", + "use_limited_amount_of_tags": "Hai %(tagsLength)s tag in totale %(includingCategory)s. Usane solo 5 nel tuo post e nella linea delle categorie.", + "are_you_sure_you_want_to_clear_this_form": "Sicuro di voler cancellare questo form?", + "uploading": "Caricamento", + "draft_saved": "Bozza salvata", + "editor": "Editor", + "insert_images_by_dragging_dropping": "Per inserire le immagini, trascinale qui", + "pasting_from_the_clipboard": "incollando dagli appunti,", + "selecting_them": "selezionandole. ", + "image_upload": "Carica immagine", + "power_up_100": "Power Up 100%%", + "default_50_50": "Default (50%% / 50%%)", + "decline_payout": "Rinuncia al pagamento", + "check_this_to_auto_upvote_your_post": "Clicca qui per effettuare l'upvote automatico del tuo post", + "markdown_styling_guide": "Guida di stile Markdown", + "or_by": "o da", + "title": "Titolo", + "update_post": "Aggiorna il Post", + "markdown_not_supported": "Il Markdown non è supportato qui" + }, + "category_selector_jsx": { + "tag_your_story": "Tag (massimo 5). Il primo tag è la categoria principale. ", + "select_a_tag": "Seleziona un tag", + "maximum_tag_length_is_24_characters": "La lunghezza massima di un tag è 24 caratteri", + "use_limited_amount_of_categories": "Utilizzare solo %(amount)s categorie", + "use_only_lowercase_letters": "Usa solo caratteri minuscoli", + "use_one_dash": "Usa solo un trattino", + "use_spaces_to_separate_tags": "Usa gli spazi per separare i tag", + "use_only_allowed_characters": "Usa solo lettere minuscole, cifre e un trattino", + "must_start_with_a_letter": "Deve iniziare con una lettera", + "must_end_with_a_letter_or_number": "Deve terminare con una lettera o un numero" + }, + "postfull_jsx": { + "this_post_is_not_available_due_to_a_copyright_claim": "Questo post non è disponibile per violazione del copyright.", + "share_on_facebook": "Condividi su Facebook", + "share_on_twitter": "Condividi su Twitter", + "share_on_linkedin": "Condividi su Linkedin", + "recent_password": "Password recente", + "in_week_convert_DEBT_TOKEN_to_LIQUID_TOKEN": "In 3,5 giorni, converti %(amount)s%(DEBT_TOKEN)s in %(LIQUID_TOKEN)s", + "view_the_full_context": "Vedi il contesto completo", + "view_the_direct_parent": "Visualizza il collegamento diretto", + "you_are_viewing_a_single_comments_thread_from": "Stai visualizzando il commento singolo di un thread di " + }, + "market_jsx": { + "action": "Azione", + "date_created": "Data creazione", + "last_price": "Ultimo prezzo", + "24h_volume": "Volume nelle 24 ore", + "spread": "Spread", + "total": "Totale", + "available": "Disponibile", + "lowest_ask": "Prezzo di vendità più basso", + "highest_bid": "Prezzo di acquisto più alto", + "buy_orders": "Ordini d'acquisto", + "sell_orders": "Ordini di vendita", + "trade_history": "Cronologia delle Transazioni", + "open_orders": "Ordini Aperti", + "sell_amount_for_atleast": "Vendi %(amount_to_sell)s per almeno %(min_to_receive)s (%(effectivePrice)s)", + "buy_atleast_amount_for": "Compra ad almeno %(min_to_receive)s per %(amount_to_sell)s (%(effectivePrice)s)", + "price_warning_above": "Questo prezzo è ben al di sopra del prezzo di mercato corrente di %(marketPrice)s, sei sicuro?", + "price_warning_below": "Questo prezzo è ben al di sotto del prezzo di mercato corrente di %(marketPrice)s, sei sicuro?", + "order_cancel_confirm": "Cancella l'ordine %(order_id)s per %(user)s?", + "order_cancelled": "Ordine %(order_id)s cancellato", + "higher": "Più alto", + "lower": "Più basso", + "total_DEBT_TOKEN_SHORT_CURRENCY_SIGN": "Totale %(DEBT_TOKEN_SHORT)s (%(CURRENCY_SIGN)s)" + }, + "recoveraccountstep1_jsx": { + "begin_recovery": "Inizia il Ripristino", + "not_valid": "Non valido", + "account_name_is_not_found": "Nome account non trovato", + "unable_to_recover_account_not_change_ownership_recently": "Non possiamo recuperare questo account, non ha cambiato proprietario di recente. ", + "password_not_used_in_last_days": "Questa password non viene usata per questo account da 30 giorni. ", + "request_already_submitted_contact_support": "La tua richiesta è stata già inviata e la stiamo lavorando. Puoi contattare %(SUPPORT_EMAIL)sper verificare lo stato della tua richiesta.", + "recover_account_intro": "Può succedere che ogni tanto la chiave personale di un membro di Steemit possa essere compromessa. La funzione \"Recupera account rubato\" dà 30 giorni al legittimo proprietario dell'account per recuperarlo dal momento in cui il ladro cambia la chiave personale. \"Recupera account rubato\" può essere usato solo su %(APP_URL)s se il proprietario dell'account ha precedentemente inserito %(APP_NAME)s come account fidato e accettato i Termini di Servizio di %(APP_NAME)s.", + "login_with_facebook_or_reddit_media_to_verify_identity": "Effettua il login con Facebook o Reddit per verificare la tua identità ", + "login_with_social_media_to_verify_identity": "Effettua il login con %(provider)s per verificare la tua identità", + "enter_email_toverify_identity": "Abbiamo bisogno di verificare la tua identità. Inserisci di seguito il tuo indirizzo email per iniziare la verifica. ", + "continue_with_email": "Continua con l'email", + "thanks_for_submitting_request_for_account_recovery": "Grazie per aver immesso la tua richiesta per Ripristinare il tuo Account usando l'autenticazione multi fattoriale basata sulla blockchain di %(APP_NAME)s. Ti risponderemo il prima possibile, comunque  considera che potrebbero esserci ritardi nella risposta a causa del grade volume di email. Preparati a verificare la tua identità.", + "recovering_account": "Stiamo recuperando l'account", + "recover_account": "Recupero Account", + "checking_account_owner": "Stiamo controllando il proprietario dell'account", + "sending_recovery_request": "Invia richiesta di recupero ", + "cant_confirm_account_ownership": "Non siamo riusciti a confermare la proprietà dell'account. Controlla la password.", + "account_recovery_request_not_confirmed": "La richiesta di recupero account non è ancora stata confermata. Per favore, ritorna tra un po'. Grazie per la pazienza! " + }, + "user_profile": { + "unknown_account": "Account sconosciuto", + "user_hasnt_made_any_posts_yet": "Sembra che %(name)snon abbia ancora pubblicato nessun post! ", + "user_hasnt_started_bloggin_yet": "Sembra che %(name)s non abbia ancora iniziato a scrivere!", + "user_hasnt_followed_anything_yet": "Sembra che %(name)snon stia seguendo ancora nessuno! Se %(name)sha aggiunto di recente nuovi utenti da seguire, il feed personalizzato si riempirà di contenuti non appena ce ne saranno di nuovi. ", + "user_hasnt_had_any_replies_yet": "%(name)s hasn't had any replies yet", + "looks_like_you_havent_posted_anything_yet": "Looks like you haven't posted anything yet.", + "create_a_post": "Create a Post", + "explore_trending_articles": "Explore Trending Articles", + "read_the_quick_start_guide": "Read The Quick Start Guide", + "browse_the_faq": "Browse The FAQ", + "followers": "Followers", + "this_is_users_reputations_score_it_is_based_on_history_of_votes": "Questo è il livello di reputazione di %(name)s. \n\nIl livello di reputazione si basa sulla storia dei voti ricevuti dall'account e viene utilizzato per nascondere contenuti di scarsa qualità. ", + "follower_count": { + "zero": "Nessun follower", + "one": "1 follower", + "other": "%(count)s follower" + }, + "followed_count": { + "zero": "Non segue nessuno", + "one": "1 following", + "other": "%(count)s following" + }, + "post_count": { + "zero": "Nessun post", + "one": "1 post", + "other": "%(count)s post" + } + }, + "authorrewards_jsx": { + "estimated_author_rewards_last_week": "Ricompense per l'autore stimate durante l'ultima settimana", + "author_rewards_history": "Cronologia di Ricompense dell'autore" + }, + "curationrewards_jsx": { + "estimated_curation_rewards_last_week": "Ricompense da curatore stimate nell'ultima settimana", + "curation_rewards_history": "Cronologia ricompense da curatore" + }, + "post_jsx": { + "now_showing_comments_with_low_ratings": "Commenti con voti bassi rivelati", + "sort_order": "Ordinamento", + "comments_were_hidden_due_to_low_ratings": "Commenti nascosti a causa del basso rating" + }, + "voting_jsx": { + "flagging_post_can_remove_rewards_the_flag_should_be_used_for_the_following": "Flaggare un post può rimuovere le ricompense e rendere questo materiale meno visibile. Alcune ragioni comuni per effettuare un flag sono", + "disagreement_on_rewards": "Disaccordo sulle ricompense", + "fraud_or_plagiarism": "Frode o Plagio", + "hate_speech_or_internet_trolling": "Incitamento all'odio o Trolling", + "intentional_miss_categorized_content_or_spam": "Contenuto volutamente categorizzato impropriamente o spam", + "pending_payout": "Pagamento in attesa $%(value)s", + "payout_declined": "Pagamento Rifiutato", + "max_accepted_payout": "Il pagamento massimo accettato è di %(value)s $", + "promotion_cost": "Costo della Promozione $%(value)s", + "past_payouts": "Pagamenti precedenti di %(value)s $", + "past_payouts_author": "- Autore %(value)s $", + "past_payouts_curators": "- Curatori %(value)s $", + "we_will_reset_curation_rewards_for_this_post": "sarà effettuato il reset delle ricompense del curatore in questo post. ", + "removing_your_vote": "Rimuovi il tuo voto", + "changing_to_an_upvote": "Cambia in un upvote", + "changing_to_a_downvote": "Cambia in un downvote", + "confirm_flag": "Conferma il flag", + "and_more": "e %(count)s in più", + "votes_plural": { + "one": "%(count)s voto", + "other": "%(count)s voti" + } + }, + "witnesses_jsx": { + "witness_thread": "witness thread", + "top_witnesses": "Voto Witness ", + "you_have_votes_remaining": { + "zero": "Non hai voti residui", + "one": "Ti resta 1 voto", + "other": "Hai %(count)svoti residui" + }, + "you_can_vote_for_maximum_of_witnesses": "Puoi votare un massimo di 30 witness", + "witness": "Witness", + "information": "Informazioni", + "if_you_want_to_vote_outside_of_top_enter_account_name": "Se desideri votare un witness al di fuori della top 50, inserisci di seguito il nome account per effettuare il voto. ", + "set_witness_proxy": "Puoi anche scegliere un proxy che voterà i witnesses per te. Questo resetterà la tua attuale selezione di testimoni.", + "witness_set": "Hai settato un proxy di voto. Se volessi riabilitare il voto manuale, cancella il tuo proxy.", + "witness_proxy_current": "Il tuo attuale proxy è:", + "witness_proxy_set": "Setta il proxy", + "witness_proxy_clear": "Cancella il proxy", + "proxy_update_error": "Il tuo proxy non è aggiornato" + }, + "votesandcomments_jsx": { + "no_responses_yet_click_to_respond": "Non c'è ancora nessuna risposta. Clicca per rispondere.", + "response_count_tooltip": { + "zero": "Nessun commento. Clicca per commentare.", + "one": "1 commento. Clicca per commentare.", + "other": "%(count)s commenti. Clicca per commentare." + }, + "vote_count": { + "zero": "Nessun voto", + "one": "1 voto", + "other": "%(count)s voti" + } + }, + "userkeys_jsx": { + "public": "Pubblico", + "private": "Privato", + "public_something_key": "%(key)sChiave Pubblica", + "private_something_key": "%(key)sChiave Privata", + "posting_key_is_required_it_should_be_different": "La chiave di pubblicazione si usa per pubblicare e votare. Dovrebbe essere diversa dalla chiave attiva e dalla chiave proprietaria. ", + "the_active_key_is_used_to_make_transfers_and_place_orders": "La chiave attiva si usa per effettuare trasferimenti e per piazzare ordini nel market interno. ", + "the_owner_key_is_required_to_change_other_keys": "La chiave proprietaria è la chiave principale dell'account e serve a cambiare le altre chiavi. ", + "the_private_key_or_password_should_be_kept_offline": "Raccomandiamo di conservare offline la chiave privata e la password della chiave proprietaria. ", + "the_memo_key_is_used_to_create_and_read_memos": "La chiave memo si usa per creare e leggere i memo. " + }, + "suggestpassword_jsx": { + "APP_NAME_cannot_recover_passwords_keep_this_page_in_a_secure_location": "%(APP_NAME)snon può recuperare la password. Conserva questa pagina in un luogo sicuro, come ad esempio una cassaforte o una cassetta di sicurezza. ", + "APP_NAME_password_backup": "Password Backup di %(APP_NAME)s", + "APP_NAME_password_backup_required": "Password Backup di %(APP_NAME)s (richiesto)", + "after_printing_write_down_your_user_name": "Dopo la stampa, scrivi il tuo nome utente. " + }, + "converttosteem_jsx": { + "your_existing_DEBT_TOKEN_are_liquid_and_transferable": "I tuoi %(DEBT_TOKEN)s sono liquidi e trasferibili. Invece se desiderassi scambiare %(DEBT_TOKEN)s direttamente in questo sito su %(link)s o trasferirli su un mercato esterno.", + "this_is_a_price_feed_conversion": "Questa è una conversione di prezzo. L'attesa di 3,5 giorni è necessaria per evitare di abusare della media dei prezzi.", + "convert_to_LIQUID_TOKEN": "Converti in %(LIQUID_TOKEN)s", + "DEBT_TOKEN_will_be_unavailable": "Questa azione avverrà tra 3,5 giorni da adesso e non può essere cancellata. Questi%(DEBT_TOKEN)s diverranno subito indisponibili" + }, + "tips_js": { + "liquid_token": "Token scambiabili che possono essere trasferiti ovunque in qualsiasi momento.
      %(LIQUID_TOKEN)s possono essere convertiti in %(VESTING_TOKEN)s con un processo chiamato powering up.", + "influence_token": "Token che ti danno più controllo sui pagamenti dei post e ti consentono di guadagnare sulle ricompense da curatore.", + "estimated_value": "Il valore stimato si basa sul valore medio di %(LIQUID_TOKEN)s in dollari US. ", + "non_transferable": "%(VESTING_TOKEN)s non è trasferibile e richiede 3 mesi, (13 pagamenti) per riconvertirli in %(LIQUID_TOKEN)s.", + "converted_VESTING_TOKEN_can_be_sent_to_yourself_but_can_not_transfer_again": "I %(VESTING_TOKEN)s convertiti possono essere spediti a te stesso o a qualcun altro ma non possono non ritrasferirli senza essere convertirli di nuovo in %(LIQUID_TOKEN)s.", + "part_of_your_steem_power_is_currently_delegated": "Parte del tuo STEEM POWER ti è attualmente delegato. La delegazione è donata per l'influenza o per aiutare i nuovi utenti ad eseguire le azioni su steemit. Il quantitativo delegato può fluttuare." + }, + "promote_post_jsx": { + "promote_post": "Promuovi post", + "spend_your_DEBT_TOKEN_to_advertise_this_post": "Spendi %(DEBT_TOKEN)s per pubblicizzare questo post nella sezione dei contenuti promossi.", + "you_successfully_promoted_this_post": "Hai promosso questo post con successo", + "this_post_was_hidden_due_to_low_ratings": "Questo post è stato nascosto a causa della bassa reputazione" + }, + "about_jsx": { + "about_app": "About %(APP_NAME)s", + "about_app_details": "%(APP_NAME)s è una piattaforma di social media dove tutti vengono ricompensati per la creazione e la cura di contenuti. Utilizza un complesso sistema a punti digitali, chiamato Steem che possiede un valore effettivo per le ricompense digitali attraverso la scoperta dei prezzi di mercato e la liquidità. ", + "learn_more_at_app_url": "Scopri di più %(APP_URL)s", + "resources": "Risorse" + }, + "markdownviewer_jsx": { + "images_were_hidden_due_to_low_ratings": "Le immagini sono state nascoste a causa della bassa reputazione." + }, + "postsummary_jsx": { + "resteemed": "Condiviso", + "resteemed_by": "Condiviso da", + "reveal_it": "mostra", + "adjust_your": "sistema il tuo", + "display_preferences": "mostrare le tue preferenze", + "create_an_account": "creare un accout", + "to_save_your_preferences": "per salvare le tue preferenze" + }, + "posts_index": { + "empty_feed_1": "Sembra che tu non stia ancora seguendo nessuno. ", + "empty_feed_2": "Se hai da poco aggiunto nuovi utenti da seguire, quando ci saranno nuovi contenuti disponibili li vedrai nel tuo feed personalizzato.", + "empty_feed_3": "Esplora gli Articoli in Trending", + "empty_feed_4": "Leggi la Guida di Avvio Rapido", + "empty_feed_5": "Leggi le FAQ" + }, + "transferhistoryrow_jsx": { + "to_savings": "ai risparmi", + "from_savings": "dai risparmi", + "cancel_transfer_from_savings": "Cancella il trasferimento dai risparmi", + "stop_power_down": "Interrompi il Power Down", + "start_power_down_of": "Avvia il Power Down di", + "receive_interest_of": "Ricevi l'interesse di" + }, + "savingswithdrawhistory_jsx": { + "cancel_this_withdraw_request": "Cancellare questa richiesta di prelevamento?", + "pending_savings_withdrawals": "PRELIEVI DEI RISPARMI IN SOSPESO", + "withdraw": "Ritira %(amount)s", + "to": "a %(to)s", + "from_to": "da %(from)s a %(to)s" + }, + "explorepost_jsx": { + "copied": "Copiato!", + "copy": "Copia", + "alternative_sources": "Fonti alternative" + }, + "header_jsx": { + "home": "Home", + "create_a_post": "Crea un post", + "change_account_password": "Cambia la password dell'account", + "create_account": "Crea un account", + "stolen_account_recovery": "Recupero account rubato", + "people_following": "Persone che seguono", + "people_followed_by": "Persone seguite da ", + "curation_rewards_by": "Ricompense da curatore da", + "author_rewards_by": "Ricompense da autore da", + "replies_to": "Risposte a ", + "comments_by": "Commenti di" + }, + "loginform_jsx": { + "you_need_a_private_password_or_key": "Hai bisogno di una password privata o di una chiave (non di una chiave pubblica)", + "cryptography_test_failed": "Test di crittofrafia fallito", + "unable_to_log_you_in": "Non è possibile effettuare il log in con questo browser.", + "the_latest_versions_of": "Le ultime versioni di ", + "are_well_tested_and_known_to_work_with": "sono testate e conosciute per funzionare con %(APP_URL)s.", + "due_to_server_maintenance": "A causa della manutenzione del server siamo in modalità sola lettura. Ci dispiace per l'inconveniente.", + "login_to_vote": "Effettua il login per votare ", + "login_to_post": "Effettua il login per postare", + "login_to_comment": "Effettua il login per commentare", + "posting": "Post", + "active_or_owner": "Attivo o proprietario", + "this_password_is_bound_to_your_account_owner_key": "Questa password è collegata alla chiave proprietaria del tuo account e non può essere usata per effettuare il login su questo sito. ", + "however_you_can_use_it_to": "Nonostante questo, puoi usarla per", + "update_your_password": "aggiornare la tua password", + "to_obtain_a_more_secure_set_of_keys": "per ottenere un set di chiavi più sicuro. ", + "this_password_is_bound_to_your_account_active_key": "Questa password è collegata alla chiave attiva del tuo account e non può essere usata per effettuare il login su questa pagina. ", + "you_may_use_this_active_key_on_other_more": "Puoi usare la chiave attiva su altre pagine di sicurezza come le pagine Wallet o Market. ", + "you_account_has_been_successfully_created": "Il tuo account è stato creato con successo! ", + "you_account_has_been_successfully_recovered": "Il tuo account è stato ripristinato con successo!", + "password_update_succes": "La password dell'account %(accountName)s è stata aggiornata con successo", + "password_info": "La password o la chiave privata è stata inserita erroneamente. C'è probabilmente un errore di battitura o un inserimento di dati errato. Suggerimento: Una password o una chiave privata generata da Steemit non conterrà mai i caratteri 0 (zero), O (o maiuscola), I (i maiuscola) e l (l minuscola).", + "enter_your_username": "Inserisci il tuo username", + "password_or_wif": "Password o WIF", + "this_operation_requires_your_key_or_master_password": "Per eseguire l'operazione è necessaria la propria chiave %(authType)so Master password. ", + "keep_me_logged_in": "Voglio restare connesso", + "amazing_community": "una comunità incredibile", + "to_comment_and_reward_others": "per commentare e premiare gli altri. ", + "sign_up_get_steem": "Sign up. Get STEEM", + "signup_button": "Sign up now to earn ", + "signup_button_emphasis": "FREE STEEM!", + "returning_users": "Utenti di ritorno:", + "join_our": "Unisciti al nostro" + }, + "chainvalidation_js": { + "account_name_should": "Il nome account", + "not_be_empty": "non deve essere vuoto.", + "be_longer": "essere più lungo.", + "be_shorter": "essere più corto.", + "each_account_segment_should": "Ogni segmento dell'account dovrebbe", + "start_with_a_letter": "iniziare con una lettera.", + "have_only_letters_digits_or_dashes": "avere solo lettere, cifre o trattini.", + "have_only_one_dash_in_a_row": "avere solo un trattino per riga. ", + "end_with_a_letter_or_digit": "terminare con una lettera o una cifra.", + "verified_exchange_no_memo": "Devi includere una memo per il trasferimento." + }, + "settings_jsx": { + "invalid_url": "URL invalido", + "name_is_too_long": "Nome troppo lungo", + "name_must_not_begin_with": "Il nome non deve iniziare con @", + "about_is_too_long": "About troppo lungo ", + "location_is_too_long": "Luogo è troppo lungo", + "website_url_is_too_long": "L'url del sito è troppo lunga ", + "public_profile_settings": "Impostazioni profilo pubblico", + "private_post_display_settings": "Impostazioni di Visualizzazione del Post Privato", + "not_safe_for_work_nsfw_content": "Contenuto inappropriato da visualizzare da un luogo di lavoro (NSFW)", + "always_hide": "Nascondi sempre", + "always_warn": "Avverti sempre", + "always_show": "Mostra sempre", + "muted_users": "Utenti mutati", + "update": "Aggiornamento", + "profile_image_url": "Url immagine profilo", + "cover_image_url": "Url immagine di copertina", + "profile_name": "Mostra nome", + "profile_about": "About", + "profile_location": "Località", + "profile_website": "Sito" + }, + "transfer_jsx": { + "amount_is_in_form": "La quantita deve essere della forma 99999.999", + "insufficient_funds": "Fondi insufficienti", + "use_only_3_digits_of_precison": "Usare solo 3 cifre di precisione", + "send_to_account": "Invia a un account", + "asset": "Asset", + "this_memo_is_private": "Questo memo è privato", + "this_memo_is_public": "Questo memo è pubblico", + "convert_to_VESTING_TOKEN": "Converti in %(VESTING_TOKEN)s", + "balance_subject_to_3_day_withdraw_waiting_period": "Saldo soggetto a 3 giorni di attesa per il prelievo.", + "move_funds_to_another_account": "Sposta i fondi verso un altro account %(APP_NAME)s", + "protect_funds_by_requiring_a_3_day_withdraw_waiting_period": "Proteggi i fondi richiedendo un periodo di attesa di 3 giorni.", + "withdraw_funds_after_the_required_3_day_waiting_period": "Preleva fondi dopo il periodo di attesa necessario di 3 giorni.", + "from": "Da", + "to": "A", + "asset_currently_collecting": "%(asset)s attualmente percepiscono %(interest)s%% ISC.", + "beware_of_spam_and_phishing_links": "Beware of spam and phishing links in transfer memos. Do not open links from users you do not trust. Do not provide your private keys to any third party websites." + }, + "userwallet_jsx": { + "conversion_complete_tip": "Si completerà su", + "in_conversion": "%(amount)s in conversione", + "transfer_to_savings": "Sposta nei Risparmi", + "power_up": "Power Up", + "power_down": "Power Down", + "market": "Market", + "convert_to_LIQUID_TOKEN": "Converti in %(LIQUID_TOKEN)s", + "withdraw_LIQUID_TOKEN": "Preleva %(LIQUID_TOKEN)s", + "withdraw_DEBT_TOKENS": "Preleva %(DEBT_TOKENS)s", + "tokens_worth_about_1_of_LIQUID_TICKER": "I token valgono circa $1.00 di %(LIQUID_TICKER)s, attualmente percepiscono %(sbdInterest)s%% ISC.", + "savings": "RISPARMI", + "estimated_account_value": "Valore stimato dell'account", + "next_power_down_is_scheduled_to_happen": "Il prossimo power downo è programmato per avvenire", + "transfers_are_temporary_disabled": "I trasferimenti sono temporaneamente disabilitati.", + "history": "CRONOLOGIA", + "redeem_rewards": "Riscuoti le ricompense (Trasferisci sul conto)", + "buy_steem_or_steem_power": "Compra STEEM o STEEM POWER" + }, + "powerdown_jsx": { + "power_down": "Power Down", + "amount": "Somma", + "already_power_down": "Stai già effettuando il powering down %(AMOUNT)s %(LIQUID_TICKER)s (%(WITHDRAWN)s %(LIQUID_TICKER)s pagati sin ora). Ricorda che se cambi l'ammontare del power down il pagamento programmato verrà resettato.", + "delegating": "Stai delegando %(AMOUNT)s %(LIQUID_TICKER)s. Questa somma sarà bloccata e non disponibile per il power down sino a quando la delega non sarà rimossa e non sia passato un intero intervallo di ricompensa.", + "per_week": "È ~%(AMOUNT)s %(LIQUID_TICKER)s a settimana.", + "warning": "Lasciare meno di %(AMOUNT)s %(VESTING_TOKEN)s sul tuo account è sconsigliato e potrebbe renderlo inutilizzabile.", + "error": "Impossibile effettuare il power down (ERROR: %(MESSAGE)s)" + }, + "checkloginowner_jsx": { + "your_password_permissions_were_reduced": "I tuoi permessi delle password sono stati ridotti", + "if_you_did_not_make_this_change": "Se non hai fatto questo cambio per favore", + "ownership_changed_on": "Proprietà Cambiata Su", + "deadline_for_recovery_is": "Il termine per il ripristino è", + "i_understand_dont_show_again": "Ho capito, non mostrare nuovamente" + } +} diff --git a/src/app/locales/normalize.sh b/src/app/locales/normalize.sh new file mode 100755 index 0000000..9bbd7fa --- /dev/null +++ b/src/app/locales/normalize.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Normalize whitespace: replace 4 spaces at beginning of locale.js lines with tabs +sed -i '' -e 's/^ / /g' *.js diff --git a/src/app/locales/ru.json b/src/app/locales/ru.json new file mode 100644 index 0000000..4fdad0b --- /dev/null +++ b/src/app/locales/ru.json @@ -0,0 +1,668 @@ +{ + "g": { + "age": "возраст", + "amount": "Количество", + "and": "и", + "are_you_sure": "Вы уверены?", + "ask": "Продажа", + "balance": "Баланс", + "balances": "Балансы", + "bid": "Покупка", + "blog": "Блог", + "browse": "Посмотреть", + "buy": "Купить", + "buy_or_sell": "Купить или Продать", + "by": "от", + "cancel": "Отмена", + "change_password": "Сменить пароль", + "choose_language": "Выберите язык", + "clear": "Очистить", + "close": "Закрыть", + "collapse_or_expand": "Свернуть/Развернуть", + "comments": "Комментарии", + "confirm": "Подтвердить", + "convert": "Конвертировать", + "date": "Дата", + "delete": "Удалить", + "dismiss": "Скрыть", + "edit": "Редактировать", + "email": "Электронная почта", + "feed": "Лента", + "follow": "Подписаться", + "for": " для ", + "from": " от ", + "go_back": "Назад", + "hide": "Скрыть", + "in": "в", + "in_reply_to": "в ответ на", + "insufficient_balance": "Недостаточный баланс", + "invalid_amount": "Недостаточно средств", + "joined": "Присоединился", + "loading": "Загрузка", + "login": "Войти", + "logout": "Выйти", + "memo": "Примечание", + "mute": "Заблокировать", + "new": "новое", + "newer": "Новее", + "next": "Следующий", + "no": "Нет", + "ok": "ОК", + "older": "Старее", + "or": "или", + "order_placed": "Заказ размещен", + "password": "Пароль", + "payouts": "Выплаты", + "permissions": "Разрешения", + "phishy_message": "Link expanded to plain text; beware of a potential phishing attempt", + "post": "Пост", + "post_as": "Запостить как", + "posts": "Посты", + "powered_up_100": "100%% в Силе Голоса", + "preview": "Предварительный просмотр", + "previous": "Предыдущий", + "price": "Цена", + "print": "Распечатать", + "promote": "Продвинуть", + "promoted": "продвигаемое", + "re": "RE", + "re_to": "RE: %(topic)s", + "recent_password": "Недавний пароль", + "receive": "Получено ", + "remove": "Удалить", + "remove_vote": "Убрать голос", + "replied_to": "ответил %(account)s", + "replies": "Ответы", + "reply": "Ответить", + "reply_count": { + "zero": "нет ответов", + "one": "1 ответ", + "few": "%(count)s ответа", + "many": "%(count)s ответов", + "other": "%(count)s ответов" + }, + "reputation": "Репутация", + "reveal_comment": "Показать комментарий", + "request": "запрос", + "required": "Обязательно", + "rewards": "вознаграждение", + "save": "Сохранить", + "saved": "Сохранено", + "search": "Поиск", + "sell": "Продать", + "settings": "Настройки", + "share_this_post": "Поделиться этим постом", + "show": "Показать", + "sign_in": "Войти", + "sign_up": "Регистрация", + "since": "начиная с", + "submit": "Отправить", + "power_up": "Усилить силу голоса", + "submit_a_story": "Добавить пост", + "tag": "Тег", + "to": " к ", + "all_tags": "All tags", + "transfer": "Перевод ", + "trending_topics": "Популярное", + "type": "Тип", + "unfollow": "Отписаться", + "unmute": "Разблокировать", + "unknown": "Неизвестно", + "upvote": "Голосовать за", + "upvote_post": "Проголосовать за пост", + "username": "Имя пользователя", + "version": "Версия", + "vote": "Проголосовать", + "votes": "голосов", + "wallet": "Кошелек", + "warning": "внимание", + "yes": "Да", + "posting": "Постинг", + "owner": "Владелец", + "active": "Активный", + "account_not_found": "Аккаунт не найден", + "this_is_wrong_password": "Это неправильный пароль", + "do_you_need_to": "Вам нужно", + "account_name": "Имя аккаунта", + "recover_your_account": "восстановить ваш аккаунт", + "reset_usernames_password": "Сбросить пароль пользователя %(username)s", + "this_will_update_usernames_authtype_key": "Это обновит %(username)s %(authType)s ключ", + "passwords_do_not_match": "Пароли не совпадают", + "you_need_private_password_or_key_not_a_public_key": "Вам нужен приватный пароль или ключ (не публичный ключ)", + "the_rules_of_APP_NAME": { + "one": "Первое правило сети %(APP_NAME)s: не теряйте свой пароль.", + "second": "Второе правило %(APP_NAME)s: Не теряйте свой пароль.", + "third": "Третье правило %(APP_NAME)s: мы не можем восстановить ваш пароль.", + "fourth": "Четвертое правило: если вы можете запомнить свой пароль, значит он не безопасен.", + "fifth": "Пятое правило: используйте только сгенерированные случайным образом пароли.", + "sixth": "Шестое правило: Никому не говорите свой пароль.", + "seventh": "Седьмое правило: Всегда надежно храните свой пароль." + }, + "recover_password": "Восстановить аккаунт", + "current_password": "Текущий пароль", + "generated_password": "Сгенерированный пароль", + "backup_password_by_storing_it": "Сделайте резервную копию в менеджере паролей или текстовом файле", + "enter_account_show_password": "Введите действительное имя аккаунта, чтобы показать пароль", + "click_to_generate_password": "Нажмите, чтобы сгененировать пароль", + "re_enter_generate_password": "Повторно введите пароль", + "understand_that_APP_NAME_cannot_recover_password": "Я понимаю, что %(APP_NAME)s не может восстановить потерянные пароли", + "i_saved_password": "Я надежно сохранил сгенерированный пароль", + "update_password": "Обновить пароль", + "confirm_password": "Подтвердить пароль", + "account_updated": "Аккаунт обновлен", + "password_must_be_characters_or_more": "Пароль должен содержать %(amount)s символ(а) или больше", + "need_password_or_key": "Вам нужен приватный пароль или ключ (не публичный ключ)", + "login_to_see_memo": "войти чтобы увидеть примечание", + "new_password": "Новый пароль", + "incorrect_password": "Неправильный пароль", + "username_does_not_exist": "Имя пользователя не найдено", + "account_name_should_start_with_a_letter": "Имя аккаунта должно начинаться с буквы.", + "account_name_should_be_shorter": "Имя аккаунта должно быть короче.", + "account_name_should_be_longer": "Имя аккаунта должно быть длиннее.", + "account_name_should_have_only_letters_digits_or_dashes": "Имя аккаунта должно должно состоять только из букв, цифр или дефисов.", + "cannot_increase_reward_of_post_within_the_last_minute_before_payout": "Не удается увеличить вознаграждение за пост в последнюю минуту перед выплатой", + "vote_currently_exists_user_must_be_indicate_a_to_reject_witness": "голос уже существует, пользователь должен обозначить желание убрать делегата", + "only_one_APP_NAME_account_allowed_per_ip_address_every_10_minutes": "Только один Steem аккаунт разрешен с одного IP адреса каждые десять минут", + "resteem_this_post": "Поделиться этим постом", + "reblog": "Поделиться", + "write_your_story": "Написать свою историю", + "remember_voting_and_posting_key": "Запомнить голос и постинг ключ", + "auto_login_question_mark": "Заходить автоматически?", + "hide_private_key": "Скрыть приватный ключ", + "show_private_key": "Показать приватный ключ", + "login_to_show": "Войти, чтобы показать", + "not_valid_email": "Не действительный адрес", + "thank_you_for_being_an_early_visitor_to_APP_NAME": "Благодарим вас за то что являетесь ранним посетителем %(APP_NAME)s. Мы свяжемся с Вами при первой же возможности.", + "author_rewards": "Авторские вознаграждения", + "curation_rewards": "Кураторские вознаграждения", + "sorry_your_reddit_account_doesnt_have_enough_karma": "Извините, у вашего Reddit аккаунта недостаточно Reddit кармы чтобы иметь возможность бесплатной регистрации. Пожалуйста, добавьте вашу электронную почту чтобы записаться в лист ожидания", + "register_with_facebook": "Регистрация с Facebook", + "or_click_the_button_below_to_register_with_facebook": "Или нажмите на кнопку ниже, чтобы зарегистрироваться в Facebook", + "server_returned_error": "ошибка сервера", + "APP_NAME_support": "%(APP_NAME)s поддержка", + "please_email_questions_to": "Пожалуйста, шлите ваши вопросы на электронную почту", + "next_7_strings_single_block": { + "authors_get_paid_when_people_like_you_upvote_their_post": "Авторы получают вознаграждение, когда пользователи голосуют за их посты", + "if_you_enjoyed_what_you_read_earn_amount": "Если Вам понравилось то, что Вы здесь прочитали, создайте свой аккаунт сегодня и начните зарабатывать БЕСПЛАТНЫЕ STEEM-ы!", + "free_steem": "БЕСПЛАТНЫЕ STEEM!", + "sign_up_earn_steem": "Зарегистрируйтесь сейчас, чтобы заработать " + }, + "next_3_strings_together": { + "show_more": "Показать больше", + "show_less": "Показать меньше", + "value_posts": "меньше сообщений низкой стоимости" + }, + "read_only_mode": "По техническим причинам вебсайт доступен только для чтения, приносим свои извинения.", + "tags_and_topics": "Теги и темы", + "show_more_topics": "Показать больше тем", + "basic": "Базовый", + "advanced": "Подробнее", + "views": { + "zero": "Нет просмотров", + "one": "%(count)s просмотр", + "few": "%(count)s просмотра", + "many": "%(count)s просмотров", + "other": "%(count)s просмотров" + }, + "responses": { + "zero": "Нет ответов", + "one": "%(count)s ответ", + "few": "%(count)s ответа", + "many": "%(count)s ответов", + "other": "%(count)s ответов" + }, + "post_key_warning": { + "confirm": "Вы собираетесь опубликовать ваш приватный ключ или мастер-пароль, это может привести к потере вашего акканта и всех средств на нем.", + "warning": "Если кто-то попросил вас опубликовать или показать ваш приватный ключ, это возможно злоумышленник, который пытается украсть ваши токены.", + "checkbox": "Я понял" + } + }, + "navigation": { + "about": "О проекте", + "explore": "Исследовать", + "APP_NAME_whitepaper": "Белая книга %(APP_NAME)s", + "buy_LIQUID_TOKEN": "Купить %(LIQUID_TOKEN)s", + "sell_LIQUID_TOKEN": "Продать %(LIQUID_TOKEN)s", + "currency_market": "Биржа", + "stolen_account_recovery": "Восстановление украденного аккаунта", + "change_account_password": "Изменить пароль аккаунта", + "witnesses": "Делегаты", + "vote_for_witnesses": "Проголосовать за делегатов", + "privacy_policy": "Политика Конфиденциальности", + "terms_of_service": "Условия пользования", + "sign_up": "Присоединиться", + "learn_more": "Документация", + "welcome": "Добро пожаловать", + "faq": "ЧаВО", + "shop": "Магазин Steemit", + "chat": "Чат Steemit", + "app_center": "Центр приложений Steemit", + "api_docs": "API-документация Steemit", + "bluepaper": "Синяя бумага Steem", + "whitepaper": "Белая книга Steem", + "intro_tagline": "Здесь говорят деньги.", + "intro_paragraph": "Ваше мнение имеет цену. Присоединяйтесь к сообществу, которое платит за контент и за работу по отбору самого лучшего контента." + }, + "main_menu": { + "hot": "актуальное", + "trending": "популярное" + }, + "reply_editor": { + "shorten_title": "Сократите заголовок", + "exceeds_maximum_length": "Превышает максимальную длину (%(maxKb)sKB)", + "including_the_category": "(включая категорию «%(rootCategory)s»)", + "use_limited_amount_of_tags": "У вас %(tagsLength)s тегов, включая %(includingCategory)s. Пожалуйста, используйте не более 5 в посте и категории.", + "are_you_sure_you_want_to_clear_this_form": "Вы уверены, что вы хотите очистить эту форму?", + "uploading": "Загрузка", + "draft_saved": "Черновик сохранен.", + "editor": "Редактор", + "insert_images_by_dragging_dropping": "Вставьте изображения, перетащив их, ", + "pasting_from_the_clipboard": "или вставив из буфера обмена, ", + "selecting_them": "выбрав их", + "image_upload": "Загрузка изображения", + "power_up_100": "Сила Голоса 100%%", + "default_50_50": "По умолчанию (50%% / 50%%)", + "decline_payout": "Отказаться от выплаты", + "check_this_to_auto_upvote_your_post": "Отметьте, чтобы проголосовать за свой пост", + "markdown_styling_guide": "Руководство стилизации в Markdown", + "or_by": "или", + "title": "Заголовок", + "update_post": "Update Post", + "markdown_not_supported": "Markdown здесь не поддерживается" + }, + "category_selector_jsx": { + "tag_your_story": "Добавь теги (до 5 штук), первый тег станет основной категорией.", + "select_a_tag": "Выбрать тег", + "maximum_tag_length_is_24_characters": "Максимальная длина категории 24 знака", + "use_limited_amount_of_categories": "Пожалуйста, используйте только %(amount)s категории", + "use_only_lowercase_letters": "Используйте только символы нижнего регистра", + "use_one_dash": "Используйте только одно тире", + "use_spaces_to_separate_tags": "Используйте пробел чтобы разделить теги", + "use_only_allowed_characters": "Используйте только строчные буквы, цифры и одно тире", + "must_start_with_a_letter": "Должно начинаться с буквы", + "must_end_with_a_letter_or_number": "Должно заканчиваться с буквы или номера" + }, + "postfull_jsx": { + "this_post_is_not_available_due_to_a_copyright_claim": "Этот пост недоступен из-за жалобы о нарушении авторских прав.", + "share_on_facebook": "Поделитесь в Facebook", + "share_on_twitter": "Поделиться в Twitter", + "share_on_linkedin": "Поделиться в Linkedin", + "recent_password": "Недавний пароль", + "in_week_convert_DEBT_TOKEN_to_LIQUID_TOKEN": "В 3,5 дня перевести %(amount)s %(DEBT_TOKEN)s в %(LIQUID_TOKEN)s", + "view_the_full_context": "Показать полный контекст", + "view_the_direct_parent": "Просмотр прямого родителя", + "you_are_viewing_a_single_comments_thread_from": "Вы читаете одну нить комментариев от" + }, + "market_jsx": { + "action": "Действие", + "date_created": "Дата создания", + "last_price": "Последняя цена", + "24h_volume": "Объем за 24 часа", + "spread": "Спред", + "total": "Итого", + "available": "Доступно", + "lowest_ask": "Лучшая цена продажи", + "highest_bid": "Лучшая цена покупки", + "buy_orders": "Заказы на покупку", + "sell_orders": "Заказы на продажу", + "trade_history": "История сделок", + "open_orders": "Открытые сделки", + "sell_amount_for_atleast": "Продать %(amount_to_sell)s за %(min_to_receive)s по цене (%(effectivePrice)s)", + "buy_atleast_amount_for": "Купить по крайней мере %(min_to_receive)s за %(amount_to_sell)s (%(effectivePrice)s)", + "price_warning_above": "Эта цена намного выше текущей рыночной цены %(marketPrice)s, вы уверены?", + "price_warning_below": "Эта цена намного выше текущей рыночной цены %(marketPrice)s, вы уверены?", + "order_cancel_confirm": "Отменить заказ %(order_id)s от %(user)s?", + "order_cancelled": "Заказ %(order_id)s отменен.", + "higher": "Дороже", + "lower": "Дешевле", + "total_DEBT_TOKEN_SHORT_CURRENCY_SIGN": "Сумма %(DEBT_TOKEN_SHORT)s (%(CURRENCY_SIGN)s)" + }, + "recoveraccountstep1_jsx": { + "begin_recovery": "Начать восстановление", + "not_valid": "Недействительно", + "account_name_is_not_found": "Имя аккаунта не найдено", + "unable_to_recover_account_not_change_ownership_recently": "Мы не можем восстановить эту учетную запись, она не изменила владельца в последнее время.", + "password_not_used_in_last_days": "Этот пароль не использовался в этой учетной записи за последние 30 дней.", + "request_already_submitted_contact_support": "Ваш запрос был отправлен, и мы работаем над этим. Пожалуйста, свяжитесь с %(SUPPORT_EMAIL)s для получения статуса вашего запроса.", + "recover_account_intro": "Иногда бывает что ключ владельца может быть скомпрометирован. Восстановление украденного аккаунта дает законному владельцу 30 дней чтобы вернуть аккаунт с момента изменения владельческого ключа мошенником. Восстановление украденного аккаунта в %(APP_URL)s возможно только если владелец аккаунта ранее указал %(APP_NAME)s's в качестве доверенного лица и согласился с Условиями Использования сайта %(APP_NAME)s's.", + "login_with_facebook_or_reddit_media_to_verify_identity": "Пожалуйста, войдите используя Facebook или Reddit для подтверждения вашей личности", + "login_with_social_media_to_verify_identity": "Пожалуйста, войдите используя %(provider)s для подтверждения вашей личности", + "enter_email_toverify_identity": "Нам нужно подтвердить вашу личность. Пожалуйста, укажите вашу электронную почту ниже для начала проверки.", + "continue_with_email": "Продолжить с электронной почтой", + "thanks_for_submitting_request_for_account_recovery": "

      Благодарим Вас за отправку запроса на восстановление аккаунта используя основанную на блокчейне мультифакторную аутентификацию %(APP_NAME)s’a.

      Мы ответим Вам как можно быстрее, однако, пожалуйста, ожидайте что может быть некоторая задержка из-за большого объема писем.

      Пожалуйста, будьте готовы подтвердить свою личность.

      С уважением,

      Ned Scott

      CEO Steemit

      ", + "recovering_account": "Восстанавливаем аккаунт", + "recover_account": "Восстановить аккаунт", + "checking_account_owner": "Проверяем владельца аккаунта", + "sending_recovery_request": "Отправляем запрос восстановления", + "cant_confirm_account_ownership": "Мы не можем подтвердить владение аккаунтом. Проверьте ваш пароль", + "account_recovery_request_not_confirmed": "Запрос восстановления аккаунта еще не подтвержден, пожалуйста проверьте позднее. Спасибо за ваше терпение." + }, + "user_profile": { + "unknown_account": "Неизвестный аккаунт", + "user_hasnt_made_any_posts_yet": "Похоже, что %(name)s еще не написал постов!", + "user_hasnt_started_bloggin_yet": "Похоже. что %(name)s еще не завёл блог!", + "user_hasnt_followed_anything_yet": "Похоже, что %(name)s еще ни на кого не подписан! Если, %(name)s недавно подписался на новых пользователей, их персонализированный канал будет заполняться сразу после появления нового контента.", + "user_hasnt_had_any_replies_yet": "%(name)s еще не получил ответов", + "looks_like_you_havent_posted_anything_yet": "Похоже, ты еще ничего не опубликовал.", + "create_a_post": "Создать сообщение", + "explore_trending_articles": "Посмотреть статьи, набирающие популярность", + "read_the_quick_start_guide": "Прочтите краткое руководство", + "browse_the_faq": "Просмотреть ЧаВО", + "followers": "Подписчики", + "this_is_users_reputations_score_it_is_based_on_history_of_votes": "Это репутация %(name)s.\n\nРепутация рассчитывается на основе истории полученных голосов и используется для сокрытия низкокачественного контента.", + "follower_count": { + "zero": "Нет подписчиков", + "one": "1 подписчик", + "few": "%(count)s подписчика", + "many": "%(count)s подписчиков", + "other": "%(count)s подписчиков" + }, + "followed_count": { + "zero": "Ни на кого не подписан", + "one": "1 подписок", + "few": "%(count)s подписки", + "many": "%(count)s подписок", + "other": "%(count)s подписок" + }, + "post_count": { + "zero": "Постов нет", + "one": "1 пост", + "few": "%(count)s поста", + "many": "%(count)s постов", + "other": "%(count)s постов" + } + }, + "authorrewards_jsx": { + "estimated_author_rewards_last_week": "Оценочные авторские вознаграждения за прошлую неделю", + "author_rewards_history": "История авторских наград" + }, + "curationrewards_jsx": { + "estimated_curation_rewards_last_week": "Оценочные кураторские вознаграждения за последнюю неделю", + "curation_rewards_history": "История кураторских наград" + }, + "post_jsx": { + "now_showing_comments_with_low_ratings": "Отображаем комментарии с низким рейтингом", + "sort_order": "Порядок сортировки", + "comments_were_hidden_due_to_low_ratings": "Комментарии были скрыты из-за низкого рейтинга" + }, + "voting_jsx": { + "flagging_post_can_remove_rewards_the_flag_should_be_used_for_the_following": "Голосование против уменьшает вознаграждение и снижает позицию в рейтинге.", + "disagreement_on_rewards": "Несогласие с вознаграждением", + "fraud_or_plagiarism": "Мошенничество или плагиат", + "hate_speech_or_internet_trolling": "Разжигание ненависти или Интернет троллинг", + "intentional_miss_categorized_content_or_spam": "Преднамеренная неправильная категоризация контента или спам", + "pending_payout": "Ожидаемая выплата", + "payout_declined": "Автор отказался от вознаграждения", + "max_accepted_payout": "Максимально допустимое вознаграждение: $%(value)s", + "promotion_cost": "Цена продвижения: $%(value)s", + "past_payouts": "Предыдущие выплаты $%(value)s", + "past_payouts_author": " - Авторские $%(value)s", + "past_payouts_curators": " - Кураторские $%(value)s", + "we_will_reset_curation_rewards_for_this_post": "это сбросит выплаты за курирование", + "removing_your_vote": "Удаление голоса", + "changing_to_an_upvote": "Изменить на голосование 'за'", + "changing_to_a_downvote": "Изменить на голосование 'против'", + "confirm_flag": "Подтвердить голос 'против'", + "and_more": "и %(count)s больше", + "votes_plural": { + "one": "%(count)s голоса", + "few": "%(count)s голоса", + "many": "%(count)s голосов", + "other": "%(count)s голосов" + } + }, + "witnesses_jsx": { + "witness_thread": "пост делегата", + "top_witnesses": "Топ делегатов", + "you_have_votes_remaining": { + "zero": "У Вас не осталось голосов", + "one": "У Вас остался 1 голос", + "few": "У Вас осталось %(count)s голоса", + "many": "У Вас осталось %(count)s голосов", + "other": "У Вас осталось %(count)s голосов" + }, + "you_can_vote_for_maximum_of_witnesses": "Вы можете голосовать максимум за 30 делегатов", + "witness": "Делегаты", + "information": "Информация", + "if_you_want_to_vote_outside_of_top_enter_account_name": "Если вы хотите проголосовать за делегата вне top 50, введите имя аккаунта ниже", + "set_witness_proxy": "Вы также можете выбрать прокси, который будет вместо вас голосовать за делегатов. Это сбросит ваш текущий выбор делегата.", + "witness_set": "Вы установили прокси для голосования. Если вы хотите голосовать вручную, пожалуйста, очистите ваш прокси.", + "witness_proxy_current": "Ваш текущий прокси", + "witness_proxy_set": "Установить прокси", + "witness_proxy_clear": "Очистить прокси", + "proxy_update_error": "Ваш прокси не был обновлен" + }, + "votesandcomments_jsx": { + "no_responses_yet_click_to_respond": "Ответов пока нет. Нажмите чтобы ответить.", + "response_count_tooltip": { + "zero": "ответов пока нет. Нажмите чтобы ответить.", + "one": "1 ответ. Нажмите чтобы ответить.", + "few": "%(count)s ответа. Нажмите чтобы ответить.", + "many": "%(count)s ответов. Нажмите чтобы ответить.", + "other": "%(count)s ответов. Нажмите чтобы ответить." + }, + "vote_count": { + "zero": "нет голосов", + "one": "1 голос", + "few": "%(count)s голоса", + "many": "%(count)s голосов", + "other": "%(count)s голосов" + } + }, + "userkeys_jsx": { + "public": "Публичное", + "private": "Приватное", + "public_something_key": "Публичный %(key)s ключ", + "private_something_key": "Приватный %(key)s ключ", + "posting_key_is_required_it_should_be_different": "Постинг ключ используется для постинга и голосования. Он должен отличаться от активного ключа и ключа владельца.", + "the_active_key_is_used_to_make_transfers_and_place_orders": "Активный ключ используется для переводов и размещения заказов на внутреннем рынке.", + "the_owner_key_is_required_to_change_other_keys": "Ключ владельца это главный ключ ко всему аккаунта, он необходим для изменения других ключей.", + "the_private_key_or_password_should_be_kept_offline": "Приватный ключ или пароль должен храниться в оффлайне так часто насколько возможно.", + "the_memo_key_is_used_to_create_and_read_memos": "Ключ примечаний используется для создания и чтения примечаний." + }, + "suggestpassword_jsx": { + "APP_NAME_cannot_recover_passwords_keep_this_page_in_a_secure_location": "%(APP_NAME)s не может восстановить пароли. Сохраните эту страницу в безопасном месте, например, в огнестойком сейфе или в депозитарной ячейке.", + "APP_NAME_password_backup": "%(APP_NAME)s резервное копирование пароля", + "APP_NAME_password_backup_required": "%(APP_NAME)s резервное копирование пароля (обязательно!)", + "after_printing_write_down_your_user_name": "После печати запишите ваше имя пользователя" + }, + "converttosteem_jsx": { + "your_existing_DEBT_TOKEN_are_liquid_and_transferable": "Ваши %(DEBT_TOKEN)s токены ликвидны. Вы можете конвертировать %(DEBT_TOKEN)s на этом сайте (%(link)s) или вывести на биржу/обменник.", + "this_is_a_price_feed_conversion": "Конвертация на сайте выполняется три с половиной дня - это требуется, чтобы исключить манипуляции с ценами.", + "convert_to_LIQUID_TOKEN": "Ковертировать в %(LIQUID_TOKEN)s", + "DEBT_TOKEN_will_be_unavailable": "Эта операция будет проходить 3-5 дней от настоящего момента и ее нельзя отменить. Эти %(DEBT_TOKEN)s мгновенно станут недоступны" + }, + "tips_js": { + "liquid_token": "Ликвидные цифровые токены, которые могут переданы куда угодно в любой момент.
      %(LIQUID_TOKEN)s может быть конвертирован в %(VESTING_TOKEN)s, этот процесс называется \"увеличение Силы Голоса\".", + "influence_token": "Токены дают вам возможность влиять на вознаграждения за контент, а также возможность зарабатывать на курации контента.", + "estimated_value": "Сметная стоимость основана на среднем значении %(LIQUID_TOKEN)s в долларах США.", + "non_transferable": "%(VESTING_TOKEN)s - не ликвидные токены, требуется три месяца (13 еженедальных выплат) чтобы сконвертировать их в ликвидные токены %(LIQUID_TOKEN)s.", + "converted_VESTING_TOKEN_can_be_sent_to_yourself_but_can_not_transfer_again": "Конвертированные %(VESTING_TOKEN)s токены могут быть отправлены себе или кому-либо еще, но не могут быть переданы вновь без конвертации в %(LIQUID_TOKEN)s.", + "part_of_your_steem_power_is_currently_delegated": "Часть STEEM POWER вам делегирована, это увеличивает ваше влияние на платформе. Количество делегированных токенов со временем может изменяться." + }, + "promote_post_jsx": { + "promote_post": "Продвинуть пост", + "spend_your_DEBT_TOKEN_to_advertise_this_post": "Используйте ваши %(DEBT_TOKEN)s чтобы прорекламировать этот пост в секции продвигаемого контента", + "you_successfully_promoted_this_post": "Операция продвижения успешно завершена", + "this_post_was_hidden_due_to_low_ratings": "Этот пост был скрыт из-за низкого рейтинга" + }, + "about_jsx": { + "about_app": "О %(APP_NAME)s", + "about_app_details": "%(APP_NAME)s - это социальная медиа платформа в которой каждый зарабатывает за создание и курирование контента. Он использует надежную систему цифровых очков под названием Голос, который поддерживает реальную ценность для цифровых наград через выявление рыночной цены и ликвидности.", + "learn_more_at_app_url": "Узнать больше в %(APP_URL)s", + "resources": "Ресурсы" + }, + "markdownviewer_jsx": { + "images_were_hidden_due_to_low_ratings": "Изображения были скрыты из-за низкого рейтинга." + }, + "postsummary_jsx": { + "resteemed": "Поделиться", + "resteemed_by": "Репостнуто", + "reveal_it": "посмотреть пост", + "adjust_your": "откорректируйте свои", + "display_preferences": "настройки отображения", + "create_an_account": "зарегистрироваться", + "to_save_your_preferences": "сохранить ваши настройки" + }, + "posts_index": { + "empty_feed_1": "Похоже, что Вы еще ни на кого не подписаны", + "empty_feed_2": "Если Вы недавно подписались на новых пользователей, Ваша персональная лента будет заполняться, когда будет доступен новый контент", + "empty_feed_3": "Посмотреть статьи, набирающие популярность", + "empty_feed_4": "Прочтите краткое руководство", + "empty_feed_5": "Просмотреть ЧаВО" + }, + "transferhistoryrow_jsx": { + "to_savings": "в сейф", + "from_savings": "из сейфа", + "cancel_transfer_from_savings": "Отменить перевод из сейфа", + "stop_power_down": "Ослабление Силы Голоса остановлено", + "start_power_down_of": "Ослабление Силы Голоса начато с", + "receive_interest_of": "Получать проценты по" + }, + "savingswithdrawhistory_jsx": { + "cancel_this_withdraw_request": "Отменить запрос вывода средств?", + "pending_savings_withdrawals": "Выплаты из сейфа", + "withdraw": "Снять %(amount)s", + "to": "для %(to)s", + "from_to": "от %(from)s к %(to)s" + }, + "explorepost_jsx": { + "copied": "Скопировано!", + "copy": "Копировать", + "alternative_sources": "Альтернативные источники" + }, + "header_jsx": { + "home": "лента", + "create_a_post": "Создать пост", + "change_account_password": "Изменить пароль аккаунта", + "create_account": "Создать Аккаунт", + "stolen_account_recovery": "Возврат украденного аккаунта", + "people_following": "Подписчики", + "people_followed_by": "Подписчики", + "curation_rewards_by": "Награда за курирование", + "author_rewards_by": "Автор награжден", + "replies_to": "Ответы на", + "comments_by": "Комментарии" + }, + "loginform_jsx": { + "you_need_a_private_password_or_key": "Вам нужен приватный пароль или ключ (не публичный ключ)", + "cryptography_test_failed": "Криптографический тест не пройден", + "unable_to_log_you_in": "У нас не получится залогинить вас в этом браузере.", + "the_latest_versions_of": "Последние версии ", + "are_well_tested_and_known_to_work_with": "хорошо протестированы и работают с %(APP_URL)s.", + "due_to_server_maintenance": "Из-за технического обслуживания сервера мы работаем в режиме чтения. Извините за неудобства.", + "login_to_vote": "Войти, чтобы проголосовать", + "login_to_post": "Войти, чтобы написать пост", + "login_to_comment": "Войти, чтобы оставить комментарий", + "posting": "Постинг", + "active_or_owner": "Активный или Владельца", + "this_password_is_bound_to_your_account_owner_key": "Этот пароль привязан к главному ключу аккаунта и не может быть использован для входа на этот сайт.", + "however_you_can_use_it_to": "Тем не менее его можно использовать чтобы ", + "update_your_password": "обновить Ваш пароль", + "to_obtain_a_more_secure_set_of_keys": "для получения более безопасного набора ключей.", + "this_password_is_bound_to_your_account_active_key": "Этот пароль привязан к главному ключу аккаунта и не может быть использован для входа на этот сайт.", + "you_may_use_this_active_key_on_other_more": "Вы можете использовать этот активный ключ на других более безопасных страниц, таких как страницы: Кошелек или Биржа.", + "you_account_has_been_successfully_created": "Ваш аккаунт был успешно создан!", + "you_account_has_been_successfully_recovered": "Ваш аккаунт был успешно восстановлен!", + "password_update_succes": "Пароль для %(accountName)s был успешно обновлен", + "password_info": "Этот пароль или закрытый ключ был введен неправильно. Вероятно, есть ошибка почерка или ввода данных. Подсказка: пароль или закрытый ключ, сгенерированный Steemit, никогда не будет содержать символы 0 (ноль), O (прописная o), I (прописная i) и l (нижний регистр L).", + "enter_your_username": "Введи свое имя пользователя", + "password_or_wif": "Пароль или WIF", + "this_operation_requires_your_key_or_master_password": "Для осуществления данной операции нужен ваш %(authType)s ключ или Основной пароль.", + "keep_me_logged_in": "Оставить меня залогиненным", + "amazing_community": "удивительное сообщество", + "to_comment_and_reward_others": " комментировать и вознаграждать других.", + "sign_up_get_steem": "Sign up. Get STEEM", + "signup_button": "Зарегистрируйтесь", + "signup_button_emphasis": " сейчас!", + "returning_users": "Вернувшиеся пользователи: ", + "join_our": "Присоединяйтесь к нашему" + }, + "chainvalidation_js": { + "account_name_should": "Имя аккаунта должно быть ", + "not_be_empty": "не может быть пустым.", + "be_longer": "длиннее.", + "be_shorter": "короче.", + "each_account_segment_should": "Имя аккаунта должно начинаться с ", + "start_with_a_letter": "должно начинаться с буквы.", + "have_only_letters_digits_or_dashes": "должно должно состоять только из букв, цифр или дефисов.", + "have_only_one_dash_in_a_row": "иметь только одно тире в строке.", + "end_with_a_letter_or_digit": "заканчиваться буквой или цифрой.", + "verified_exchange_no_memo": "Для перевода на биржу Вы должны указать примечание." + }, + "settings_jsx": { + "invalid_url": "Недопустимый URL-адрес", + "name_is_too_long": "Имя слишком длинное", + "name_must_not_begin_with": "Имя не должно начинаться с @", + "about_is_too_long": "Информация о Вашем блоге слишком длинная", + "location_is_too_long": "Местоположение слишком длинное", + "website_url_is_too_long": "URL-адрес веб-сайта слишком длинный", + "public_profile_settings": "Настройки Публичного Профиля", + "private_post_display_settings": "Настройки отображения приватных постов", + "not_safe_for_work_nsfw_content": "Небезопасный для работы (NSFW) контент", + "always_hide": "Всегда скрывать", + "always_warn": "Всегда предупреждать", + "always_show": "Отображать всегда", + "muted_users": "Заблокированные пользователи", + "update": "Обновить", + "profile_image_url": "Добавьте url вашего изображения", + "cover_image_url": "URL-адрес изображения обложки", + "profile_name": "Отображаемое имя", + "profile_about": "О себе", + "profile_location": "Местоположение", + "profile_website": "Веб-сайт" + }, + "transfer_jsx": { + "amount_is_in_form": "Сумма должна быть в формате 99999.999", + "insufficient_funds": "Недостаточно средств", + "use_only_3_digits_of_precison": "Используйте только 3 цифры точности", + "send_to_account": "Отправить аккаунту", + "asset": "Актив", + "this_memo_is_private": "Это примечание является приватным", + "this_memo_is_public": "Это примечание является публичным", + "convert_to_VESTING_TOKEN": "Перевести в %(VESTING_TOKEN)s", + "balance_subject_to_3_day_withdraw_waiting_period": "Вывод баланса из сейфа на обычный счет, занимает 3 дня,", + "move_funds_to_another_account": "Отправить средства на другой %(APP_NAME)s аккаунт.", + "protect_funds_by_requiring_a_3_day_withdraw_waiting_period": "Защитите средства от вывода 3-х дневным периодом ожидания.", + "withdraw_funds_after_the_required_3_day_waiting_period": "Снять средства после необходимого 3 дневного периода ожидания.", + "from": "От", + "to": "Кому", + "asset_currently_collecting": "начисляемые годовые на %(asset)s: %(interest)s%%.", + "beware_of_spam_and_phishing_links": "Остерегайтесь спама и фишинговых ссылок в передачах. Не открывайте ссылки от пользователей, которым вы не доверяете. Не предоставляйте свои личные ключи третьим сторонам." + }, + "userwallet_jsx": { + "conversion_complete_tip": "Завершается", + "in_conversion": "%(amount)s на конвертации", + "transfer_to_savings": "Отправить в сейф", + "power_up": "Усилить силу голоса", + "power_down": "Уменьшить силу голоса", + "market": "Биржа", + "convert_to_LIQUID_TOKEN": "Перевести в %(LIQUID_TOKEN)s", + "withdraw_LIQUID_TOKEN": "Снять %(LIQUID_TOKEN)s", + "withdraw_DEBT_TOKENS": "Снять %(DEBT_TOKENS)s", + "tokens_worth_about_1_of_LIQUID_TICKER": "Стоимость токенов $1.00 в %(LIQUID_TICKER)s, начисляемые годовые %(sbdInterest)s%%.", + "savings": "Сберегательный счет", + "estimated_account_value": "Приблизительная стоимость аккаунта", + "next_power_down_is_scheduled_to_happen": "Следующее понижение силы голоса будет", + "transfers_are_temporary_disabled": "Переводы временно преостановлены.", + "history": "ИСТОРИЯ", + "redeem_rewards": "Получить вознаграждение", + "buy_steem_or_steem_power": "Купить STEEM или STEEM POWER" + }, + "powerdown_jsx": { + "power_down": "Уменьшить силу голоса (power down)", + "amount": "Количество", + "already_power_down": "Вы уже запустили понижение силы голоса на %(AMOUNT)s %(LIQUID_TICKER)s (%(WITHDRAWN)s %(LIQUID_TICKER)s уже выплачено). Если вы измените количество, отсчет времени сбросится и составит 13 недель от сегодняшего дня.", + "delegating": "Вы делегируете %(AMOUNT)s %(LIQUID_TICKER)s. Это количество заблокировано и не может быть выведено пока делегирование не будет отменено и не пройдет один полный цикл выплат вознаграждений.", + "per_week": "Это ~%(AMOUNT)s %(LIQUID_TICKER)s в неделю.", + "warning": "Оставлять меньше чем %(AMOUNT)s %(LIQUID_TICKER)s не рекомендуется, т.к. это может остановить все транзакции с использованием это аккаунта.", + "error": "Неполучается уменьшить силу голоса (ERROR: %(MESSAGE)s)" + }, + "checkloginowner_jsx": { + "your_password_permissions_were_reduced": "Your password permissions were reduced", + "if_you_did_not_make_this_change": "If you did not make this change please", + "ownership_changed_on": "Владелец аккаунта или пароль были изменены ", + "deadline_for_recovery_is": "Вы можете восстановить доступ до ", + "i_understand_dont_show_again": "Понятно, больше не показывать" + } +} diff --git a/src/app/redux/AppReducer.js b/src/app/redux/AppReducer.js new file mode 100644 index 0000000..cb3e3ed --- /dev/null +++ b/src/app/redux/AppReducer.js @@ -0,0 +1,78 @@ +import {Map, OrderedMap} from 'immutable'; +import tt from 'counterpart'; + +const defaultState = Map({ + requests: {}, + loading: false, + error: '', + location: {}, + notifications: null, + ignoredLoadingRequestCount: 0, + notificounters: Map({ + total: 0, + feed: 0, + reward: 0, + send: 0, + mention: 0, + follow: 0, + vote: 0, + reply: 0, + account_update: 0, + message: 0, + receive: 0 + }), + user_preferences: Map({ + locale: null, + nsfwPref: 'warn', + theme: 'light', + blogmode: false, + currency: 'USD' + }) +}); + +export default function reducer(state = defaultState, action) { + if (action.type === '@@router/LOCATION_CHANGE') { + return state.set('location', {pathname: action.payload.pathname}); + } + if (action.type === 'STEEM_API_ERROR') { + return state.set('error', action.error).set('loading', false); + } + let res = state; + if (action.type === 'FETCH_DATA_BEGIN') { + res = state.set('loading', true); + } + if (action.type === 'FETCH_DATA_END') { + res = state.set('loading', false); + } + if (action.type === 'ADD_NOTIFICATION') { + const n = { + action: tt('g.dismiss'), + dismissAfter: 10000, + ...action.payload + }; + res = res.update('notifications', s => { + return s ? s.set(n.key, n) : OrderedMap({[n.key]: n}); + }); + } + if (action.type === 'REMOVE_NOTIFICATION') { + res = res.update('notifications', s => s.delete(action.payload.key)); + } + if (action.type === 'UPDATE_NOTIFICOUNTERS' && action.payload) { + const nc = action.payload; + if (nc.follow > 0) { + nc.total -= nc.follow; + nc.follow = 0; + } + res = res.set('notificounters', Map(nc)); + } + if (action.type === 'SET_USER_PREFERENCES') { + res = res.set('user_preferences', Map(action.payload)); + } + if (action.type === 'TOGGLE_NIGHTMODE') { + res = res.setIn(['user_preferences', 'nightmode'], !res.getIn(['user_preferences', 'nightmode'])); + } + if (action.type === 'TOGGLE_BLOGMODE') { + res = res.setIn(['user_preferences', 'blogmode'], !res.getIn(['user_preferences', 'blogmode'])); + } + return res; +} diff --git a/src/app/redux/AuthSaga.js b/src/app/redux/AuthSaga.js new file mode 100644 index 0000000..990303d --- /dev/null +++ b/src/app/redux/AuthSaga.js @@ -0,0 +1,160 @@ +import {takeEvery} from 'redux-saga'; +import {call, put, select} from 'redux-saga/effects'; +import {Set, Map, fromJS, List} from 'immutable' +import user from 'app/redux/User' +import {getAccount} from 'app/redux/SagaShared' +import {PrivateKey} from 'steem/lib/auth/ecc'; +import {api} from 'steem'; + +// operations that require only posting authority +const postingOps = Set(`vote, comment, delete_comment, custom_json, claim_reward_balance`.trim().split(/,\s*/)) + +export const authWatches = [ + watchForAuth +] + +function* watchForAuth() { + yield* takeEvery('user/ACCOUNT_AUTH_LOOKUP', accountAuthLookup); +} + +export function* accountAuthLookup({payload: {account, private_keys, login_owner_pubkey}}) { + account = fromJS(account) + private_keys = fromJS(private_keys) + // console.log('accountAuthLookup', account.name) + const stateUser = yield select(state => state.user) + let keys + if (private_keys) + keys = private_keys + else + keys = stateUser.getIn(['current', 'private_keys']) + + if (!keys || !keys.has('posting_private')) return + const toPub = k => k ? k.toPublicKey().toString() : '-' + const posting = keys.get('posting_private') + const active = keys.get('active_private') + const memo = keys.get('memo_private') + const auth = { + posting: posting ? yield authorityLookup( + {pubkeys: Set([toPub(posting)]), authority: account.get('posting'), authType: 'posting'}) : 'none', + active: active ? yield authorityLookup( + {pubkeys: Set([toPub(active)]), authority: account.get('active'), authType: 'active'}) : 'none', + owner: 'none', + memo: account.get('memo_key') === toPub(memo) ? 'full' : 'none' + } + const accountName = account.get('name') + const pub_keys_used = {posting: toPub(posting), active: toPub(active), owner: login_owner_pubkey}; + yield put(user.actions.setAuthority({accountName, auth, pub_keys_used})) +} + +/** + @arg {object} data + @arg {object} data.authority Immutable Map blockchain authority + @arg {object} data.pubkeys Immutable Set public key strings + @return {string} full, partial, none +*/ +function* authorityLookup({pubkeys, authority, authType}) { + return yield call(authStr, {pubkeys, authority, authType}) +} + +function* authStr({pubkeys, authority, authType, recurse = 1}) { + const t = yield call(threshold, {pubkeys, authority, authType, recurse}) + const r = authority.get('weight_threshold') + return t >= r ? 'full' : t > 0 ? 'partial' : 'none' +} + +export function* threshold({pubkeys, authority, authType, recurse = 1}) { + if (!pubkeys.size) return 0 + let t = pubkeyThreshold({pubkeys, authority}) + const account_auths = authority.get('account_auths') + const aaNames = account_auths.map(v => v.get(0), List()) + if (aaNames.size) { + const aaAccounts = yield api.getAccountsAsync(aaNames) + const aaThreshes = account_auths.map(v => v.get(1), List()) + for (let i = 0; i < aaAccounts.size; i++) { + const aaAccount = aaAccounts.get(i) + t += pubkeyThreshold({authority: aaAccount.get(authType), pubkeys}) + if (recurse <= 2) { + const auth = yield call(authStr, + {authority: aaAccount, pubkeys, recurse: ++recurse}) + if (auth === 'full') { + const aaThresh = aaThreshes.get(i) + t += aaThresh + } + } + } + } + return t +} + +function pubkeyThreshold({pubkeys, authority}) { + let available = 0 + const key_auths = authority.get('key_auths') + key_auths.forEach(k => { + if (pubkeys.has(k.get(0))) { + available += k.get(1) + } + }) + return available +} + +export function* findSigningKey({opType, username, password}) { + let authTypes + if (postingOps.has(opType)) + authTypes = 'posting, active' + else + authTypes = 'active, owner' + authTypes = authTypes.split(', ') + + const currentUser = yield select(state => state.user.get('current')) + const currentUsername = currentUser && currentUser.get('username') + + username = username || currentUsername + if (!username) return null + + const private_keys = currentUsername === username ? currentUser.get('private_keys') : Map() + + const account = yield call(getAccount, username); + if (!account) throw new Error('Account not found') + + for (const authType of authTypes) { + let private_key + if (password) { + try { + private_key = PrivateKey.fromWif(password) + } catch (e) { + private_key = PrivateKey.fromSeed(username + authType + password) + } + } else { + if(private_keys) + private_key = private_keys.get(authType + '_private') + } + if (private_key) { + const pubkey = private_key.toPublicKey().toString() + const pubkeys = Set([pubkey]) + const authority = account.get(authType) + const auth = yield call(authorityLookup, {pubkeys, authority, authType}) + if (auth === 'full') return private_key + } + } + return null +} + +// function isPostingOnlyKey(pubkey, account) { +// // TODO Support account auths +// // yield put(g.actions.authLookup({account, pubkeys: pubkey}) +// // authorityLookup({pubkeys, authority: Map(account.posting), authType: 'posting'}) +// for (const p of account.posting.key_auths) { +// if (pubkey === p[0]) { +// if (account.active.account_auths.length || account.owner.account_auths.length) { +// console.log('UserSaga, skipping save password, account_auths are not yet supported.') +// return false +// } +// for (const a of account.active.key_auths) +// if (pubkey === a[0]) return false +// for (const a of account.owner.key_auths) +// if (pubkey === a[0]) return false +// return true +// } +// } +// return false +// } diff --git a/src/app/redux/DemoState.js b/src/app/redux/DemoState.js new file mode 100644 index 0000000..86aaac5 --- /dev/null +++ b/src/app/redux/DemoState.js @@ -0,0 +1,220 @@ +module.exports = { + user: { + current: { + name: 'alice', + pending: { + trxid: { + trx: {}, + broadcast: new Date(), + server_response: null, + error: null, + confirmed: { + blocknum: 1234 + } + } + }, + wallet: { + locked: false, + server_salt: 'salt', + unencrypted_keys: { + $pubkey: 'privkey', + $pubkey1: 'privkey1', + $pubkey2: 'privkey2', + $pubkey3: 'privkey3' + }, + encrypted_keys: { + $pubkey: 'encryptedprivkey' + } + } + }, + users: { + alice: { + open_orders: [], + convert_requests: [], + history: { + trade: [], + transfer: [], + vote: [], + post: [], + reward: [] + }, + posts: { + recent: ['slug', 'slug1'], + expiring: ['slug', 'slug'], + best: ['slug', 'slug'] + }, + proxy: null, + witness_votes: [] + } + } + }, + discussions: { + update_status: { /// used to track async state of fetching + trending: { + last_update: new Date(), + fetching: false, + timeout: new Date(), + fetch_cursor: null /// fetching from start, else author/slug + }, + recent: {}, + expiring: {}, + best: {}, + active: {}, + category: { + general: { /// the category name + trending: { /// ~trending within category + last_update: new Date(), + fetching: false, + timeout: new Date(), + fetch_cursor: null /// fetching from start, else author/slug + }, + recent: {}, + expiring: {}, + best: {} + } + } + }, + trending: [ + 'author/slug', + 'author3/slug' + ], + recent: [ + 'author3/slug', + 'author/slug' + ], + expiring: [ + 'author3/slug', + 'author/slug' + ], + best: [ + 'author2/slug', + 'author/slug' + ], + active: [ + 'author1/slug', + 'author/slug' + ], + category: { + '~trending': ['cat1', 'cat2'], /// used to track trending categories + '~best': ['bestcat1', 'bestcat2'], /// used to track all time best categories + '~active': [], + general: { + trending: [ + 'author/slug', + 'author3/slug' + ], + recent: [ + 'author2/slug', + 'author3/slug' + ], + expiring: [ + 'author1/slug', + 'author/slug' + ], + best: [ + 'author2/slug', + 'author3/slug' + ], + active: [ + 'author2/slug', + 'author3/slug' + ] + } + }, + 'author/slug': { + fetched: new Date(), /// the date at which this data was requested from the server + id: '2.9.0', + author: 'author', + permlink: 'slug', + parent_author: '', + parent_permlink: 'general', + title: 'title', + body: `Lorem ipsum dolor sit amet, molestiae adversarium nec cu, mei in stet illud. Eam homero option cu, no periculis erroribus concludaturque vis. + No sit dissentias persequeris. Sea voluptua indoctum instructior cu, usu an fierent concludaturque. Ad ferri voluptua perpetua pri, an mel + liberavisse consectetuer, epicuri postulant mea ne. Sanctus epicurei vituperatoribus pro cu.`, + json_metadata: '', + last_update: '2016-02-29T21:08:48', + created: '2016-02-29T21:08:48', + depth: 0, + children: 4, + children_rshares2: '0', + net_rshares: 0, + abs_rshares: 0, + cashout_time: '2016-02-29T22:08:48', + total_payout_value: '0.000 USD', + pending_payout_value: '0.000 CLOUT', + total_pending_payout_value: '0.000 CLOUT', + replies: [], /// there is data to be fetched if 'children' is not 0 + fetched_replies: new Date(), + fetching_replies: false + } + }, + market: { + current_feed: 1.00, + feed_history: [ + /// last week of feed data with 1 hr sampling of median feed + ], + order_history: [ + ['time', 'buy', 1000, 1.000], /// time, type, quantity, price + ['time', 'sell', 100, 0.99] + ], + available_candlesticks: [5, 15, 30, 120, 240, 1440], + available_zoom: [6, 24, 48, 96, 168/* 1 week*/, 540, 1000000/*all*/], /// hours + current_candlestick: 5, /// min + current_zoom: 24, /// hours + price_history: [ + { + time: '2016-02-29T22:08:00', + open: 1.000, + close: 1.000, + high: 1.000, + low: 1.000, + volume: 10 + }, + { + time: '2016-02-29T22:09:00', + open: 1.000, + close: 1.000, + high: 1.000, + low: 1.000, + volume: 10 + } + ] + }, + bids: [ /// sorted by price from highest to lowest + { + id: '...', + owner: 'alice', + price: 1.0, + quantity: 100, + cum_quantity: 100, + expiration: null + }, + { + id: '...', + owner: 'alice', + price: 0.9, + quantity: 100, + cum_quantity: 200, + expiration: null + } + ], + asks: [ /// sorted by price from lowest to highest + { + id: '...', + owner: 'alice', + price: 1.1, + bid_quantity: 100, + cum_quantity: 100, + expiration: null + }, + { + id: '...', + owner: 'alice', + price: 1.2, + bid_quantity: 100, + cum_quantity: 200, + expiration: null + } + ] +}; diff --git a/src/app/redux/EmptyState.js b/src/app/redux/EmptyState.js new file mode 100644 index 0000000..ad22649 --- /dev/null +++ b/src/app/redux/EmptyState.js @@ -0,0 +1,38 @@ +/* Stub content (or objects) that may be inserted into the UI before being accepted by the blockchain. */ + +//TODO! +import { LIQUID_TICKER, DEBT_TICKER } from 'app/client_config' +export const emptyContent = { + fetched: new Date(), /// the date at which this data was requested from the server + id: '2.8.0', + author: '', + permlink: '', + category: '', + i18n_category: '', + parent_author: '', + parent_permlink: '', + title: '', + body: '', + json_metadata: '{}', + last_update: new Date().toISOString(), + created: new Date().toISOString(), + depth: 0, + children: 0, + children_rshares2: '0', + net_rshares: 0, + abs_rshares: 0, + cashout_time: new Date().toISOString(), + total_vote_weight: '0', + total_payout_value: ['0.000', DEBT_TICKER].join(" "), + pending_payout_value: ['0.000', LIQUID_TICKER].join(" "), + total_pending_payout_value: ['0.000', LIQUID_TICKER].join(" "), + active_votes: [], + replies: [], + stats: { + authorRepLog10: 86, + gray: false, + hasPendingPayout: false, + allowDelete: true, + hide: false, + }, +} diff --git a/src/app/redux/FetchDataSaga.js b/src/app/redux/FetchDataSaga.js new file mode 100644 index 0000000..c6a9000 --- /dev/null +++ b/src/app/redux/FetchDataSaga.js @@ -0,0 +1,273 @@ +import {takeLatest, takeEvery} from 'redux-saga'; +import {call, put, select, fork} from 'redux-saga/effects'; +import {loadFollows, fetchFollowCount} from 'app/redux/FollowSaga'; +import {getContent} from 'app/redux/SagaShared'; +import GlobalReducer from './GlobalReducer'; +import constants from './constants'; +import {fromJS, Map} from 'immutable' +import {api} from 'steem'; + +export const fetchDataWatches = [watchLocationChange, watchDataRequests, watchFetchJsonRequests, watchFetchState, watchGetContent]; + +export function* watchDataRequests() { + yield* takeLatest('REQUEST_DATA', fetchData); +} + +export function* watchGetContent() { + yield* takeEvery('GET_CONTENT', getContentCaller); +} + +export function* getContentCaller(action) { + yield getContent(action.payload); +} + +let is_initial_state = true; +export function* fetchState(location_change_action) { + const {pathname} = location_change_action.payload; + const m = pathname.match(/^\/@([a-z0-9\.-]+)/) + if(m && m.length === 2) { + const username = m[1] + yield fork(fetchFollowCount, username) + yield fork(loadFollows, "getFollowersAsync", username, 'blog') + yield fork(loadFollows, "getFollowingAsync", username, 'blog') + } + + // `ignore_fetch` case should only trigger on initial page load. No need to call + // fetchState immediately after loading fresh state from the server. Details: #593 + const server_location = yield select(state => state.offchain.get('server_location')); + const ignore_fetch = (pathname === server_location && is_initial_state) + is_initial_state = false; + if(ignore_fetch) return; + + let url = `${pathname}`; + if (url === '/') url = 'trending'; + // Replace /curation-rewards and /author-rewards with /transfers for UserProfile + // to resolve data correctly + if (url.indexOf("/curation-rewards") !== -1) url = url.replace("/curation-rewards", "/transfers"); + if (url.indexOf("/author-rewards") !== -1) url = url.replace("/author-rewards", "/transfers"); + + yield put({type: 'FETCH_DATA_BEGIN'}); + try { + const state = yield call([api, api.getStateAsync], url) + yield put(GlobalReducer.actions.receiveState(state)); + } catch (error) { + console.error('~~ Saga fetchState error ~~>', url, error); + yield put({type: 'global/STEEM_API_ERROR', error: error.message}); + } + yield put({type: 'FETCH_DATA_END'}); +} + +export function* watchLocationChange() { + yield* takeLatest('@@router/LOCATION_CHANGE', fetchState); +} + +export function* watchFetchState() { + yield* takeLatest('FETCH_STATE', fetchState); +} + +export function* fetchData(action) { + const {order, author, permlink, accountname} = action.payload; + let {category} = action.payload; + if( !category ) category = ""; + category = category.toLowerCase(); + + yield put({type: 'global/FETCHING_DATA', payload: {order, category}}); + let call_name, args; + if (order === 'trending') { + call_name = 'getDiscussionsByTrendingAsync'; + args = [ + { tag: category, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else if (order === 'trending30') { + call_name = 'getDiscussionsByTrending30Async'; + args = [ + { tag: category, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else if (order === 'promoted') { + call_name = 'getDiscussionsByPromotedAsync'; + args = [ + { tag: category, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else if( order === 'active' ) { + call_name = 'getDiscussionsByActiveAsync'; + args = [ + { tag: category, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else if( order === 'cashout' ) { + call_name = 'getDiscussionsByCashoutAsync'; + args = [ + { tag: category, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else if( order === 'payout' ) { + call_name = 'getPostDiscussionsByPayout'; + args = [ + { tag: category, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else if( order === 'payout_comments' ) { + call_name = 'getCommentDiscussionsByPayout'; + args = [ + { tag: category, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else if( order === 'updated' ) { + call_name = 'getDiscussionsByActiveAsync'; + args = [ + { tag: category, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else if( order === 'created' || order === 'recent' ) { + call_name = 'getDiscussionsByCreatedAsync'; + args = [ + { tag: category, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else if( order === 'by_replies' ) { + call_name = 'getRepliesByLastUpdateAsync'; + args = [author, permlink, constants.FETCH_DATA_BATCH_SIZE]; + } else if( order === 'responses' ) { + call_name = 'getDiscussionsByChildrenAsync'; + args = [ + { tag: category, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else if( order === 'votes' ) { + call_name = 'getDiscussionsByVotesAsync'; + args = [ + { tag: category, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else if( order === 'hot' ) { + call_name = 'getDiscussionsByHotAsync'; + args = [ + { tag: category, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else if( order === 'by_feed' ) { // https://github.com/steemit/steem/issues/249 + call_name = 'getDiscussionsByFeedAsync'; + args = [ + { tag: accountname, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else if( order === 'by_author' ) { + call_name = 'getDiscussionsByBlogAsync'; + args = [ + { tag: accountname, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else if( order === 'by_comments' ) { + call_name = 'getDiscussionsByCommentsAsync'; + args = [ + { limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } else { + call_name = 'getDiscussionsByActiveAsync'; + args = [{ + tag: category, + limit: constants.FETCH_DATA_BATCH_SIZE, + start_author: author, + start_permlink: permlink}]; + } + yield put({type: 'FETCH_DATA_BEGIN'}); + try { + const data = yield call([api, api[call_name]], ...args); + yield put(GlobalReducer.actions.receiveData({data, order, category, author, permlink, accountname})); + } catch (error) { + console.error('~~ Saga fetchData error ~~>', call_name, args, error); + yield put({type: 'global/STEEM_API_ERROR', error: error.message}); + } + yield put({type: 'FETCH_DATA_END'}); +} + +// export function* watchMetaRequests() { +// yield* takeLatest('global/REQUEST_META', fetchMeta); +// } +export function* fetchMeta({payload: {id, link}}) { + try { + const metaArray = yield call(() => new Promise((resolve, reject) => { + function reqListener() { + const resp = JSON.parse(this.responseText) + if (resp.error) { + reject(resp.error) + return + } + resolve(resp) + } + const oReq = new XMLHttpRequest() + oReq.addEventListener('load', reqListener) + oReq.open('GET', '/http_metadata/' + link) + oReq.send() + })) + const {title, metaTags} = metaArray + let meta = {title} + for (let i = 0; i < metaTags.length; i++) { + const [name, content] = metaTags[i] + meta[name] = content + } + // http://postimg.org/image/kbefrpbe9/ + meta = { + link, + card: meta['twitter:card'], + site: meta['twitter:site'], // @username tribbute + title: meta['twitter:title'], + description: meta['twitter:description'], + image: meta['twitter:image'], + alt: meta['twitter:alt'], + } + if(!meta.image) { + meta.image = meta['twitter:image:src'] + } + yield put(GlobalReducer.actions.receiveMeta({id, meta})) + } catch(error) { + yield put(GlobalReducer.actions.receiveMeta({id, meta: {error}})) + } +} + +export function* watchFetchJsonRequests() { + yield* takeEvery('global/FETCH_JSON', fetchJson); +} + +/** + @arg {string} id unique key for result global['fetchJson_' + id] + @arg {string} url + @arg {object} body (for JSON.stringify) +*/ +function* fetchJson({payload: {id, url, body, successCallback, skipLoading = false}}) { + try { + const payload = { + method: body ? 'POST' : 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: body ? JSON.stringify(body) : undefined + } + let result = yield skipLoading ? fetch(url, payload) : call(fetch, url, payload) + result = yield result.json() + if(successCallback) result = successCallback(result) + yield put(GlobalReducer.actions.fetchJsonResult({id, result})) + } catch(error) { + console.error('fetchJson', error) + yield put(GlobalReducer.actions.fetchJsonResult({id, error})) + } +} diff --git a/src/app/redux/FetchDataSaga.test.js b/src/app/redux/FetchDataSaga.test.js new file mode 100644 index 0000000..5c82c75 --- /dev/null +++ b/src/app/redux/FetchDataSaga.test.js @@ -0,0 +1,53 @@ +/*global describe, it, before, beforeEach, after, afterEach */ + +import chai, {expect} from 'chai'; +import dirtyChai from 'dirty-chai'; +import sinon from 'sinon'; +import {call, put} from 'redux-saga/effects'; +import {fetchState} from './FetchDataSaga'; +chai.use(dirtyChai); + +const action = { + payload: { + pathname: '/recent', + search: '', + action: 'PUSH' + } +}; + +describe('sagas', () => { + // not maintained + // it('should fetch state and submit RECEIVE_STATE action', () => { + // const url = '/recent'; + // const db_api = Apis.instance().db_api; + // const expectedCallResult = call([db_api, db_api.exec], 'get_state', [url]); + // const generator = fetchState(action); + // const callResult = generator.next().value; + // expect( + // callResult.CALL.args + // ).to.be.eql(expectedCallResult.CALL.args); + // + // const expectedPutResult = put({type: 'global/RECEIVE_STATE', payload: undefined}); + // const putResult = generator.next().value; + // expect( + // putResult + // ).to.be.eql(expectedPutResult); + // }); + // + // it('should try to fetch state and submit STEEM_API_ERROR if failed', () => { + // const generator = fetchState(action); + // expect(generator.next().value).to.be.ok(); + // const result = generator.throw({message: 'test error'}).value; + // const expectedPutResult = put({type: 'global/STEEM_API_ERROR', error: 'test error'}); + // expect( + // result + // ).to.be.eql(expectedPutResult); + // }); + // + // it('should try to fetch state and submit STEEM_API_ERROR if failed', () => { + // const pop_action = {payload: {...action.payload, action: 'POP'}}; + // const generator = fetchState(pop_action); + // const result = generator.next().value; + // expect(result).to.be.null(); + // }); +}); diff --git a/src/app/redux/FollowSaga.js b/src/app/redux/FollowSaga.js new file mode 100644 index 0000000..2219cc2 --- /dev/null +++ b/src/app/redux/FollowSaga.js @@ -0,0 +1,99 @@ +import {fromJS, Map, Set} from 'immutable' +import {call, put, select} from 'redux-saga/effects'; +import {api} from 'steem'; + +/** + This loadFollows both 'blog' and 'ignore' +*/ + +//fetch for follow/following count +export function* fetchFollowCount(account) { + const counts = yield call([api, api.getFollowCountAsync], account) + yield put({ + type: 'global/UPDATE', + payload: { + key: ['follow_count', account], + updater: m => m.mergeDeep({ + follower_count: counts.follower_count, + following_count: counts.following_count}) + }}) +} + +// Test limit with 2 (not 1, infinate looping) +export function* loadFollows(method, account, type, force = false) { + if(yield select(state => state.global.getIn(['follow', method, account, type + '_loading']))) { + // console.log('Already loading', method, account, type) + return + } + + if(!force) { + const hasResult = yield select(state => state.global.hasIn(['follow', method, account, type + '_result'])) + if(hasResult) { + // console.log('Already loaded', method, account, type) + return + } + } + + yield put({ + type: 'global/UPDATE', + payload: { + key: ['follow', method, account], + notSet: Map(), + updater: m => m.set(type + '_loading', true), + }}) + + yield loadFollowsLoop(method, account, type) +} + +function* loadFollowsLoop(method, account, type, start = '', limit = 100) { + if(method === "getFollowersAsync") limit = 1000; + const res = fromJS(yield api[method](account, start, type, limit)); + // console.log('res.toJS()', res.toJS()) + + let cnt = 0 + let lastAccountName = null + + yield put({type: 'global/UPDATE', payload: { + key: ['follow_inprogress', method, account], + notSet: Map(), + updater: (m) => { + m = m.asMutable() + res.forEach((value) => { + cnt += 1; + + const whatList = value.get('what') + const accountNameKey = method === "getFollowingAsync" ? "following" : "follower"; + const accountName = lastAccountName = value.get(accountNameKey) + whatList.forEach((what) => { + //currently this is always true: what === type + m.update(what, Set(), s => s.add(accountName)) + }) + }) + return m.asImmutable() + } + }}) + + if(cnt === limit) { + // This is paging each block of up to limit results + yield call(loadFollowsLoop, method, account, type, lastAccountName) + } else { + // This condition happens only once at the very end of the list. + // Every account has a different followers and following list for: blog, ignore + yield put({type: 'global/UPDATE', payload: { + key: [], + updater: (m) => { + m = m.asMutable() + + const result = m.getIn(['follow_inprogress', method, account, type], Set()) + m.deleteIn(['follow_inprogress', method, account, type]) + m.updateIn(['follow', method, account], Map(), mm => mm.merge({ + // Count may be set separately without loading the full xxx_result set + [type + '_count']: result.size, + [type + '_result']: result.sort().reverse(), + [type + '_loading']: false, + })) + return m.asImmutable() + } + }}) + } +} diff --git a/src/app/redux/GlobalReducer.js b/src/app/redux/GlobalReducer.js new file mode 100644 index 0000000..9d91a4c --- /dev/null +++ b/src/app/redux/GlobalReducer.js @@ -0,0 +1,302 @@ +import {Map, Set, List, fromJS, Iterable} from 'immutable'; +import createModule from 'redux-modules'; +import {emptyContent} from 'app/redux/EmptyState'; +import constants from './constants'; +import {contentStats} from 'app/utils/StateFunctions' + +const emptyContentMap = Map(emptyContent) + +export default createModule({ + name: 'global', + initialState: Map({status: {}}), + transformations: [ + { + action: 'SET_COLLAPSED', + reducer: (state, action) => { + return state.withMutations(map => { + map.updateIn(['content', action.payload.post], value => { + value.merge(Map({collapsed: action.payload.collapsed})); + }); + }); + } + }, + { + action: 'RECEIVE_STATE', + reducer: (state, action) => { + let payload = fromJS(action.payload) + if(payload.has('content')) { + const content = payload.get('content').withMutations(c => { + c.forEach((cc, key) => { + cc = emptyContentMap.mergeDeep(cc) + const stats = fromJS(contentStats(cc)) + c.setIn([key, 'stats'], stats) + }) + }) + payload = payload.set('content', content) + } + // console.log('state.mergeDeep(action.payload).toJS(), action.payload', state.mergeDeep(action.payload).toJS(), action.payload) + return state.mergeDeep(payload); + } + }, + { + action: 'RECEIVE_ACCOUNT', + reducer: (state, {payload: {account}}) => { + account = fromJS(account, (key, value) => { + if (key === 'witness_votes') return value.toSet() + const isIndexed = Iterable.isIndexed(value); + return isIndexed ? value.toList() : value.toOrderedMap(); + }) + // Merging accounts: A get_state will provide a very full account but a get_accounts will provide a smaller version + return state.updateIn(['accounts', account.get('name')], Map(), a => a.mergeDeep(account)) + } + }, + { + action: 'RECEIVE_COMMENT', + reducer: (state, {payload: op}) => { + const {author, permlink, parent_author = '', parent_permlink = '', title = '', body} = op + const key = author + '/' + permlink + + let updatedState = state.updateIn(['content', key], Map(emptyContent), r => r.merge({ + author, permlink, parent_author, parent_permlink, + title: title.toString('utf-8'), + body: body.toString('utf-8'), + })) + // console.log('updatedState content', updatedState.getIn(['content', key]).toJS()) + + if (parent_author !== '' && parent_permlink !== '') { + const parent_key = parent_author + '/' + parent_permlink + updatedState = updatedState.updateIn(['content', parent_key, 'replies'], List(), r => r.insert(0, key)) + const children = updatedState.getIn(['content', parent_key, 'replies'], List()).size; + updatedState = updatedState.updateIn(['content', parent_key, 'children'], 0, r => children) + // console.log('updatedState parent', updatedState.toJS()) + } + return updatedState + } + }, + { + action: 'RECEIVE_CONTENT', + reducer: (state, {payload: {content}}) => { + // console.log('GlobalReducer -- RECEIVE_CONTENT content', content) + content = fromJS(content) + const key = content.get('author') + '/' + content.get('permlink') + return state.updateIn(['content', key], Map(), c => { + c = emptyContentMap.mergeDeep(c) + c = c.delete('active_votes') + c = c.mergeDeep(content) + c = c.set('stats', fromJS(contentStats(c))) + return c + }) + } + }, + { // works... + action: 'LINK_REPLY', + reducer: (state, {payload: op}) => { + const {author, permlink, parent_author = '', parent_permlink = ''} = op + if (parent_author === '' || parent_permlink === '') return state + const key = author + '/' + permlink + const parent_key = parent_author + '/' + parent_permlink + // Add key if not exist + let updatedState = state.updateIn(['content', parent_key, 'replies'], List(), + l => (l.findIndex(i => i === key) === -1 ? l.push(key) : l)) + const children = updatedState.getIn(['content', parent_key, 'replies'], List()).size; + updatedState = updatedState.updateIn(['content', parent_key, 'children'], 0, r => children) + return updatedState; + } + }, + { // works... + action: 'UPDATE_ACCOUNT_WITNESS_VOTE', + reducer: (state, {payload: {account, witness, approve}}) => + state.updateIn(['accounts', account, 'witness_votes'], Set(), + votes => (approve ? Set(votes).add(witness) : Set(votes).remove(witness))) + }, + { // works... + action: 'UPDATE_ACCOUNT_WITNESS_PROXY', + reducer: (state, {payload: {account, proxy}}) => + state.setIn(['accounts', account, 'proxy'], proxy) + }, + { + action: 'DELETE_CONTENT', + reducer: (state, {payload: {author, permlink}}) => { + const key = author + '/' + permlink + const content = state.getIn(['content', key]) + const parent_author = content.get('parent_author') || '' + const parent_permlink = content.get('parent_permlink') || '' + let updatedState = state.deleteIn(['content', key]) + if (parent_author !== '' && parent_permlink !== '') { + const parent_key = parent_author + '/' + parent_permlink + updatedState = updatedState.updateIn(['content', parent_key, 'replies'], + List(), r => r.filter(i => i !== key)) + } + return updatedState + } + }, + { + action: 'VOTED', + reducer: (state, {payload: {username, author, permlink, weight}}) => { + const key = ['content', author + '/' + permlink, 'active_votes'] + let active_votes = state.getIn(key, List()) + const idx = active_votes.findIndex(v => v.get('voter') === username) + // steemd flips weight into percent + if(idx === -1) + active_votes = active_votes.push(Map({voter: username, percent: weight})); + else { + active_votes = active_votes.set(idx, Map({voter: username, percent: weight})); + } + state.setIn(key, active_votes); + return state; + } + }, + { + action: 'FETCHING_DATA', + reducer: (state, {payload: {order, category}}) => { + const new_state = state.updateIn(['status', category || '', order], () => { + return {fetching: true}; + }); + return new_state; + } + }, + { + action: 'RECEIVE_DATA', + reducer: (state, {payload: {data, order, category, author, accountname, /*permlink*/}}) => { + // console.log('-- RECEIVE_DATA reducer -->', order, category, author, permlink, data); + // console.log('-- RECEIVE_DATA state -->', state.toJS()); + let new_state; + if (order === 'by_author' || order === 'by_feed' || order === 'by_comments' || order === 'by_replies') { + // category is either "blog", "feed", "comments", or "recent_replies" (respectively) -- and all posts are keyed under current profile + const key = ['accounts', accountname, category] + new_state = state.updateIn(key, List(), list => { + return list.withMutations(posts => { + data.forEach(value => { + const key2 = `${value.author}/${value.permlink}` + if (!posts.includes(key2)) posts.push(key2); + }); + }); + }); + } else { + new_state = state.updateIn(['discussion_idx', category || '', order], list => { + return list.withMutations(posts => { + data.forEach(value => { + const entry = `${value.author}/${value.permlink}`; + if (!posts.includes(entry)) posts.push(entry); + }); + }); + }); + } + new_state = new_state.updateIn(['content'], content => { + return content.withMutations(map => { + data.forEach(value => { + // console.log('GlobalReducer -- RECEIVE_DATA', value) + const key = `${value.author}/${value.permlink}`; + value = fromJS(value) + value = value.set('stats', fromJS(contentStats(value))) + map.set(key, value); + }); + }); + }); + new_state = new_state.updateIn(['status', category || '', order], () => { + if (data.length < constants.FETCH_DATA_BATCH_SIZE) { + return {fetching: false, last_fetch: new Date()}; + } + return {fetching: false}; + }); + // console.log('-- new_state -->', new_state.toJS()); + return new_state; + } + }, + { + action: 'RECEIVE_RECENT_POSTS', + reducer: (state, {payload: {data}}) => { + // console.log('-- RECEIVE_RECENT_POSTS state -->', state.toJS()); + // console.log('-- RECEIVE_RECENT_POSTS reducer -->', data); + let new_state = state.updateIn(['discussion_idx', '', 'created'], list => { + if (!list) list = List(); + return list.withMutations(posts => { + data.forEach(value => { + const entry = `${value.author}/${value.permlink}`; + if (!posts.includes(entry)) posts.unshift(entry); + }); + }); + }); + new_state = new_state.updateIn(['content'], content => { + return content.withMutations(map => { + data.forEach(value => { + const key = `${value.author}/${value.permlink}`; + if (!map.has(key)) { + value = fromJS(value) + value = value.set('stats', fromJS(contentStats(value))) + map.set(key, value); + } + }); + }); + }); + // console.log('-- new_state -->', new_state.toJS()); + return new_state; + } + }, + { + action: 'REQUEST_META', // browser console debug + reducer: (state, {payload: {id, link}}) => + state.setIn(['metaLinkData', id], Map({link})) + }, + { + action: 'RECEIVE_META', // browser console debug + reducer: (state, {payload: {id, meta}}) => + state.updateIn(['metaLinkData', id], data => data.merge(meta)) + }, + { + action: 'SET', + reducer: (state, {payload: {key, value}}) => { + key = Array.isArray(key) ? key : [key] + return state.setIn(key, fromJS(value)) + } + }, + { + action: 'REMOVE', + reducer: (state, {payload: {key}}) => { + key = Array.isArray(key) ? key : [key] + return state.removeIn(key) + } + }, + { + action: 'UPDATE', + reducer: (state, {payload: {key, notSet = Map(), updater}}) => + // key = Array.isArray(key) ? key : [key] // TODO enable and test + state.updateIn(key, notSet, updater) + }, + { + action: 'SET_META_DATA', // browser console debug + reducer: (state, {payload: {id, meta}}) => + state.setIn(['metaLinkData', id], fromJS(meta)) + }, + { + action: 'CLEAR_META', // browser console debug + reducer: (state, {payload: {id}}) => + state.deleteIn(['metaLinkData', id]) + }, + { + action: 'CLEAR_META_ELEMENT', // browser console debug + reducer: (state, {payload: {formId, element}}) => + state.updateIn(['metaLinkData', formId], data => data.remove(element)) + }, + { + action: 'FETCH_JSON', + reducer: state => state // saga + }, + { + action: 'FETCH_JSON_RESULT', + reducer: (state, {payload: {id, result, error}}) => + state.set(id, fromJS({result, error})) + }, + { + action: 'SHOW_DIALOG', + reducer: (state, {payload: {name, params = {}}}) => + state.update('active_dialogs', Map(), d => d.set(name, fromJS({params}))) + }, + { + action: 'HIDE_DIALOG', + reducer: (state, {payload: {name}}) => + state.update('active_dialogs', d => d.delete(name)) + }, + + ] +}); diff --git a/src/app/redux/MarketReducer.js b/src/app/redux/MarketReducer.js new file mode 100644 index 0000000..719acd8 --- /dev/null +++ b/src/app/redux/MarketReducer.js @@ -0,0 +1,40 @@ +import {Map} from 'immutable'; +import createModule from 'redux-modules'; + + +export default createModule({ + name: 'market', + initialState: Map({status: {}}), + transformations: [ + { + action: 'RECEIVE_ORDERBOOK', + reducer: (state, action) => { + return state.set('orderbook', action.payload); + } + }, + { + action: 'RECEIVE_TICKER', + reducer: (state, action) => { + return state.set('ticker', action.payload); + } + }, + { + action: 'RECEIVE_OPEN_ORDERS', + reducer: (state, action) => { + return state.set('open_orders', action.payload); + } + }, + { + action: 'RECEIVE_TRADE_HISTORY', + reducer: (state, action) => { + return state.set('history', action.payload); + } + }, + { + action: 'APPEND_TRADE_HISTORY', + reducer: (state, action) => { + return state.set('history', [...action.payload, ...state.get('history')]); + } + } + ] +}); diff --git a/src/app/redux/MarketSaga.js b/src/app/redux/MarketSaga.js new file mode 100644 index 0000000..b51b5ce --- /dev/null +++ b/src/app/redux/MarketSaga.js @@ -0,0 +1,87 @@ +import {takeLatest} from 'redux-saga'; +import {call, put} from 'redux-saga/effects'; +import MarketReducer from './MarketReducer'; +import {getAccount} from './SagaShared'; +import {api} from 'steem'; + +export const marketWatches = [watchLocationChange, watchUserLogin, watchMarketUpdate]; + +const wait = ms => ( + new Promise(resolve => { + setTimeout(() => resolve(), ms) + })) + +let polling = false +let active_user = null +let last_trade = null + +export function* fetchMarket(location_change_action) { + const {pathname} = location_change_action.payload; + if (pathname && pathname != "/market") { + polling = false + return + } + + if(polling == true) return + polling = true + + while(polling) { + + try { + const state = yield call([api, api.getOrderBookAsync], 500); + yield put(MarketReducer.actions.receiveOrderbook(state)); + + let trades; + if(last_trade == null ) { + trades = yield call([api, api.getRecentTradesAsync], 25); + yield put(MarketReducer.actions.receiveTradeHistory(trades)); + } else { + let start = last_trade.toISOString().slice(0, -5) + trades = yield call([api, api.getTradeHistoryAsync], start, "1969-12-31T23:59:59", 1000); + trades = trades.reverse() + yield put(MarketReducer.actions.appendTradeHistory(trades)); + } + if(trades.length > 0) { + last_trade = new Date((new Date(Date.parse(trades[0]['date']))).getTime() + 1000) + } + + const state3 = yield call([api, api.getTickerAsync]); + yield put(MarketReducer.actions.receiveTicker(state3)); + } catch (error) { + console.error('~~ Saga fetchMarket error ~~>', error); + yield put({type: 'global/STEEM_API_ERROR', error: error.message}); + } + + yield call(wait, 3000); + } +} + +export function* fetchOpenOrders(set_user_action) { + const {username} = set_user_action.payload + + try { + const state = yield call([api, api.getOpenOrdersAsync], username); + yield put(MarketReducer.actions.receiveOpenOrders(state)); + yield call(getAccount, username, true); + } catch (error) { + console.error('~~ Saga fetchOpenOrders error ~~>', error); + yield put({type: 'global/STEEM_API_ERROR', error: error.message}); + } +} + +export function* reloadMarket(reload_action) { + yield fetchMarket(reload_action); + yield fetchOpenOrders(reload_action); +} + +export function* watchUserLogin() { + yield* takeLatest('user/SET_USER', fetchOpenOrders); +} + +export function* watchLocationChange() { + yield* takeLatest('@@router/LOCATION_CHANGE', fetchMarket); +} + +export function* watchMarketUpdate() { + yield* takeLatest('market/UPDATE_MARKET', reloadMarket); +} diff --git a/src/app/redux/Offchain.jsx b/src/app/redux/Offchain.jsx new file mode 100644 index 0000000..08fa018 --- /dev/null +++ b/src/app/redux/Offchain.jsx @@ -0,0 +1,13 @@ +import Immutable from 'immutable'; +import {PropTypes} from 'react'; + +const defaultState = Immutable.fromJS({user: {}}); + +export default function reducer(state = defaultState, action) { + if (action.type === 'user/SAVE_LOGIN_CONFIRM') { + if (!action.payload) { + state = state.set('account', null); + } + } + return state; +} diff --git a/src/app/redux/PollDataSaga.js b/src/app/redux/PollDataSaga.js new file mode 100644 index 0000000..5f676d1 --- /dev/null +++ b/src/app/redux/PollDataSaga.js @@ -0,0 +1,45 @@ +import { call, put, select } from 'redux-saga/effects'; +import GlobalReducer from './GlobalReducer'; +import {getNotifications, webPushRegister} from 'app/utils/ServerApiClient'; +import registerServiceWorker from 'app/utils/RegisterServiceWorker'; +import {api} from 'steem'; + +const wait = ms => ( + new Promise(resolve => { + setTimeout(() => resolve(), ms) + }) +) + +let webpush_params = null; + +function* pollData() { + while(true) { + yield call(wait, 20000); + + const username = yield select(state => state.user.getIn(['current', 'username'])); + if (username) { + if (webpush_params === null) { + try { + webpush_params = yield call(registerServiceWorker); + if (webpush_params) yield call(webPushRegister, username, webpush_params); + } catch (error) { + console.error(error); + webpush_params = {error}; + } + } + const nc = yield call(getNotifications, username, webpush_params); + yield put({type: 'UPDATE_NOTIFICOUNTERS', payload: nc}); + } + + try { + const data = yield call([api, api.getDynamicGlobalPropertiesAsync]); + // console.log('-- pollData.pollData -->', data); + // const data = yield call([api, api.getDiscussionsByCreatedAsync], {limit: 10}); + // yield put(GlobalReducer.actions.receiveRecentPosts({data})); + } catch (error) { + console.error('~~ pollData saga error ~~>', error); + } + } +} + +export default pollData; diff --git a/src/app/redux/RootReducer.js b/src/app/redux/RootReducer.js new file mode 100644 index 0000000..5743bdd --- /dev/null +++ b/src/app/redux/RootReducer.js @@ -0,0 +1,67 @@ +import {Map, fromJS} from 'immutable'; +import {combineReducers} from 'redux'; +import {routerReducer} from 'react-router-redux'; +import appReducer from './AppReducer'; +//import discussionReducer from './DiscussionReducer'; +import globalReducerModule from './GlobalReducer'; +import marketReducerModule from './MarketReducer'; +import user from './User'; +// import auth from './AuthSaga'; +import transaction from './Transaction'; +import offchain from './Offchain'; +import {reducer as formReducer} from 'redux-form'; // @deprecated, instead use: app/utils/ReactForm.js +import {contentStats} from 'app/utils/StateFunctions' + +function initReducer(reducer, type) { + return (state, action) => { + if(!state) return reducer(state, action); + + // @@redux/INIT server and client init + if (action.type === '@@redux/INIT' || action.type === '@@INIT') { + if(!(state instanceof Map)) { + state = fromJS(state); + } + if(type === 'global') { + const content = state.get('content').withMutations(c => { + c.forEach((cc, key) => { + if(!c.getIn([key, 'stats'])) { + // This may have already been set in UniversalRender; if so, then + // active_votes were cleared from server response. In this case it + // is important to not try to recalculate the stats. (#1040) + c.setIn([key, 'stats'], fromJS(contentStats(cc))) + } + }) + }) + state = state.set('content', content) + } + return state + } + + if (action.type === '@@router/LOCATION_CHANGE' && type === 'global') { + state = state.set('pathname', action.payload.pathname) + // console.log(action.type, type, action, state.toJS()) + } + + return reducer(state, action); + } +} + +export default combineReducers({ + global: initReducer(globalReducerModule.reducer, 'global'), + market: initReducer(marketReducerModule.reducer), + offchain: initReducer(offchain), + user: initReducer(user.reducer), + // auth: initReducer(auth.reducer), + transaction: initReducer(transaction.reducer), + //discussion: initReducer(discussionReducer), + discussion: initReducer((state = {}) => state), + routing: initReducer(routerReducer), + app: initReducer(appReducer), + form: formReducer, +}); + +/* +let now + benchStart: initReducer((state = {}, action) => {console.log('>> action.type', action.type); now = Date.now(); return state}), + benchEnd: initReducer((state = {}, action) => {console.log('<< action.type', action.type, (Date.now() - now), 'ms'); return state}), +*/ diff --git a/src/app/redux/SagaShared.js b/src/app/redux/SagaShared.js new file mode 100644 index 0000000..7d99ce9 --- /dev/null +++ b/src/app/redux/SagaShared.js @@ -0,0 +1,89 @@ +import {fromJS} from 'immutable' +import {call, put, select} from 'redux-saga/effects'; +import g from 'app/redux/GlobalReducer' +import {takeEvery, takeLatest} from 'redux-saga'; +import tt from 'counterpart'; +import {api} from 'steem'; +import {setUserPreferences} from 'app/utils/ServerApiClient'; + +const wait = ms => ( + new Promise(resolve => { + setTimeout(() => resolve(), ms) + }) +); + +export const sharedWatches = [watchGetState, watchTransactionErrors, watchUserSettingsUpdates] + +export function* getAccount(username, force = false) { + let account = yield select(state => state.global.get('accounts').get(username)) + if (force || !account) { + [account] = yield call([api, api.getAccountsAsync], [username]) + if(account) { + account = fromJS(account) + yield put(g.actions.receiveAccount({account})) + } + } + return account +} + +export function* watchGetState() { + yield* takeEvery('global/GET_STATE', getState); +} +/** Manual refreshes. The router is in FetchDataSaga. */ +export function* getState({payload: {url}}) { + try { + const state = yield call([api, api.getStateAsync], url) + yield put(g.actions.receiveState(state)); + } catch (error) { + console.error('~~ Saga getState error ~~>', url, error); + yield put({type: 'global/STEEM_API_ERROR', error: error.message}); + } +} + +export function* watchTransactionErrors() { + yield* takeEvery('transaction/ERROR', showTransactionErrorNotification); +} + +function* showTransactionErrorNotification() { + const errors = yield select(state => state.transaction.get('errors')); + for (const [key, message] of errors) { + yield put({type: 'ADD_NOTIFICATION', payload: {key, message}}); + yield put({type: 'transaction/DELETE_ERROR', payload: {key}}); + } +} + +export function* getContent({author, permlink, resolve, reject}) { + let content; + while(!content) { + content = yield call([api, api.getContentAsync], author, permlink); + if(content["author"] == "") { // retry if content not found. #1870 + content = null; + yield call(wait, 3000); + } + } + + yield put(g.actions.receiveContent({content})) + if (resolve && content) { + resolve(content); + } else if (reject && !content) { + reject(); + } +} + +/** + * Save this user's preferences, either directly from the submitted payload or from whatever's saved in the store currently. + * + * @param {Object?} params.payload + */ +function* saveUserPreferences({payload}) { + if (payload) { + yield setUserPreferences(payload); + } + + const prefs = yield select(state => state.app.get('user_preferences')); + yield setUserPreferences(prefs.toJS()); +} + +function* watchUserSettingsUpdates() { + yield* takeLatest(['SET_USER_PREFERENCES', 'TOGGLE_NIGHTMODE', 'TOGGLE_BLOGMODE'], saveUserPreferences); +} diff --git a/src/app/redux/Transaction.js b/src/app/redux/Transaction.js new file mode 100644 index 0000000..435e1a0 --- /dev/null +++ b/src/app/redux/Transaction.js @@ -0,0 +1,140 @@ +import {fromJS, Map} from 'immutable'; +import createModule from 'redux-modules'; + +export default createModule({ + name: 'transaction', + initialState: fromJS({ + operations: [], + status: { key: '', error: false, busy: false, }, + errors: null + }), + transformations: [ + { + action: 'CONFIRM_OPERATION', + reducer: (state, {payload}) => { + const operation = fromJS(payload.operation) + const confirm = payload.confirm + const warning = payload.warning + const checkbox = payload.checkbox + return state.merge({ + show_confirm_modal: true, + confirmBroadcastOperation: operation, + confirmErrorCallback: payload.errorCallback, + confirm, + warning, + checkbox + }) + } + }, + { action: 'HIDE_CONFIRM', reducer: state => + state.merge({show_confirm_modal: false, confirmBroadcastOperation: undefined, confirm: undefined}) + }, + { + // An error will end up in QUEUE + action: 'BROADCAST_OPERATION', + reducer: (state) => {//, {payload: {type, operation, keys}} + // See TransactionSaga.js + return state + }, + }, + { + // An error will end up in QUEUE + action: 'UPDATE_AUTHORITIES', + reducer: (state) => state, + }, + { + // An error will end up in QUEUE + action: 'UPDATE_META', + reducer: (state) => state, + }, + { + action: 'ERROR', + reducer: (state, {payload: {operations, error, errorCallback}}) => { + let errorStr = error.toString(); + let errorKey = 'Transaction broadcast error.'; + for (const [type/*, operation*/] of operations) { + switch (type) { + case 'vote': + if (/uniqueness constraint/.test(errorStr)) { + errorKey = 'You already voted for this post'; + console.error('You already voted for this post.') + } + break; + case 'comment': + if (/You may only post once per minute/.test(errorStr)) { + errorKey = 'You may only post once per minute.' + } else if (errorStr === 'Testing, fake error') + errorKey = 'Testing, fake error'; + break; + case 'transfer': + if (/get_balance/.test(errorStr)) { + errorKey = 'Insufficient balance.' + } + break; + case 'withdraw_vesting': + if(/Account registered by another account requires 10x account creation fee worth of Steem Power/.test(errorStr)) + errorKey = 'Account requires 10x the account creation fee in Steem Power (approximately 300 SP) before it can power down.' + break; + default: + break; + } + if (state.hasIn(['TransactionError', type + '_listener'])) { + state = state.setIn(['TransactionError', type], fromJS({key: errorKey, exception: errorStr})) + } else { + if (error.message) { + // Depends on FC_ASSERT formatting + // https://github.com/steemit/steemit.com/issues/222 + const err_lines = error.message.split('\n'); + if (err_lines.length > 2) { + errorKey = err_lines[1]; + const txt = errorKey.split(': '); + if(txt.length && txt[txt.length - 1].trim() !== '') { + errorKey = errorStr = txt[txt.length - 1] + } else + errorStr = `Transaction failed: ${err_lines[1]}`; + } + } + if (errorStr.length > 200) errorStr = errorStr.substring(0, 200); + // Catch for unknown key better error handling + if (/unknown key: /.test(errorKey)) { + errorKey = "Steem account doesn't exist."; + errorStr = "Transaction failed: Steem account doesn't exist."; + } + // Catch for invalid active authority + if (/Missing Active Authority /.test(errorKey)) { + errorKey = "Not your valid active key."; + errorStr = "Transaction failed: Not your valid active key."; + } + state = state.update('errors', errors => { + return errors ? errors.set(errorKey, errorStr) : Map({[errorKey]: errorStr}); + }); + } + } + if (errorCallback) try { errorCallback(errorKey) } catch (error2) { console.error(error2) } + return state + }, + }, + { + action: 'DELETE_ERROR', + reducer: (state, {payload: {key}}) => { + return state.deleteIn(['errors', key]); + } + }, + { + action: 'SET', + reducer: (state, {payload: {key, value}}) => { + key = Array.isArray(key) ? key : [key] + return state.setIn(key, fromJS(value)) + } + }, + { + action: 'REMOVE', + reducer: (state, {payload: {key}}) => { + key = Array.isArray(key) ? key : [key] + return state.removeIn(key) + } + }, + ] +}); + +// const log = v => {console.log('l', v); return v} diff --git a/src/app/redux/TransactionSaga.js b/src/app/redux/TransactionSaga.js new file mode 100644 index 0000000..5d5246b --- /dev/null +++ b/src/app/redux/TransactionSaga.js @@ -0,0 +1,733 @@ +import {takeEvery} from 'redux-saga'; +import {call, put, select} from 'redux-saga/effects'; +import {fromJS, Set, Map} from 'immutable' +import {getAccount, getContent} from 'app/redux/SagaShared' +import {findSigningKey} from 'app/redux/AuthSaga' +import g from 'app/redux/GlobalReducer' +import user from 'app/redux/User' +import tr from 'app/redux/Transaction' +import tt from 'counterpart' +import getSlug from 'speakingurl' +import {DEBT_TICKER} from 'app/client_config' +import {serverApiRecordEvent} from 'app/utils/ServerApiClient' +import {PrivateKey, PublicKey} from 'steem/lib/auth/ecc'; +import {api, broadcast, auth, memo} from 'steem'; + +export const transactionWatches = [ + watchForBroadcast, + watchForUpdateAuthorities, + watchForUpdateMeta, + watchForRecoverAccount, +] + +export function* watchForBroadcast() { + yield* takeEvery('transaction/BROADCAST_OPERATION', broadcastOperation); +} +export function* watchForUpdateAuthorities() { + yield* takeEvery('transaction/UPDATE_AUTHORITIES', updateAuthorities); +} +export function* watchForUpdateMeta() { + yield* takeEvery('transaction/UPDATE_META', updateMeta); +} +export function* watchForRecoverAccount() { + yield* takeEvery('transaction/RECOVER_ACCOUNT', recoverAccount); +} + +const hook = { + preBroadcast_comment, + preBroadcast_transfer, + preBroadcast_vote, + preBroadcast_account_witness_vote, + preBroadcast_custom_json, + error_vote, + error_custom_json, + // error_account_update, + error_account_witness_vote, + accepted_comment, + accepted_delete_comment, + accepted_vote, + accepted_account_update, + accepted_withdraw_vesting, +} + +function* preBroadcast_transfer({operation}) { + let memoStr = operation.memo + if(memoStr) { + memoStr = toStringUtf8(memoStr) + memoStr = memoStr.trim() + if(/^#/.test(memoStr)) { + const memo_private = yield select( + state => state.user.getIn(['current', 'private_keys', 'memo_private']) + ) + if(!memo_private) throw new Error('Unable to encrypt memo, missing memo private key') + const account = yield call(getAccount, operation.to) + if(!account) throw new Error(`Unknown to account ${operation.to}`) + const memo_key = account.get('memo_key') + memoStr = memo.encode(memo_private, memo_key, memoStr) + operation.memo = memoStr + } + } + return operation +} +const toStringUtf8 = o => (o ? Buffer.isBuffer(o) ? o.toString('utf-8') : o.toString() : o) + +function* preBroadcast_vote({operation, username}) { + if (!operation.voter) operation.voter = username + const {voter, author, permlink, weight} = operation + // give immediate feedback + yield put(g.actions.set({key: `transaction_vote_active_${author}_${permlink}`, value: true})) + yield put(g.actions.voted({username: voter, author, permlink, weight})) + return operation +} +function* preBroadcast_account_witness_vote({operation, username}) { + if (!operation.account) operation.account = username + const {account, witness, approve} = operation + yield put(g.actions.updateAccountWitnessVote({account, witness, approve})) + return operation +} + +function* preBroadcast_custom_json({operation}) { + const json = JSON.parse(operation.json) + if(operation.id === 'follow') { + try { + if(json[0] === 'follow') { + const {follower, following, what: [action]} = json[1] + yield put(g.actions.update({ + key: ['follow', 'getFollowingAsync', follower], + notSet: Map(), + updater: m => { + //m = m.asMutable() + if(action == null) { + m = m.update('blog_result', Set(), r => r.delete(following)) + m = m.update('ignore_result', Set(), r => r.delete(following)) + } else if(action === 'blog') { + m = m.update('blog_result', Set(), r => r.add(following)) + m = m.update('ignore_result', Set(), r => r.delete(following)) + } else if(action === 'ignore') { + m = m.update('ignore_result', Set(), r => r.add(following)) + m = m.update('blog_result', Set(), r => r.delete(following)) + } + m = m.set('blog_count', m.get('blog_result', Set()).size) + m = m.set('ignore_count', m.get('ignore_result', Set()).size) + return m//.asImmutable() + } + })) + } + } catch(e) { + console.error('TransactionSaga unrecognized follow custom_json format', operation.json); + } + } + return operation +} + +function* error_account_witness_vote({operation: {account, witness, approve}}) { + yield put(g.actions.updateAccountWitnessVote({account, witness, approve: !approve})) +} + +/** Keys, username, and password are not needed for the initial call. This will check the login and may trigger an action to prompt for the password / key. */ +function* broadcastOperation({payload: + {type, operation, confirm, warning, keys, username, password, successCallback, errorCallback, allowPostUnsafe} +}) { + const operationParam = {type, operation, keys, username, password, successCallback, errorCallback, allowPostUnsafe} + + const conf = typeof confirm === 'function' ? confirm() : confirm + if(conf) { + yield put(tr.actions.confirmOperation({confirm, warning, operation: operationParam, errorCallback})) + return + } + const payload = {operations: [[type, operation]], keys, username, successCallback, errorCallback} + if (!allowPostUnsafe && hasPrivateKeys(payload)) { + const confirm = tt('g.post_key_warning.confirm') + const warning = tt('g.post_key_warning.warning') + const checkbox = tt('g.post_key_warning.checkbox') + operationParam.allowPostUnsafe = true + yield put(tr.actions.confirmOperation({confirm, warning, checkbox, operation: operationParam, errorCallback})) + return + } + try { + if (!keys || keys.length === 0) { + payload.keys = [] + // user may already be logged in, or just enterend a signing passowrd or wif + const signingKey = yield call(findSigningKey, {opType: type, username, password}) + if (signingKey) + payload.keys.push(signingKey) + else { + if (!password) { + yield put(user.actions.showLogin({operation: {type, operation, username, successCallback, errorCallback, saveLogin: true}})) + return + } + } + } + yield call(broadcastPayload, {payload}) + let eventType = type.replace(/^([a-z])/, g => g.toUpperCase()).replace(/_([a-z])/g, g => g[1].toUpperCase()); + if (eventType === 'Comment' && !operation.parent_author) eventType = 'Post'; + const page = eventType === 'Vote' ? `@${operation.author}/${operation.permlink}` : ''; + serverApiRecordEvent(eventType, page); + } catch(error) { + console.error('TransactionSage', error); + if(errorCallback) errorCallback(error.toString()) + } +} + +function hasPrivateKeys(payload) { + const blob = JSON.stringify(payload.operations) + let m, re = /P?(5[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{50})/g + while (true) { + m = re.exec(blob) + if (m) { + try { + PrivateKey.fromWif(m[1]) // performs the base58check + return true + } catch (e) {} + } else { + break + } + } + return false +} + +function* broadcastPayload({payload: {operations, keys, username, successCallback, errorCallback}}) { + // console.log('broadcastPayload') + if ($STM_Config.read_only_mode) return; + for (const [type] of operations) // see also transaction/ERROR + yield put(tr.actions.remove({key: ['TransactionError', type]})) + + { + const newOps = [] + for (const [type, operation] of operations) { + if (hook['preBroadcast_' + type]) { + const op = yield call(hook['preBroadcast_' + type], {operation, username}) + if(Array.isArray(op)) + for(const o of op) + newOps.push(o) + else + newOps.push([type, op]) + } else { + newOps.push([type, operation]) + } + } + operations = newOps + } + + // status: broadcasting + const broadcastedEvent = () => { + for (const [type, operation] of operations) { + if (hook['broadcasted_' + type]) { + try { + hook['broadcasted_' + type]({operation}) + } catch (error) { + console.error(error) + } + } + } + } + + try { + yield new Promise((resolve, reject) => { + // Bump transaction (for live UI testing).. Put 0 in now (no effect), + // to enable browser's autocomplete and help prevent typos. + const env = process.env; + const bump = env.BROWSER ? parseInt(localStorage.getItem('bump') || 0) : 0; + if (env.BROWSER && bump === 1) { // for testing + console.log('TransactionSaga bump(no broadcast) and reject', JSON.stringify(operations, null, 2)) + setTimeout(() => {reject(new Error('Testing, fake error'))}, 2000) + } else if (env.BROWSER && bump === 2) { // also for testing + console.log('TransactionSaga bump(no broadcast) and resolve', JSON.stringify(operations, null, 2)) + setTimeout(() => {resolve(); broadcastedEvent()}, 2000) + } else { + broadcast.send({ extensions: [], operations }, keys, (err) => { + if(err) { + console.error(err); + reject(err) + } else { + broadcastedEvent() + resolve() + } + }) + } + }) + // status: accepted + for (const [type, operation] of operations) { + if (hook['accepted_' + type]) { + try { + yield call(hook['accepted_' + type], {operation}) + } catch (error) { + console.error(error) + } + } + const config = operation.__config + if (config && config.successMessage) { + yield put({type: 'ADD_NOTIFICATION', payload: { + key: "trx_" + Date.now(), + message: config.successMessage, + dismissAfter: 5000 + }}) + } + } + if (successCallback) try { successCallback() } catch (error) { console.error(error) } + } catch (error) { + console.error('TransactionSaga\tbroadcastPayload', error); + // status: error + yield put(tr.actions.error({operations, error, errorCallback})); + for (const [type, operation] of operations) { + if (hook['error_' + type]) { + try { + yield call(hook['error_' + type], {operation}) + } catch (error2) { + console.error(error2) + } + } + } + } +} + +function* accepted_comment({operation}) { + const {author, permlink} = operation + // update again with new $$ amount from the steemd node + yield call(getContent, {author, permlink}) + // receiveComment did the linking already (but that is commented out) + yield put(g.actions.linkReply(operation)) + // mark the time (can only post 1 per min) + // yield put(user.actions.acceptedComment()) +} +function* accepted_delete_comment({operation}) { + yield put(g.actions.deleteContent(operation)) +} + +function* accepted_vote({operation: {author, permlink, weight}}) { + console.log('Vote accepted, weight', weight, 'on', author + '/' + permlink, 'weight'); + // update again with new $$ amount from the steemd node + yield put(g.actions.remove({key: `transaction_vote_active_${author}_${permlink}`})) + yield call(getContent, {author, permlink}) +} + +function* accepted_withdraw_vesting({operation}) { + let [account] = yield call([api, api.getAccountsAsync], [operation.account]) + account = fromJS(account) + yield put(g.actions.receiveAccount({account})) +} + +function* accepted_account_update({operation}) { + let [account] = yield call([api, api.getAccountsAsync], [operation.account]) + account = fromJS(account) + yield put(g.actions.receiveAccount({account})) + + // bug, fork, etc.. the folowing would be mis-leading + // const {account} = operation + // const {owner, active, posting, memo_key, json_metadata} = operation + // { + // const update = { accounts: { [account]: {memo_key, json_metadata} } } + // if (posting) update.accounts[account].posting = posting + // if (active) update.accounts[account].active = active + // if (owner) update.accounts[account].owner = owner + // yield put(g.actions.receiveState(update)) + // } +} + +// TODO remove soon, this was replaced by the UserKeys edit running usernamePasswordLogin (on dialog close) +// function* error_account_update({operation}) { +// const {account} = operation +// const stateUser = yield select(state => state.user) +// const username = stateUser.getIn(['current', 'username']) +// if (username === account) { +// const pending_private_key = stateUser.getIn(['current', 'pending_private_key']) +// if (pending_private_key) { +// // remove pending key +// const update = { pending_private_key: undefined } +// yield put(user.actions.setUser(update)) +// } +// } +// } + +import base58 from 'bs58' +import secureRandom from 'secure-random' + +// function* preBroadcast_account_witness_vote({operation, username}) { +// } +function* preBroadcast_comment({operation, username}) { + if (!operation.author) operation.author = username + let permlink = operation.permlink + const {author, __config: {originalBody, autoVote, comment_options}} = operation + const {parent_author = '', parent_permlink = operation.category } = operation + const {title} = operation + let {body} = operation + + body = body.trim() + + // TODO Slightly smaller blockchain comments: if body === json_metadata.steem.link && Object.keys(steem).length > 1 remove steem.link ..This requires an adjust of get_state and the API refresh of the comment to put the steem.link back if Object.keys(steem).length >= 1 + + let body2 + if (originalBody) { + const patch = createPatch(originalBody, body) + // Putting body into buffer will expand Unicode characters into their true length + if (patch && patch.length < new Buffer(body, 'utf-8').length) + body2 = patch + } + if (!body2) body2 = body + if (!permlink) permlink = yield createPermlink(title, author, parent_author, parent_permlink) + + const md = operation.json_metadata + const json_metadata = typeof md === 'string' ? md : JSON.stringify(md) + const op = { + ...operation, + permlink: permlink.toLowerCase(), + parent_author, parent_permlink, json_metadata, + title: new Buffer((operation.title || '').trim(), 'utf-8'), + body: new Buffer(body2, 'utf-8'), + } + + const comment_op = [ + ['comment', op], + ] + + // comment_options must come directly after comment + if(comment_options) { + const { + max_accepted_payout = ["1000000.000", DEBT_TICKER].join(" "), + percent_steem_dollars = 10000, // 10000 === 100% + allow_votes = true, + allow_curation_rewards = true, + } = comment_options + comment_op.push( + ['comment_options', { + author, + permlink, + max_accepted_payout, + percent_steem_dollars, + allow_votes, + allow_curation_rewards, + extensions: comment_options.extensions ? comment_options.extensions : [] + }] + ) + } + + if(autoVote) { + const vote = {voter: op.author, author: op.author, permlink: op.permlink, weight: 10000} + comment_op.push(['vote', vote]) + } + + return comment_op +} + +function* createPermlink(title, author, parent_author, parent_permlink) { + let permlink + if (title && title.trim() !== '') { + let s = slug(title) + if(s === '') { + s = base58.encode(secureRandom.randomBuffer(4)) + } + // ensure the permlink(slug) is unique + const slugState = yield call([api, api.getContentAsync], author, s) + let prefix + if (slugState.body !== '') { + // make sure slug is unique + prefix = base58.encode(secureRandom.randomBuffer(4)) + '-' + } else { + prefix = '' + } + permlink = prefix + s + } else { + // comments: re-parentauthor-parentpermlink-time + const timeStr = new Date().toISOString().replace(/[^a-zA-Z0-9]+/g, '') + parent_permlink = parent_permlink.replace(/(-\d{8}t\d{9}z)/g, '') + permlink = `re-${parent_author}-${parent_permlink}-${timeStr}` + } + if(permlink.length > 255) { + // STEEMIT_MAX_PERMLINK_LENGTH + permlink = permlink.substring(permlink.length - 255, permlink.length) + } + // only letters numbers and dashes shall survive + permlink = permlink.toLowerCase().replace(/[^a-z0-9-]+/g, '') + return permlink +} + +import diff_match_patch from 'diff-match-patch' +const dmp = new diff_match_patch() + +function createPatch(text1, text2) { + if (!text1 && text1 === '') return undefined + const patches = dmp.patch_make(text1, text2) + const patch = dmp.patch_toText(patches) + return patch +} + +function* error_custom_json({operation: {id, required_posting_auths}}) { + if(id === 'follow') { + const follower = required_posting_auths[0] + yield put(g.actions.update({ + key: ['follow', 'getFollowingAsync', follower, 'loading'], + updater: () => null + })) + } +} +function* error_vote({operation: {author, permlink}}) { + yield put(g.actions.remove({key: `transaction_vote_active_${author}_${permlink}`})); + yield call(getContent, {author, permlink}); // unvote +} + +// function* error_comment({operation}) { +// // Rollback an immediate UI update (the transaction had an error) +// yield put(g.actions.deleteContent(operation)) +// const {author, permlink, parent_author, parent_permlink} = operation +// yield call(getContent, {author, permlink}) +// if (parent_author !== '' && parent_permlink !== '') { +// yield call(getContent, {parent_author, parent_permlink}) +// } +// } + +function slug(text) { + return getSlug(text.replace(/[<>]/g, ''), {truncate: 128}) + //const shorten = txt => { + // let t = '' + // let words = 0 + // const txt2 = txt.replace(/ +/g, ' ') // only 1 space in a row + // for (let i = 0; i < txt2.length; i++) { + // const ch = txt2.charAt(i) + // if (ch === '.' && i !== 0) { + // if(i === txt2.length - 1) + // break + // // If it looks like the end of a sentence + // if(txt2.charAt(i + 1) === ' ') + // break + // } + // if (ch === ' ' || ch === '\n') { + // words++ + // if (words === 15) break + // if (i > 100) break + // } + // t += ch + // } + // return t + //} + //return shorten(text) + // .replace(/\n/g, ' ') + // .replace(/[ \.]/g, '-') + // .replace(/[^a-zA-Z0-9-_]+/g, '') // only letters and numbers _ and - + // .replace(/--/g, '-') + // .toLowerCase() +} + +const pwPubkey = (name, pw, role) => auth.wifToPublic(auth.toWif(name, pw.trim(), role)) + +function* recoverAccount({payload: {account_to_recover, old_password, new_password, onError, onSuccess}}) { + const [account] = yield call([api, api.getAccountsAsync], [account_to_recover]) + if(!account) { + onError('Unknown account ' + account) + return + } + if(auth.isWif(new_password)) { + onError('Your new password should not be a WIF') + return + } + if(auth.isPubkey(new_password)) { + onError('Your new password should not be a Public Key') + return + } + + const oldOwnerPrivate = auth.isWif(old_password) ? old_password : + auth.toWif(account_to_recover, old_password, 'owner') + + const oldOwner = auth.wifToPublic(oldOwnerPrivate) + + const newOwnerPrivate = auth.toWif(account_to_recover, new_password.trim(), 'owner') + const newOwner = auth.wifToPublic(newOwnerPrivate) + const newActive = pwPubkey(account_to_recover, new_password.trim(), 'active') + const newPosting = pwPubkey(account_to_recover, new_password.trim(), 'posting') + const newMemo = pwPubkey(account_to_recover, new_password.trim(), 'memo') + + const new_owner_authority = {weight_threshold: 1, account_auths: [], + key_auths: [[newOwner, 1]]} + + const recent_owner_authority = {weight_threshold: 1, account_auths: [], + key_auths: [[oldOwner, 1]]} + + try { + yield broadcast.sendAsync({extensions: [], operations: [ + ['recover_account', { + account_to_recover, + new_owner_authority, + recent_owner_authority, + }] + ]}, [oldOwnerPrivate, newOwnerPrivate]) + + // change password + // change password probably requires a separate transaction (single trx has not been tested) + const {json_metadata} = account + yield broadcast.sendAsync({extensions: [], operations: [ + ['account_update', { + account: account.name, + active: {weight_threshold: 1, account_auths: [], key_auths: [[newActive, 1]]}, + posting: {weight_threshold: 1, account_auths: [], key_auths: [[newPosting, 1]]}, + memo_key: newMemo, + json_metadata, + }] + ]}, [newOwnerPrivate]) + if(onSuccess) onSuccess() + } catch(error) { + console.error('Recover account', error) + if(onError) onError(error) + } +} + +/** auths must start with most powerful key: owner for example */ +// const twofaAccount = 'steem' +function* updateAuthorities({payload: {accountName, signingKey, auths, twofa, onSuccess, onError}}) { + // Be sure this account is up-to-date (other required fields are sent in the update) + const [account] = yield call([api, api.getAccountsAsync], [accountName]) + if (!account) { + onError('Account not found') + return + } + // const signingPubkey = signingKey ? signingKey.toPublicKey() : null + const ops2 = {} + let oldPrivate + const addAuth = (authType, oldAuth, newAuth) => { + let oldAuthPubkey, oldPrivateAuth + try { + oldPrivateAuth = PrivateKey.fromWif(oldAuth) + oldAuthPubkey = oldPrivateAuth.toPublic().toString() + } catch(e) { + try { + oldAuthPubkey = PublicKey.fromStringOrThrow(oldAuth).toString() + } catch(e2) { + // + } + } + if(!oldAuthPubkey) { + if(!oldAuth) { + onError('Missing old key, not sure what to replace') + console.error('Missing old key, not sure what to replace') + return false + } + oldPrivateAuth = PrivateKey.fromSeed(accountName + authType + oldAuth) + oldAuthPubkey = oldPrivateAuth.toPublicKey().toString() + } + if(authType === 'owner' && !oldPrivate) + oldPrivate = oldPrivateAuth + else if(authType === 'active' && !oldPrivate) + oldPrivate = oldPrivateAuth + else if(authType === 'posting' && !oldPrivate) + oldPrivate = oldPrivateAuth + + let newPrivate, newAuthPubkey + try { + newPrivate = PrivateKey.fromWif(newAuth) + newAuthPubkey = newPrivate.toPublicKey().toString() + } catch (e) { + newPrivate = PrivateKey.fromSeed(accountName + authType + newAuth) + newAuthPubkey = newPrivate.toPublicKey().toString() + } + // if (oldAuthPubkey === newAuthPubkey) { + // onError('This is the same key') + // return false + // } + let authority + if (authType === 'memo') { + account.memo_key = newAuthPubkey + } else { + authority = fromJS(account[authType]).toJS() + authority.key_auths = [] + authority.key_auths.push([newAuthPubkey, authority.weight_threshold]) + // const key_auths = authority.key_auths + // let found + // for (let i = 0; i < key_auths.length; i++) { + // if (key_auths[i][0] === oldAuthPubkey) { + // key_auths[i][0] = newAuthPubkey + // found = true + // break + // } + // } + // if (!found) { + // key_auths.push([newAuthPubkey, authority.weight_threshold]) + // console.log(`Could not find an ${authType} key to update, adding instead`) + // } + + // Add twofaAccount with full authority + // if(twofa && authType === 'owner') { + // let account_auths = fromJS(authority.account_auths) + // if(!account_auths.find(v => v.get(0) === twofaAccount)) { + // account_auths = account_auths.push(fromJS([twofaAccount, authority.weight_threshold])) + // } + // authority.account_auths = account_auths.toJS() + // } + } + ops2[authType] = authority ? authority : account[authType] + return true + } + for(const auth of auths) + if(!addAuth(auth.authType, auth.oldAuth, auth.newAuth)) + return + + let key = oldPrivate + if(!key) { + try { + key = PrivateKey.fromWif(signingKey) + } catch(e2) { + // probably updating a memo .. see if we got an active or owner + const auth = (authType) => { + const priv = PrivateKey.fromSeed(accountName + authType + signingKey) + const pubkey = priv.toPublicKey().toString() + const authority = account[authType] + const key_auths = authority.key_auths + for (let i = 0; i < key_auths.length; i++) { + if (key_auths[i][0] === pubkey) { + return priv + } + } + return null + } + key = auth('active') + if(!key) key = auth('owner') + } + } + if (!key) { + onError(`Incorrect Password`) + throw new Error('Trying to update a memo without a signing key?') + } + const {memo_key, json_metadata} = account + const payload = { + type: 'account_update', operation: { + account: account.name, ...ops2, + memo_key, json_metadata, + }, keys: [key], + successCallback: onSuccess, + errorCallback: onError, + } + // console.log('sign key.toPublicKey().toString()', key.toPublicKey().toString()) + // console.log('payload', payload) + yield call(broadcastOperation, {payload}) +} + +/** auths must start with most powerful key: owner for example */ +// const twofaAccount = 'steem' +function* updateMeta(params) { + // console.log('params', params) + const {meta, account_name, signingKey, onSuccess, onError} = params.payload.operation + console.log('meta', meta) + console.log('account_name', account_name) + // Be sure this account is up-to-date (other required fields are sent in the update) + const [account] = yield call([api, api.getAccountsAsync], [account_name]) + if (!account) { + onError('Account not found') + return + } + if (!signingKey) { + onError(`Incorrect Password`) + throw new Error('Have to pass owner key in order to change meta') + } + + try { + console.log('account.name', account.name) + const operations = ['update_account_meta', { + account_name: account.name, + json_meta: JSON.stringify(meta), + }] + yield broadcast.sendAsync({extensions: [], operations}, [signingKey]) + if(onSuccess) onSuccess() + // console.log('sign key.toPublicKey().toString()', key.toPublicKey().toString()) + // console.log('payload', payload) + } catch(e) { + console.error('Update meta', e) + if(onError) onError(e) + } +} diff --git a/src/app/redux/User.js b/src/app/redux/User.js new file mode 100644 index 0000000..6e84ee1 --- /dev/null +++ b/src/app/redux/User.js @@ -0,0 +1,155 @@ +import {fromJS} from 'immutable'; +import createModule from 'redux-modules'; +import { DEFAULT_LANGUAGE } from 'app/client_config'; +import store from 'store'; + +const defaultState = fromJS({ + current: null, + show_login_modal: false, + show_transfer_modal: false, + show_powerdown_modal: false, + show_promote_post_modal: false, + show_signup_modal: false, + pub_keys_used: null, + locale: DEFAULT_LANGUAGE, +}); + +if (process.env.BROWSER) { + const locale = store.get('language'); + if (locale) defaultState.locale = locale; +} + +export default createModule({ + name: 'user', + initialState: defaultState, + transformations: [ + { + action: 'SHOW_LOGIN', + reducer: (state, {payload}) => { + // https://github.com/mboperator/redux-modules/issues/11 + if (typeof payload === 'function') payload = undefined; + let operation, loginDefault + if(payload) { + operation = fromJS(payload.operation) + loginDefault = fromJS(payload.loginDefault) + } + return state.merge({show_login_modal: true, loginBroadcastOperation: operation, loginDefault}) + } + }, + { + action: 'SHOW_TERMS', + reducer: (state, {payload}) => { + // https://github.com/mboperator/redux-modules/issues/11 + if (typeof payload === 'function') payload = undefined; + let operation, termsDefault; + if(payload) { + operation = fromJS(payload.operation); + termsDefault = fromJS(payload.termsDefault) + } + return state.merge({show_terms_modal: true, loginBroadcastOperation: operation, termsDefault}) + } + }, + { action: 'HIDE_LOGIN', reducer: state => + state.merge({show_login_modal: false, loginBroadcastOperation: undefined, loginDefault: undefined}) }, + { action: 'SAVE_LOGIN_CONFIRM', reducer: (state, {payload}) => state.set('saveLoginConfirm', payload) }, + { action: 'SAVE_LOGIN', reducer: (state) => state }, // Use only for low security keys (like posting only keys) + { action: 'REMOVE_HIGH_SECURITY_KEYS', reducer: (state) => { + if(!state.hasIn(['current', 'private_keys'])) return state + let empty = false + state = state.updateIn(['current', 'private_keys'], private_keys => { + if(!private_keys) return null + if(private_keys.has('active_private')) + console.log('removeHighSecurityKeys') + private_keys = private_keys.delete('active_private') + empty = private_keys.size === 0 + return private_keys + }) + if(empty) { + // User logged in with Active key then navigates away from the page + // LOGOUT + return defaultState.merge({logged_out: true}) + } + const username = state.getIn(['current', 'username']) + state = state.setIn(['authority', username, 'active'], 'none') + state = state.setIn(['authority', username, 'owner'], 'none') + return state + }}, + { action: 'CHANGE_LANGUAGE', reducer: (state, {payload}) => { + return state.set('locale', payload)} + }, + { action: 'SHOW_TRANSFER', reducer: state => state.set('show_transfer_modal', true) }, + { action: 'HIDE_TRANSFER', reducer: state => state.set('show_transfer_modal', false) }, + { action: 'SHOW_POWERDOWN', reducer: state => state.set('show_powerdown_modal', true) }, + { action: 'HIDE_POWERDOWN', reducer: state => state.set('show_powerdown_modal', false) }, + { action: 'SHOW_PROMOTE_POST', reducer: state => state.set('show_promote_post_modal', true) }, + { action: 'HIDE_PROMOTE_POST', reducer: state => state.set('show_promote_post_modal', false) }, + { action: 'SET_TRANSFER_DEFAULTS', reducer: (state, {payload}) => state.set('transfer_defaults', fromJS(payload)) }, + { action: 'CLEAR_TRANSFER_DEFAULTS', reducer: (state) => state.remove('transfer_defaults') }, + { action: 'SET_POWERDOWN_DEFAULTS', reducer: (state, {payload}) => state.set('powerdown_defaults', fromJS(payload)) }, + { action: 'CLEAR_POWERDOWN_DEFAULTS', reducer: (state) => state.remove('powerdown_defaults') }, + { + action: 'USERNAME_PASSWORD_LOGIN', + reducer: state => state, // saga + }, + { + action: 'SET_USER', + reducer: (state, {payload}) => { + // console.log('SET_USER') + if (payload.vesting_shares) payload.vesting_shares = parseFloat(payload.vesting_shares); + if (payload.delegated_vesting_shares) payload.delegated_vesting_shares = parseFloat(payload.delegated_vesting_shares); + if (payload.received_vesting_shares) payload.received_vesting_shares = parseFloat(payload.received_vesting_shares); + return state.mergeDeep({ current: payload, show_login_modal: false, loginBroadcastOperation: undefined, loginDefault: undefined, logged_out: undefined }) + } + }, + { + action: 'CLOSE_LOGIN', + reducer: (state) => state.merge({ login_error: undefined, show_login_modal: false, loginBroadcastOperation: undefined, loginDefault: undefined }) + }, + { + action: 'LOGIN_ERROR', + reducer: (state, {payload: {error}}) => state.merge({ login_error: error, logged_out: undefined }) + }, + { + action: 'LOGOUT', + reducer: () => { + return defaultState.merge({logged_out: true}) + } + }, + // { + // action: 'ACCEPTED_COMMENT', + // // User can only post 1 comment per minute + // reducer: (state) => state.merge({ current: {lastComment: Date.now()} }) + // }, + { action: 'SHOW_SIGN_UP', reducer: state => state.set('show_signup_modal', true) }, + { action: 'HIDE_SIGN_UP', reducer: state => state.set('show_signup_modal', false) }, + + { + action: 'KEYS_ERROR', + reducer: (state, {payload: {error}}) => state.merge({ keys_error: error }) + }, + // { action: 'UPDATE_PERMISSIONS', reducer: state => { + // return state // saga + // }}, + { // AuthSaga + action: 'ACCOUNT_AUTH_LOOKUP', + reducer: state => state + }, + { // AuthSaga + action: 'SET_AUTHORITY', + reducer: (state, {payload: {accountName, auth, pub_keys_used}}) => { + state = state.setIn(['authority', accountName], fromJS(auth)) + if(pub_keys_used) + state = state.set('pub_keys_used', pub_keys_used) + return state + }, + }, + { action: 'HIDE_CONNECTION_ERROR_MODAL', reducer: state => state.set('hide_connection_error_modal', true) }, + { + action: 'SET', + reducer: (state, {payload: {key, value}}) => { + key = Array.isArray(key) ? key : [key] + return state.setIn(key, fromJS(value)) + } + }, + ] +}); diff --git a/src/app/redux/UserActions.js b/src/app/redux/UserActions.js new file mode 100644 index 0000000..bfae60e --- /dev/null +++ b/src/app/redux/UserActions.js @@ -0,0 +1,6 @@ +// export function setUser(user) { +// return { +// type: 'SET_USER', +// username: user ? user.name : null +// }; +// } diff --git a/src/app/redux/UserSaga.js b/src/app/redux/UserSaga.js new file mode 100644 index 0000000..77bb585 --- /dev/null +++ b/src/app/redux/UserSaga.js @@ -0,0 +1,482 @@ +import {fromJS, Set, List} from 'immutable' +import {takeLatest} from 'redux-saga'; +import {call, put, select, fork} from 'redux-saga/effects'; +import {accountAuthLookup} from 'app/redux/AuthSaga' +import user from 'app/redux/User' +import {getAccount} from 'app/redux/SagaShared' +import {browserHistory} from 'react-router' +import {serverApiLogin, serverApiLogout} from 'app/utils/ServerApiClient'; +import {serverApiRecordEvent} from 'app/utils/ServerApiClient'; +import {loadFollows} from 'app/redux/FollowSaga' +import {PrivateKey, Signature, hash} from 'steem/lib/auth/ecc'; +import {api} from 'steem'; +import {translate} from 'app/Translator'; +import DMCAUserList from 'app/utils/DMCAUserList'; + + +export const userWatches = [ + watchRemoveHighSecurityKeys, // keep first to remove keys early when a page change happens + loginWatch, + saveLoginWatch, + logoutWatch, + // getCurrentAccountWatch, + loginErrorWatch, + lookupPreviousOwnerAuthorityWatch, + watchLoadSavingsWithdraw, + uploadImageWatch, +] + +const highSecurityPages = Array(/\/market/, /\/@.+\/(transfers|permissions|password)/, /\/~witnesses/) + +function* lookupPreviousOwnerAuthorityWatch() { + yield* takeLatest('user/lookupPreviousOwnerAuthority', lookupPreviousOwnerAuthority); +} +function* loginWatch() { + yield* takeLatest('user/USERNAME_PASSWORD_LOGIN', usernamePasswordLogin); +} +function* saveLoginWatch() { + yield* takeLatest('user/SAVE_LOGIN', saveLogin_localStorage); +} +function* logoutWatch() { + yield* takeLatest('user/LOGOUT', logout); +} + +function* loginErrorWatch() { + yield* takeLatest('user/LOGIN_ERROR', loginError); +} + +function* watchLoadSavingsWithdraw() { + yield* takeLatest('user/LOAD_SAVINGS_WITHDRAW', loadSavingsWithdraw); +} + +export function* watchRemoveHighSecurityKeys() { + yield* takeLatest('@@router/LOCATION_CHANGE', removeHighSecurityKeys); +} + +function* loadSavingsWithdraw() { + const username = yield select(state => state.user.getIn(['current', 'username'])) + const to = yield call([api, api.getSavingsWithdrawToAsync], username) + const fro = yield call([api, api.getSavingsWithdrawFromAsync], username) + + const m = {} + for(const v of to) m[v.id] = v + for(const v of fro) m[v.id] = v + + const withdraws = List(fromJS(m).values()) + .sort((a, b) => strCmp(a.get('complete'), b.get('complete'))) + + yield put(user.actions.set({ + key: 'savings_withdraws', + value: withdraws, + })) +} + +const strCmp = (a, b) => a > b ? 1 : a < b ? -1 : 0 + +// function* getCurrentAccountWatch() { +// // yield* takeLatest('user/SHOW_TRANSFER', getCurrentAccount); +// } + +function* removeHighSecurityKeys({payload: {pathname}}) { + const highSecurityPage = highSecurityPages.find(p => p.test(pathname)) != null + // Let the user keep the active key when going from one high security page to another. This helps when + // the user logins into the Wallet then the Permissions tab appears (it was hidden). This keeps them + // from getting logged out when they click on Permissions (which is really bad because that tab + // disappears again). + if(!highSecurityPage) + yield put(user.actions.removeHighSecurityKeys()) +} + +/** + @arg {object} action.username - Unless a WIF is provided, this is hashed with the password and key_type to create private keys. + @arg {object} action.password - Password or WIF private key. A WIF becomes the posting key, a password can create all three + key_types: active, owner, posting keys. +*/ +function* usernamePasswordLogin(action) { + // Sets 'loading' while the login is taking place. The key generation can take a while on slow computers. + yield call(usernamePasswordLogin2, action) + const current = yield select(state => state.user.get('current')) + if(current) { + const username = current.get('username') + yield fork(loadFollows, "getFollowingAsync", username, 'blog') + yield fork(loadFollows, "getFollowingAsync", username, 'ignore') + } +} + +// const isHighSecurityOperations = ['transfer', 'transfer_to_vesting', 'withdraw_vesting', +// 'limit_order_create', 'limit_order_cancel', 'account_update', 'account_witness_vote'] + + +const clean = (value) => value == null || value === '' || /null|undefined/.test(value) ? undefined : value + +function* usernamePasswordLogin2({payload: {username, password, saveLogin, + operationType /*high security*/, afterLoginRedirectToWelcome +}}) { + // login, using saved password + let autopost, memoWif, login_owner_pubkey, login_wif_owner_pubkey + if (!username && !password) { + const data = localStorage.getItem('autopost2') + if (data) { // auto-login with a low security key (like a posting key) + autopost = true; // must use simi-colon + // The 'password' in this case must be the posting private wif .. See setItme('autopost') + [username, password, memoWif, login_owner_pubkey] = new Buffer(data, 'hex').toString().split('\t'); + memoWif = clean(memoWif); + login_owner_pubkey = clean(login_owner_pubkey); + } + } + // no saved password + if (!username || !password) { + const offchain_account = yield select(state => state.offchain.get('account')) + if (offchain_account) serverApiLogout() + return + } + + let userProvidedRole // login via: username/owner + if (username.indexOf('/') > -1) { + // "alice/active" will login only with Alices active key + [username, userProvidedRole] = username.split('/') + } + + const pathname = yield select(state => state.global.get('pathname')) + const highSecurityLogin = + // /owner|active/.test(userProvidedRole) || + // isHighSecurityOperations.indexOf(operationType) !== -1 || + highSecurityPages.find(p => p.test(pathname)) != null + + const isRole = (role, fn) => (!userProvidedRole || role === userProvidedRole ? fn() : undefined) + + const account = yield call(getAccount, username) + if (!account) { + yield put(user.actions.loginError({ error: 'Username does not exist' })) + return + } + //dmca user block + if (username && DMCAUserList.includes(username)) { + yield put(user.actions.loginError({ error: translate('terms_violation') })) + return + } + + let private_keys + try { + const private_key = PrivateKey.fromWif(password) + login_wif_owner_pubkey = private_key.toPublicKey().toString() + private_keys = fromJS({ + posting_private: isRole('posting', () => private_key), + active_private: isRole('active', () => private_key), + memo_private: private_key, + }) + } catch (e) { + // Password (non wif) + login_owner_pubkey = PrivateKey.fromSeed(username + 'owner' + password).toPublicKey().toString() + private_keys = fromJS({ + posting_private: isRole('posting', () => PrivateKey.fromSeed(username + 'posting' + password)), + active_private: isRole('active', () => PrivateKey.fromSeed(username + 'active' + password)), + memo_private: PrivateKey.fromSeed(username + 'memo' + password), + }) + } + if (memoWif) + private_keys = private_keys.set('memo_private', PrivateKey.fromWif(memoWif)) + + yield call(accountAuthLookup, {payload: {account, private_keys, highSecurityLogin, login_owner_pubkey}}) + let authority = yield select(state => state.user.getIn(['authority', username])) + const hasActiveAuth = authority.get('active') === 'full' + if(!highSecurityLogin) { + const accountName = account.get('name') + authority = authority.set('active', 'none') + yield put(user.actions.setAuthority({accountName, auth: authority})) + } + const fullAuths = authority.reduce((r, auth, type) => (auth === 'full' ? r.add(type) : r), Set()) + if (!fullAuths.size) { + localStorage.removeItem('autopost2') + const owner_pub_key = account.getIn(['owner', 'key_auths', 0, 0]); + // const pub_keys = yield select(state => state.user.get('pub_keys_used')) + // serverApiRecordEvent('login_attempt', JSON.stringify({name: username, ...pub_keys, cur_owner: owner_pub_key})) + // FIXME pls parameterize opaque things like this into a constants file + // code like this requires way too much historical knowledge to + // understand. + if (owner_pub_key === 'STM7sw22HqsXbz7D2CmJfmMwt9rimtk518dRzsR1f8Cgw52dQR1pR') { + yield put(user.actions.loginError({ error: 'Hello. Your account may have been compromised. We are working on restoring an access to your account. Please send an email to support@steemit.com.' })) + return + } + if(login_owner_pubkey === owner_pub_key || login_wif_owner_pubkey === owner_pub_key) { + yield put(user.actions.loginError({ error: 'owner_login_blocked' })) + } else if(!highSecurityLogin && hasActiveAuth) { + yield put(user.actions.loginError({ error: 'active_login_blocked' })) + } else { + const generated_type = password[0] === 'P' && password.length > 40; + serverApiRecordEvent('login_attempt', JSON.stringify({name: username, login_owner_pubkey, owner_pub_key, generated_type})) + yield put(user.actions.loginError({ error: 'Incorrect Password' })) + } + return + } + if (authority.get('posting') !== 'full') + private_keys = private_keys.remove('posting_private') + + if(!highSecurityLogin || authority.get('active') !== 'full') + private_keys = private_keys.remove('active_private') + + const owner_pubkey = account.getIn(['owner', 'key_auths', 0, 0]) + const active_pubkey = account.getIn(['active', 'key_auths', 0, 0]) + const posting_pubkey = account.getIn(['posting', 'key_auths', 0, 0]) + + if (private_keys.get('memo_private') && + account.get('memo_key') !== private_keys.get('memo_private').toPublicKey().toString() + ) + // provided password did not yield memo key + private_keys = private_keys.remove('memo_private') + + if(!highSecurityLogin) { + if( + posting_pubkey === owner_pubkey || + posting_pubkey === active_pubkey + ) { + yield put(user.actions.loginError({ error: 'This login gives owner or active permissions and should not be used here. Please provide a posting only login.' })) + localStorage.removeItem('autopost2') + return + } + } + const memo_pubkey = private_keys.has('memo_private') ? + private_keys.get('memo_private').toPublicKey().toString() : null + + if( + memo_pubkey === owner_pubkey || + memo_pubkey === active_pubkey + ) + // Memo key could be saved in local storage.. In RAM it is not purged upon LOCATION_CHANGE + private_keys = private_keys.remove('memo_private') + + // If user is signing operation by operaion and has no saved login, don't save to RAM + if(!operationType || saveLogin) { + // Keep the posting key in RAM but only when not signing an operation. + // No operation or the user has checked: Keep me logged in... + yield put(user.actions.setUser({username, private_keys, login_owner_pubkey, vesting_shares: account.get('vesting_shares'), + received_vesting_shares: account.get('received_vesting_shares'), + delegated_vesting_shares: account.get('delegated_vesting_shares')})) + } else { + yield put(user.actions.setUser({username, vesting_shares: account.get('vesting_shares'), + received_vesting_shares: account.get('received_vesting_shares'), + delegated_vesting_shares: account.get('delegated_vesting_shares')})) + } + + if (!autopost && saveLogin) + yield put(user.actions.saveLogin()); + + try { + // const challengeString = yield serverApiLoginChallenge() + const offchainData = yield select(state => state.offchain) + const serverAccount = offchainData.get('account') + const challengeString = offchainData.get('login_challenge') + if (!serverAccount && challengeString) { + const signatures = {} + const challenge = {token: challengeString} + const bufSha = hash.sha256(JSON.stringify(challenge, null, 0)) + const sign = (role, d) => { + if (!d) return + const sig = Signature.signBufferSha256(bufSha, d) + signatures[role] = sig.toHex() + } + sign('posting', private_keys.get('posting_private')) + // sign('active', private_keys.get('active_private')) + serverApiLogin(username, signatures); + } + } catch(error) { + // Does not need to be fatal + console.error('Server Login Error', error); + } + if (afterLoginRedirectToWelcome) browserHistory.push('/welcome'); +} + +function* saveLogin_localStorage() { + if (!process.env.BROWSER) { + console.error('Non-browser environment, skipping localstorage') + return + } + localStorage.removeItem('autopost2') + const [username, private_keys, login_owner_pubkey] = yield select(state => ([ + state.user.getIn(['current', 'username']), + state.user.getIn(['current', 'private_keys']), + state.user.getIn(['current', 'login_owner_pubkey']), + ])) + if (!username) { + console.error('Not logged in') + return + } + // Save the lowest security key + const posting_private = private_keys.get('posting_private') + if (!posting_private) { + console.error('No posting key to save?') + return + } + const account = yield select(state => state.global.getIn(['accounts', username])) + if(!account) { + console.error('Missing global.accounts[' + username + ']') + return + } + const postingPubkey = posting_private.toPublicKey().toString() + try { + account.getIn(['active', 'key_auths']).forEach(auth => { + if(auth.get(0) === postingPubkey) + throw 'Login will not be saved, posting key is the same as active key' + }) + account.getIn(['owner', 'key_auths']).forEach(auth => { + if(auth.get(0) === postingPubkey) + throw 'Login will not be saved, posting key is the same as owner key' + }) + } catch(e) { + console.error(e) + return + } + const memoKey = private_keys.get('memo_private') + const memoWif = memoKey && memoKey.toWif() + const data = new Buffer(`${username}\t${posting_private.toWif()}\t${memoWif || ''}\t${login_owner_pubkey || ''}`).toString('hex') + // autopost is a auto login for a low security key (like the posting key) + localStorage.setItem('autopost2', data) +} + +function* logout() { + yield put(user.actions.saveLoginConfirm(false)) // Just incase it is still showing + if (process.env.BROWSER) + localStorage.removeItem('autopost2') + serverApiLogout(); +} + +function* loginError({payload: {/*error*/}}) { + serverApiLogout(); +} + +/** + If the owner key was changed after the login owner key, this function will find the next owner key history record after the change and store it under user.previous_owner_authority. +*/ +function* lookupPreviousOwnerAuthority({payload: {}}) { + const current = yield select(state => state.user.get('current')) + if(!current) return + + const login_owner_pubkey = current.get('login_owner_pubkey') + if(!login_owner_pubkey) return + + const username = current.get('username') + const key_auths = yield select(state => state.global.getIn(['accounts', username, 'owner', 'key_auths'])) + if (key_auths && key_auths.find(key => key.get(0) === login_owner_pubkey)) { + // console.log('UserSaga ---> Login matches current account owner'); + return + } + // Owner history since this index was installed July 14 + let owner_history = fromJS(yield call([api, api.getOwnerHistoryAsync], username)) + if(owner_history.count() === 0) return + owner_history = owner_history.sort((b, a) => {//sort decending + const aa = a.get('last_valid_time') + const bb = b.get('last_valid_time') + return aa < bb ? -1 : aa > bb ? 1 : 0 + }) + // console.log('UserSaga ---> owner_history', owner_history.toJS()) + const previous_owner_authority = owner_history.find(o => { + const auth = o.get('previous_owner_authority') + const weight_threshold = auth.get('weight_threshold') + const key3 = auth.get('key_auths').find(key2 => key2.get(0) === login_owner_pubkey && key2.get(1) >= weight_threshold) + return key3 ? auth : null + }) + if(!previous_owner_authority) { + console.log('UserSaga ---> Login owner does not match owner history'); + return + } + // console.log('UserSage ---> previous_owner_authority', previous_owner_authority.toJS()) + yield put(user.actions.setUser({previous_owner_authority})) +} + +function* uploadImageWatch() { + yield* takeLatest('user/UPLOAD_IMAGE', uploadImage); +} + +function* uploadImage({payload: {file, dataUrl, filename = 'image.txt', progress}}) { + const _progress = progress + progress = msg => { + // console.log('Upload image progress', msg) + _progress(msg) + } + + const stateUser = yield select(state => state.user) + const username = stateUser.getIn(['current', 'username']) + const d = stateUser.getIn(['current', 'private_keys', 'posting_private']) + if(!username) { + progress({error: 'Please login first.'}) + return + } + if(!d) { + progress({error: 'Login with your posting key'}) + return + } + + if(!file && !dataUrl) { + console.error('uploadImage required: file or dataUrl') + return + } + + let data, dataBs64 + if(file) { + // drag and drop + const reader = new FileReader() + data = yield new Promise(resolve => { + reader.addEventListener('load', () => { + const result = new Buffer(reader.result, 'binary') + resolve(result) + }) + reader.readAsBinaryString(file) + }) + } else { + // recover from preview + const commaIdx = dataUrl.indexOf(',') + dataBs64 = dataUrl.substring(commaIdx + 1) + data = new Buffer(dataBs64, 'base64') + } + + // The challenge needs to be prefixed with a constant (both on the server and checked on the client) to make sure the server can't easily make the client sign a transaction doing something else. + const prefix = new Buffer('ImageSigningChallenge') + const bufSha = hash.sha256(Buffer.concat([prefix, data])) + + const formData = new FormData() + if(file) { + formData.append('file', file) + } else { + // formData.append('file', file, filename) <- Failed to add filename=xxx to Content-Disposition + // Can't easily make this look like a file so this relies on the server supporting: filename and filebinary + formData.append('filename', filename) + formData.append('filebase64', dataBs64) + } + + const sig = Signature.signBufferSha256(bufSha, d) + const postUrl = `${$STM_Config.upload_image}/${username}/${sig.toHex()}` + + const xhr = new XMLHttpRequest() + xhr.open('POST', postUrl) + xhr.onload = function () { + console.log(xhr.status, xhr.responseText) + const res = JSON.parse(xhr.responseText) + const {error} = res + if(error) { + progress({error: 'Error: ' + error}) + return + } + const {url} = res + progress({url}) + } + xhr.onerror = function (error) { + console.error(filename, error) + progress({error: 'Unable to contact the server.'}) + } + xhr.upload.onprogress = function (event) { + if (event.lengthComputable) { + const percent = Math.round((event.loaded / event.total) * 100) + progress({message: `Uploading ${percent}%`}) + // console.log('Upload', percent) + } + } + xhr.send(formData) +} + + +// function* getCurrentAccount() { +// const current = yield select(state => state.user.get('current')) +// if (!current) return +// const [account] = yield call([api, api.getAccountsAsync], [current.get('username')]) +// yield put(g.actions.receiveAccount({ account })) +// } diff --git a/src/app/redux/constants.js b/src/app/redux/constants.js new file mode 100644 index 0000000..b50d794 --- /dev/null +++ b/src/app/redux/constants.js @@ -0,0 +1,6 @@ +export default { + FETCH_DATA_BATCH_SIZE: 20, + FETCH_DATA_EXPIRE_SEC: 30, + DEFAULT_SORT_ORDER: 'trending', + JULY_14_HACK: new Date('2016-07-14T00:00:00Z').getTime(), +}; diff --git a/src/app/redux/tests/AppReducer.test.js b/src/app/redux/tests/AppReducer.test.js new file mode 100644 index 0000000..c20ae36 --- /dev/null +++ b/src/app/redux/tests/AppReducer.test.js @@ -0,0 +1,53 @@ +/*global describe, it, before, beforeEach, after, afterEach */ +import chai, {expect} from 'chai'; +import dirtyChai from 'dirty-chai'; +import chaiImmutable from 'chai-immutable'; +import {Map} from 'immutable'; +import reducer from '../AppReducer'; +chai.use(dirtyChai); +chai.use(chaiImmutable); + +const defaultState = Map({ + effects: Map({}), + loading: false, + error: '' +}); + +const effectTriggered = { + type: 'EFFECT_TRIGGERED', + effectId: 1, + effect: { + CALL: true + } +}; + +const effectResolved = { + type: 'EFFECT_RESOLVED', + effectId: '1' +}; + + +describe('AppReducer', () => { + it('should return default state', () => { + expect( + reducer(undefined, {}) + ).to.equal(defaultState); + }); + + it('triggered effect should be added to effects and turn on loading', () => { + const state = reducer(undefined, effectTriggered); + expect(state.get('loading')).to.be.true(); + expect(state.get('effects').size).to.equal(1); + }); + + it('resolved effect should be added to effects and turn on loading', () => { + const triggeredState = Map({ + effects: Map({['1']: Date.now()}), + loading: true, + error: '' + }); + const state = reducer(triggeredState, effectResolved); + expect(state.get('effects').size).to.equal(0); + expect(state.get('loading')).to.be.false(); + }); +}); diff --git a/src/app/redux/tests/global.json b/src/app/redux/tests/global.json new file mode 100644 index 0000000..f25e836 --- /dev/null +++ b/src/app/redux/tests/global.json @@ -0,0 +1,82 @@ +{ + "pathname": "trending", + "props": { + "id": "2.0.0", + "head_block_number": 38897, + "head_block_id": "000097f1d40a280e4758128e7b57d60cee9df36d", + "time": "2016-03-24T15:39:06", + "current_witness": "mottler-5", + "total_pow": "18446744048449238345", + "num_pow_witnesses": 79, + "virtual_supply": "156738.000 STEEM", + "current_supply": "156738.000 STEEM", + "confidential_supply": "0.000 STEEM", + "current_sbd_supply": "0.000 SBD", + "confidential_sbd_supply": "0.000 SBD", + "total_vesting_fund_steem": "274.000 STEEM", + "total_vesting_shares": "274.000000 VESTS", + "total_reward_fund_steem": "77794.000 STEEM", + "total_reward_shares2": "0", + "sbd_interest_rate": 1000, + "average_block_size": 117, + "maximum_block_size": 131072, + "current_aslot": 47582, + "recent_slots_filled": "329648522672200722443889453235959593423", + "last_irreversible_block_num": 38876, + "max_virtual_bandwidth": "2203586534107862357", + "current_reserve_ratio": 1945 + }, + "tag_idx": { + "trending": [], + "active": [], + "recent": [], + "best": [] + }, + "categories": {}, + "content": {}, + "accounts": {}, + "pow_queue": [], + "witnesses": { + "id": "2.7.0", + "current_virtual_time": "0", + "next_shuffle_block_num": 38913, + "current_shuffled_witnesses": [ + "mottler-6", + "alice1", + "mottler-4", + "faddy3", + "dumpthisjunk", + "dealthandtaxes", + "lambda", + "cloop3", + "mr11acdee3", + "dumpcoin", + "nxt", + "devshuster", + "nxt1", + "mottler-7", + "failcoin1", + "mottler-1", + "smooth", + "mottler-5", + "blizzt", + "rurikovich", + "woot" + ], + "median_props": { + "account_creation_fee": "100.000 STEEM", + "maximum_block_size": 131072, + "sbd_interest_rate": 1000 + } + }, + "discussion_idx": { + "": { + "category": "", + "trending": [], + "recent": [], + "active": [], + "maturing": [], + "best": [] + } + } +} diff --git a/src/app/redux/tests/global.test.js b/src/app/redux/tests/global.test.js new file mode 100644 index 0000000..a553976 --- /dev/null +++ b/src/app/redux/tests/global.test.js @@ -0,0 +1,25 @@ +/*global describe, it, before, beforeEach, after, afterEach */ + +import chai, {expect} from 'chai'; +import chaiImmutable from 'chai-immutable'; +import Immutable, {Map} from 'immutable'; +import ReducerModule from '../GlobalReducer'; +chai.use(chaiImmutable); + +const {reducer, actions} = ReducerModule; + +describe('global reducer', () => { + it('should return empty state', () => { + expect( + reducer(undefined, {}) + ).to.equal(Map({})); + }); + + it('should apply new global state', () => { + const state = Immutable.fromJS(require('./global.json')); + //const action = {type: 'global/RECEIVE_STATE', payload: state}; + expect( + reducer(undefined, actions.receiveState(state)) + ).to.equal(state); + }); +}); diff --git a/src/app/utils/Accessors.js b/src/app/utils/Accessors.js new file mode 100644 index 0000000..dd56b75 --- /dev/null +++ b/src/app/utils/Accessors.js @@ -0,0 +1,11 @@ +export function immutableAccessor(obj, ...keys) { + if (!obj) return {} + if (keys.length === 1) return obj.get(keys[0]); + return keys.reduce((res, key) => {res[key] = obj.get(key); return res;}, {}); +} + +export function objAccessor(obj, ...keys) { + if (!obj) return {} + if (keys.length === 1) return obj[keys[0]]; + return keys.reduce((res, key) => {res[key] = obj[key]; return res;}, {}); +} diff --git a/src/app/utils/AppPropTypes.js b/src/app/utils/AppPropTypes.js new file mode 100644 index 0000000..b74c1c0 --- /dev/null +++ b/src/app/utils/AppPropTypes.js @@ -0,0 +1,8 @@ +import {PropTypes} from 'react'; + +const Children = PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node +]); + +export default {Children}; diff --git a/src/app/utils/BadActorList.js b/src/app/utils/BadActorList.js new file mode 100644 index 0000000..62d9977 --- /dev/null +++ b/src/app/utils/BadActorList.js @@ -0,0 +1,101 @@ +const list = ` +polonox +poloneix +biitrex +biittrex +bitter +bitterx +bittex +bittrax +bittre +bittrec +bittres +bittrex.com +bittrexe +bittrexx +bittrez +bittrix +bittrx +bitttrex +bitrex +bitrexx +bitrix +bitrrex +bttrex +btrex +bttrex +ittrex +bittrex-deposit +poloiex +poloinex +polomiex +polon +poloneex +poloneix +polonex +poloni +poloniax +polonie +poloniec +poloniee +polonieex +poloniek +polonieks +polonies +poloniet +poloniets +poloniex.com +poloniexcold +poloniexe +poloniexs +poloniext +poloniexx +poloniey +poloniez +poloniiex +poloniix +poloniks +poloniox +polonium +polonix +polonniex +polooniex +pooniex +poooniex +plolniex +ploniex +plooniex +poloex +oloniex +pooloniex +poliniex +polniex +poleniex +polionex +pollniex +polloniex +polnoiex +polonyex +polonied +polonixe +blocktardes +blocktrade +bocktrades +changelly.com +changely +shapeshif +shapeshift +randomwhale +randowale +coinpayments +minnowboost +minnowboster +minowbooster +blocktades +bloocktrades +bloctrades +blocktradess +blocktrade +`.trim().split('\n'); + +export default list; diff --git a/src/app/utils/BrowserTests.js b/src/app/utils/BrowserTests.js new file mode 100644 index 0000000..ddab1b8 --- /dev/null +++ b/src/app/utils/BrowserTests.js @@ -0,0 +1,44 @@ +import assert from 'assert' +import {serverApiRecordEvent} from 'app/utils/ServerApiClient' +import {PrivateKey, PublicKey} from 'steem/lib/auth/ecc' +import {config} from 'steem'; + +export const browserTests = {} + +export default function runTests() { + let rpt = '' + let pass = true + function it(name, fn) { + console.log('Testing', name) + rpt += 'Testing ' + name + '\n' + try { + fn() + } catch(error) { + console.error(error) + pass = false + rpt += error.stack + '\n\n' + serverApiRecordEvent('client_error', error) + } + } + + let private_key, public_key + const wif = '5JdeC9P7Pbd1uGdFVEsJ41EkEnADbbHGq6p1BwFxm6txNBsQnsw' + const pubkey = config.get('address_prefix') +'8m5UgaFAAYQRuaNejYdS8FVLVp9Ss3K1qAVk5de6F8s3HnVbvA' + + it('create private key', () => { + private_key = PrivateKey.fromSeed('1') + assert.equal(private_key.toWif(), wif) + }) + it('supports WIF format', () => { + assert(PrivateKey.fromWif(wif)) + }) + it('finds public from private key', () => { + public_key = private_key.toPublicKey() + // substring match ignore prefix + assert.equal(public_key.toString(), pubkey, 'Public key did not match') + }) + it('parses public key', () => { + assert(PublicKey.fromString(public_key.toString())) + }) + if(!pass) return rpt +} diff --git a/src/app/utils/ChainValidation.js b/src/app/utils/ChainValidation.js new file mode 100644 index 0000000..b3e3417 --- /dev/null +++ b/src/app/utils/ChainValidation.js @@ -0,0 +1,66 @@ +import tt from 'counterpart'; +import BadActorList from 'app/utils/BadActorList'; +import VerifiedExchangeList from 'app/utils/VerifiedExchangeList'; +import {PrivateKey, PublicKey} from 'steem/lib/auth/ecc'; + +export function validate_account_name(value, memo) { + let i, label, len, length, ref, suffix; + + suffix = tt('chainvalidation_js.account_name_should'); + if (!value) { + return suffix + tt('chainvalidation_js.not_be_empty'); + } + length = value.length; + if (length < 3) { + return suffix + tt('chainvalidation_js.be_longer'); + } + if (length > 16) { + return suffix + tt('chainvalidation_js.be_shorter'); + } + if (/\./.test(value)) { + suffix = tt('chainvalidation_js.each_account_segment_should'); + } + if (BadActorList.includes(value)) { + return 'Use caution sending to this account. Please double check your spelling for possible phishing. '; + } + if (VerifiedExchangeList.includes(value) && !memo) { + return tt('chainvalidation_js.verified_exchange_no_memo') + } + ref = value.split('.'); + for (i = 0, len = ref.length; i < len; i++) { + label = ref[i]; + if (!/^[a-z]/.test(label)) { + return suffix + tt('chainvalidation_js.start_with_a_letter'); + } + if (!/^[a-z0-9-]*$/.test(label)) { + return suffix + tt('chainvalidation_js.have_only_letters_digits_or_dashes'); + } + if (/--/.test(label)) { + return suffix + tt('chainvalidation_js.have_only_one_dash_in_a_row'); + } + if (!/[a-z0-9]$/.test(label)) { + return suffix + tt('chainvalidation_js.end_with_a_letter_or_digit'); + } + if (!(label.length >= 3)) { + return suffix + tt('chainvalidation_js.be_longer'); + } + } + return null; +} + +export function validate_memo_field(value, username, memokey) { + let suffix; + value = value.split(' ').filter(v=>v!=''); + for (var w in value) { + if (PrivateKey.isWif(value[w])) { + return suffix = 'Do not use private keys in memos. '; + } + if (memokey === PrivateKey.fromSeed(username + 'memo' + value[w]).toPublicKey().toString()) { + return suffix = 'Do not use passwords in memos. '; + } + if (/5[HJK]\w{40,45}/i.test(value[w])) { + return suffix = 'Please do not include what appears to be a private key or password. ' + } + } + return null; +} diff --git a/src/app/utils/ComponentFormatters.jsx b/src/app/utils/ComponentFormatters.jsx new file mode 100644 index 0000000..d5ee991 --- /dev/null +++ b/src/app/utils/ComponentFormatters.jsx @@ -0,0 +1,6 @@ +import React from 'react' + +export const authorNameAndRep = (author, authorRepLog10) => + {author} + {authorRepLog10 != null && ({authorRepLog10})} + \ No newline at end of file diff --git a/src/app/utils/ConsoleExports.js b/src/app/utils/ConsoleExports.js new file mode 100644 index 0000000..1e3ce89 --- /dev/null +++ b/src/app/utils/ConsoleExports.js @@ -0,0 +1,67 @@ +import {PrivateKey, PublicKey, Aes, key_utils} from 'steem/lib/auth/ecc'; + + +// import secureRandom from 'secure-random' +// import links from 'app/utils/Links' +// import assert from 'assert' + +module.exports = { + + PrivateKey, PublicKey, Aes, key_utils, + + // Run once to start, then again to stop and print a report + // https://facebook.github.io/react/docs/perf.html + perf: () => { + const Perf = require('react-addons-perf') + if (perfStarted) { + Perf.stop() + const lm = Perf.getLastMeasurements() + Perf.printInclusive(lm) + Perf.printExclusive(lm) + Perf.printWasted(lm) + perfStarted = false + } else { + Perf.start() + perfStarted = true + } + return Perf + }, + + resolve: (object, atty = '_') => { + if (! object.then) { + console.log(object) + return object + } + return new Promise((resolve, reject) => { + object.then(result => { + console.log(result) + resolve(result) + window[atty] = result + }).catch(error => { + console.error(error) + reject(error) + window[atty] = error + }) + }) + }, + + init: context => { + if (! context) return + for (const obj in module.exports) { + if (obj === 'init') continue + context[obj] = module.exports[obj] + } + }, + + // retest: () => { + // const largeData = secureRandom.randomBuffer(1024 * 10).toString('hex') + // const all = links.any() + // for (let i = 0; i < 10000; i++) { + // const match = (largeData + 'https://example.com').match(all) + // assert(match, 'no match') + // assert(match[0] === 'https://example.com', 'no match') + // } + // }, +} + +let perfStarted = false diff --git a/src/app/utils/ContentPreview.js b/src/app/utils/ContentPreview.js new file mode 100644 index 0000000..bea503b --- /dev/null +++ b/src/app/utils/ContentPreview.js @@ -0,0 +1,17 @@ +export default function contentPreview(content, length) { + const txt = content.replace(/ +/g, ' '); // only 1 space in a row + const max_words = length / 7; + let words = 0; + let res = ''; + for (let i = 0; i < txt.length; i++) { + const ch = txt.charAt(i); + if (ch === '.') break; + if (ch === ' ' || ch === '\n') { + words++; + if (words > max_words) break; + if (i > length) break; + }; + res += ch; + } + return res; +} diff --git a/src/app/utils/DMCAList.js b/src/app/utils/DMCAList.js new file mode 100644 index 0000000..8e270d9 --- /dev/null +++ b/src/app/utils/DMCAList.js @@ -0,0 +1,47 @@ +export default ` +/children/@carlidos/why-it-s-important-to-teach-your-child-gratitude +/curation/@carlidos/lead-by-better-borrowing-3-content-curation-tips-100-of-steem-dollars-will-be-donated-to-curie +/compliment/@carlidos/you-get-complimented-on-and-you-cower-down-why +/motivation/@carlidos/the-power-that-moves-the-world +/project-positivity/@carlidos/the-wonder-of-mother-earth-project-positivity +/children/@carlidos/impossible-to-get-anything-done-with-a-preschooler-at-home +/steemit/@carlidos/internet-censorship-and-surveillance +/life/@carlidos/how-your-child-develops-emotionally-through-play-focuses-on-my-son +/kyoto/@carlidos/two-days-in-kyoto-japan +/psych101/@psych101/psych-101-moving-through-and-recovering-from-social-anxiety +/crowdfunding/@carlidos/the-basics-of-crowdfunding +/psychology/@carlidos/psych-101-classic-schools-of-thoughts-in-psychology +/psychology/@carlidos/the-makings-of-schizophrenia-part-1 +/psychology/@carlidos/psych-101-beginner-operant-condition +/luciddreaming/@carlidos/6wisjx-how-to-interpret-your-own-dreams +/life/@carlidos/regain-control-of-your-time-and-finally-get-things-done +/psych101/@psych101/psych-101-show-your-appreciation-to-those-close-to-you-while-you-still-can +/life/@carlidos/psych-101-the-temptation-to-project-in-cyberspace-psych-101-the-trap-of-projection-in-cyberspace +/life/@carlidos/psych-101-support-groups-and-the-risks-of-imitation +/life/@carlidos/the-pitfalls-of-dating-with-a-big-age-difference-psychology +/life/@carlidos/psych-101-how-to-induce-a-lucid-dream-psych-101-the-memory-palace +/psychology/@carlidos/psych-101-beginner-classical-conditioning-psych-101-intermediate-classical-conditioning +/sex/@carlidos/honoring-your-sexual-needs-and-boundaries-within-a-relationship +/life/@carlidos/the-power-of-trusting-yourself +/psych/@carlidos/psych-101-how-psychology-shapes-our-world +/love/@carlidos/is-love-without-attachment-possible +/food/@outchemy/5-top-countries-you-need-to-visit-if-you-are-a-chocolate-addict +/travel/@outchemy/broken-relationships-the-museum-of-zagreb-that-celebrates-heartache +/travel/@outchemy/get-rid-of-the-toxins-see-these-amazing-detox-vacations +/photography/@maxmalini/the-dark-hedges +/creation/@andysyuhada/toys-creation-own-photography +/photography/@andysyuhada/animals-photography +/photography/@andysyuhada/flies-and-locusts-aceh +/photography/@andysyuhada/how-to-make-toys-photography +/photography/@andysyuhada/life-photography-toys +/photography/@andysyuhada/photographing-birds-in-a-tree-relaxing +/photography/@andysyuhada/toys-photography-life-toys +/photography/@andysyuhada/toys-photography-very-funny-was-dancing-and-singing +/photography/@andysyuhada/tutorial-photography-pengujian-kamera +/photography/@elvinanurhaliza/butterfly-photography +/photography/@elvinanurhaliza/finepix-s4800-reptiles-etc +/photography/@elvinanurhaliza/photography-flowers-finepix-s4800 +/photography/@elvinanurhaliza/photohraphy-dewy-flowers +/photography/@elvinanurhaliza/spider-finepix-s4800-reptiles-etc +/entertainment/@seanfrederic/feature-film-loco-i-m-a-producer-on-starts-filming-soon +`.trim().split('\n'); diff --git a/src/app/utils/DMCAUserList.js b/src/app/utils/DMCAUserList.js new file mode 100644 index 0000000..9bbe446 --- /dev/null +++ b/src/app/utils/DMCAUserList.js @@ -0,0 +1,8 @@ +const list = ` +spaces +the-gaming-llama +cmgsteems +iamgod +`.trim().split('\n'); + +export default list; diff --git a/src/app/utils/DomUtils.js b/src/app/utils/DomUtils.js new file mode 100644 index 0000000..36021a8 --- /dev/null +++ b/src/app/utils/DomUtils.js @@ -0,0 +1,5 @@ +export function findParent(el, class_name) { + if (el.className && el.className.indexOf && el.className.indexOf(class_name) !== -1) return el; + if (el.parentNode) return findParent(el.parentNode, class_name); + return null; +} diff --git a/src/app/utils/ExtractContent.js b/src/app/utils/ExtractContent.js new file mode 100644 index 0000000..f972c08 --- /dev/null +++ b/src/app/utils/ExtractContent.js @@ -0,0 +1,123 @@ +import remarkableStripper from 'app/utils/RemarkableStripper' +import links from 'app/utils/Links' +import sanitize from 'sanitize-html' +import {htmlDecode} from 'app/utils/Html' +import HtmlReady from 'shared/HtmlReady' +import Remarkable from 'remarkable' + +const remarkable = new Remarkable({ html: true, linkify: false }) + +export default function extractContent(get, content) { + const { + author, + permlink, + parent_author, + parent_permlink, + json_metadata, + category, + title, + created, + net_rshares, + children + } = get( + content, + 'author', + 'permlink', + 'parent_author', + 'parent_permlink', + 'json_metadata', + 'category', + 'title', + 'created', + 'net_rshares', + 'children' + ); + const author_link = '/@' + get(content, 'author'); + let link = `/@${author}/${permlink}`; + if (category) link = `/${category}${link}`; + const body = get(content, 'body'); + let jsonMetadata = {} + let image_link + try { + jsonMetadata = JSON.parse(json_metadata) + if(typeof jsonMetadata == 'string') { + // At least one case where jsonMetadata was double-encoded: #895 + jsonMetadata = JSON.parse(jsonMetadata) + } + // First, attempt to find an image url in the json metadata + if(jsonMetadata) { + if(jsonMetadata.image && Array.isArray(jsonMetadata.image)) { + [image_link] = jsonMetadata.image + } + } + } catch(error) { + // console.error('Invalid json metadata string', json_metadata, 'in post', author, permlink); + } + + // If nothing found in json metadata, parse body and check images/links + if(!image_link) { + let rtags + { + const isHtml = /^([\S\s]*)<\/html>$/.test(body) + const htmlText = isHtml ? body : remarkable.render(body.replace(/|$)/g, '(html comment removed: $1)')) + rtags = HtmlReady(htmlText, {mutate: false}) + } + + [image_link] = Array.from(rtags.images) + } + + // Was causing broken thumnails. IPFS was not finding images uploaded to another server until a restart. + // if(config.ipfs_prefix && image_link) // allow localhost nodes to see ipfs images + // image_link = image_link.replace(links.ipfsPrefix, config.ipfs_prefix) + + let desc + let desc_complete = false + if(!desc) { + // Short description. + // Remove bold and header, etc. + // Stripping removes links with titles (so we got the links above).. + // Remove block quotes if detected at beginning of comment preview if comment has a parent + const body2 = remarkableStripper.render(get(content, 'depth') > 1 ? body.replace(/(^(\n|\r|\s)*)>([\s\S]*?).*\s*/g, '') : body); + desc = sanitize(body2, {allowedTags: []})// remove all html, leaving text + desc = htmlDecode(desc) + + // Strip any raw URLs from preview text + desc = desc.replace(/https?:\/\/[^\s]+/g, ''); + + // Grab only the first line (not working as expected. does rendering/sanitizing strip newlines?) + desc = desc.trim().split("\n")[0]; + + if(desc.length > 140) { + desc = desc.substring(0, 140).trim(); + + const dotSpace = desc.lastIndexOf('. ') + if(dotSpace > 80 && !get(content, 'depth') > 1) { + desc = desc.substring(0, dotSpace + 1) + } else { + // Truncate, remove the last (likely partial) word (along with random punctuation), and add ellipses + desc = desc.substring(0, 120).trim().replace(/[,!\?]?\s+[^\s]+$/, "…"); + } + } + desc_complete = body2 === desc // is the entire body in desc? + } + const pending_payout = get(content, 'pending_payout_value'); + return { + author, + author_link, + permlink, + parent_author, + parent_permlink, + json_metadata: jsonMetadata, + category, + title, + created, + net_rshares, + children, + link, + image_link, + desc, + desc_complete, + body, + pending_payout, + }; +} diff --git a/src/app/utils/ExtractMeta.js b/src/app/utils/ExtractMeta.js new file mode 100644 index 0000000..e2745db --- /dev/null +++ b/src/app/utils/ExtractMeta.js @@ -0,0 +1,87 @@ +import extractContent from 'app/utils/ExtractContent'; +import {objAccessor} from 'app/utils/Accessors'; +import normalizeProfile from 'app/utils/NormalizeProfile'; + +const site_desc = 'Steemit is a social media platform where everyone gets paid for creating and curating content. It leverages a robust digital points system (Steem) for digital rewards.'; + +function addSiteMeta(metas) { + metas.push({title: 'Steemit'}); + metas.push({name: 'description', content: site_desc}); + metas.push({property: 'og:type', content: 'website'}); + metas.push({property: 'og:site_name', content: 'Steemit'}); + metas.push({property: 'og:title', content: 'Steemit'}); + metas.push({property: 'og:description', content: site_desc}); + metas.push({property: 'og:image', content: 'https://steemit.com/images/steemit.png'}); + metas.push({property: 'fb:app_id', content: $STM_Config.fb_app}); + metas.push({name: 'twitter:card', content: 'summary'}); + metas.push({name: 'twitter:site', content: '@steemit'}); + metas.push({name: 'twitter:title', content: '#Steemit'}); + metas.push({name: 'twitter:description', site_desc}); + metas.push({name: 'twitter:image', content: 'https://steemit.com/images/steemit.png'}); +} + +export default function extractMeta(chain_data, rp) { + const metas = []; + if (rp.username && rp.slug) { // post + const post = `${rp.username}/${rp.slug}`; + const content = chain_data.content[post]; + const author = chain_data.accounts[rp.username]; + const profile = normalizeProfile(author); + if (content && content.id !== '0.0.0') { // API currently returns 'false' data with id 0.0.0 for posts that do not exist + const d = extractContent(objAccessor, content, false); + const url = 'https://steemit.com' + d.link; + const title = d.title + ' — Steemit'; + const desc = d.desc + " by " + d.author; + const image = d.image_link || profile.profile_image + const {category, created} = d + + // Standard meta + metas.push({title}); + metas.push({canonical: url}); + metas.push({name: 'description', content: desc}); + + // Open Graph data + metas.push({property: 'og:title', content: title}); + metas.push({property: 'og:type', content: 'article'}); + metas.push({property: 'og:url', content: url}); + metas.push({property: 'og:image', content: image || 'https://steemit.com/images/steemit.png'}); + metas.push({property: 'og:description', content: desc}); + metas.push({property: 'og:site_name', content: 'Steemit'}); + metas.push({property: 'fb:app_id', content: $STM_Config.fb_app}); + metas.push({property: 'article:tag', content: category}); + metas.push({property: 'article:published_time', content: created}); + + // Twitter card data + metas.push({name: 'twitter:card', content: image ? 'summary_large_image' : 'summary'}); + metas.push({name: 'twitter:site', content: '@steemit'}); + metas.push({name: 'twitter:title', content: title}); + metas.push({name: 'twitter:description', content: desc}); + metas.push({name: 'twitter:image', content: image || 'https://steemit.com/images/steemit-twshare-2.png'}); + } else { + addSiteMeta(metas); + } + } else if (rp.accountname) { // user profile root + const account = chain_data.accounts[rp.accountname]; + let {name, about, profile_image} = normalizeProfile(account); + if(name == null) name = account.name; + if(about == null) about = "Join thousands on steemit who share, post and earn rewards."; + if(profile_image == null) profile_image = 'https://steemit.com/images/steemit-twshare-2.png'; + // Set profile tags + const title = `@${account.name}`; + const desc = `The latest posts from ${name}. Follow me at @${account.name}. ${about}`; + const image = profile_image; + + // Standard meta + metas.push({name: 'description', content: desc}); + + // Twitter card data + metas.push({name: 'twitter:card', content: 'summary'}); + metas.push({name: 'twitter:site', content: '@steemit'}); + metas.push({name: 'twitter:title', content: title}); + metas.push({name: 'twitter:description', content: desc}); + metas.push({name: 'twitter:image', content: image}); + } else { // site + addSiteMeta(metas); + } + return metas; +} diff --git a/src/app/utils/FormatCoins.js b/src/app/utils/FormatCoins.js new file mode 100644 index 0000000..390ed29 --- /dev/null +++ b/src/app/utils/FormatCoins.js @@ -0,0 +1,15 @@ +import { APP_NAME, LIQUID_TOKEN, LIQUID_TOKEN_UPPERCASE, DEBT_TOKEN, DEBT_TOKEN_SHORT, CURRENCY_SIGN, VESTING_TOKEN } from 'app/client_config'; + +// TODO add comments and explanations +// TODO change name to formatCoinTypes? +// TODO make use of DEBT_TICKER etc defined in config/clietn_config +export function formatCoins(string) { + // return null or undefined if string is not provided + if(!string) return string + // TODO use .to:owerCase() ? for string normalisation + string = string.replace('SBD', DEBT_TOKEN_SHORT ).replace('SD', DEBT_TOKEN_SHORT) + .replace('Steem Power', VESTING_TOKEN).replace('STEEM POWER', VESTING_TOKEN) + .replace('Steem', LIQUID_TOKEN).replace('STEEM', LIQUID_TOKEN_UPPERCASE) + .replace('$', CURRENCY_SIGN) + return string +} diff --git a/src/app/utils/FormatDecimal.test.js b/src/app/utils/FormatDecimal.test.js new file mode 100644 index 0000000..044ff01 --- /dev/null +++ b/src/app/utils/FormatDecimal.test.js @@ -0,0 +1,23 @@ +/*global describe, it, before, beforeEach, after, afterEach */ + +import chai, {expect} from 'chai'; +import dirtyChai from 'dirty-chai'; +import {formatDecimal} from './ParsersAndFormatters'; +chai.use(dirtyChai); + +describe('formatDecimal', () => { + it('should format decimals', () => { + const test_cases = [ + [100.0, '100.00'], + [101, '101.00'], + ['102', '102.00'], + [1000.12, '1,000.12'], + [100000, '100,000.00'], + [1000000000000.00, '1,000,000,000,000.00'], + [-1000, '-1,000.00'], + ]; + test_cases.forEach(v => { + expect(formatDecimal(v[0]).join('')).to.equal(v[1]); + }); + }); +}); diff --git a/src/app/utils/Html.js b/src/app/utils/Html.js new file mode 100644 index 0000000..7ebb8ab --- /dev/null +++ b/src/app/utils/Html.js @@ -0,0 +1,21 @@ + +export const htmlDecode = txt => txt.replace(/&[a-z]+;/g, ch => { + const char = htmlCharMap[ch.substring(1, ch.length - 1)] + return char ? char : ch +}) + +const htmlCharMap = { + amp: '&', + quot: '"', + lsquo: '‘', + rsquo: '’', + sbquo: '‚', + ldquo: '“', + rdquo: '”', + bdquo: '„', + hearts: '♥', + trade: '™', + hellip: '…', + pound: '£', + copy: '' +} diff --git a/src/app/utils/ImageUserBlockList.js b/src/app/utils/ImageUserBlockList.js new file mode 100644 index 0000000..a439d57 --- /dev/null +++ b/src/app/utils/ImageUserBlockList.js @@ -0,0 +1,5 @@ +const list = ` +iamgod +`.trim().split('\n'); + +export default list; diff --git a/src/app/utils/JsPlugins.js b/src/app/utils/JsPlugins.js new file mode 100644 index 0000000..3009b2a --- /dev/null +++ b/src/app/utils/JsPlugins.js @@ -0,0 +1,38 @@ +// 3rd party plugins + +export default function init(config) { + + if (config.google_analytics_id) { + (function (i, s, o, g, r, a, m) { + i['GoogleAnalyticsObject'] = r; + i[r] = i[r] || function () { + (i[r].q = i[r].q || []).push(arguments) + }, i[r].l = 1 * new Date(); + a = s.createElement(o), + m = s.getElementsByTagName(o)[0]; + a.async = 1; + a.src = g; + m.parentNode.insertBefore(a, m) + })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); + ga('create', config.google_analytics_id, 'auto'); + } + + if (config.facebook_app_id) { + window.fbAsyncInit = function () { + FB.init({ + appId: config.facebook_app_id, + xfbml: true, + version: 'v2.6' + }); + }; + (function (d, s, id) { + var js, fjs = d.getElementsByTagName(s)[0]; + if (d.getElementById(id)) {return;} + js = d.createElement(s); + js.id = id; + js.src = "//connect.facebook.net/en_US/sdk.js"; + fjs.parentNode.insertBefore(js, fjs); + }(document, 'script', 'facebook-jssdk')); + } + +} diff --git a/src/app/utils/Links.js b/src/app/utils/Links.js new file mode 100644 index 0000000..6b39fe6 --- /dev/null +++ b/src/app/utils/Links.js @@ -0,0 +1,42 @@ + +const urlChar = '[^\\s"<>\\]\\[\\(\\)]' +const urlCharEnd = urlChar.replace(/\]$/, '.,\']') // insert bad chars to end on +const imagePath = '(?:(?:\\.(?:tiff?|jpe?g|gif|png|svg|ico)|ipfs/[a-z\\d]{40,}))' +const domainPath = '(?:[-a-zA-Z0-9\\._]*[-a-zA-Z0-9])' +const urlChars = '(?:' + urlChar + '*' + urlCharEnd + ')?' + +const urlSet = ({domain = domainPath, path} = {}) => { + // urlChars is everything but html or markdown stop chars + return `https?:\/\/${domain}(?::\\d{2,5})?(?:[/\\?#]${urlChars}${path ? path : ''})${path ? '' : '?'}` +} + +/** + Unless your using a 'g' (glob) flag you can store and re-use your regular expression. Use the cache below. If your using a glob (for example: replace all), the regex object becomes stateful and continues where it left off when called with the same string so naturally the regexp object can't be cached for long. +*/ +export const any = (flags = 'i') => new RegExp(urlSet(), flags) +export const local = (flags = 'i') => new RegExp(urlSet({domain: '(?:localhost|(?:.*\\.)?steemit.com)'}), flags) +export const remote = (flags = 'i') => new RegExp(urlSet({domain: `(?!localhost|(?:.*\\.)?steemit.com)${domainPath}`}), flags) +export const youTube = (flags = 'i') => new RegExp(urlSet({domain: '(?:(?:.*\.)?youtube.com|youtu.be)'}), flags) +export const image = (flags = 'i') => new RegExp(urlSet({path: imagePath}), flags) +export const imageFile = (flags = 'i') => new RegExp(imagePath, flags) +// export const nonImage = (flags = 'i') => new RegExp(urlSet({path: '!' + imageFile}), flags) +// export const markDownImageRegExp = (flags = 'i') => new RegExp('\!\[[\w\s]*\]\(([^\)]+)\)', flags); + +export default { + any: any(), + local: local(), + remote: remote(), + image: image(), + imageFile: imageFile(), + youTube: youTube(), + youTubeId: /(?:(?:youtube.com\/watch\?v=)|(?:youtu.be\/)|(?:youtube.com\/embed\/))([A-Za-z0-9\_\-]+)/i, + vimeoId: /(?:vimeo.com\/|player.vimeo.com\/video\/)([0-9]+)/, + // simpleLink: new RegExp(`(.*)<\/a>`, 'ig'), + ipfsPrefix: /(https?:\/\/.*)?\/ipfs/i, +} + +// Original regex +// const urlRegex = '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$'; + +// About performance +// Using exec on the same regex object requires a new regex to be created and compile for each text (ex: post). Instead replace can be used `body.replace(remoteRe, l => {` discarding the result for better performance`}). Re-compiling is a chrome bottleneck but did not effect nodejs. diff --git a/src/app/utils/Links.test.js b/src/app/utils/Links.test.js new file mode 100644 index 0000000..4c84e17 --- /dev/null +++ b/src/app/utils/Links.test.js @@ -0,0 +1,110 @@ + +import assert from 'assert' +import secureRandom from 'secure-random' +import links, * as linksRe from 'app/utils/Links' + +describe('Links', () => { + it('all', () => { + match(linksRe.any(), 'https://example.com/wiki/Poe\'s_law', 'https://example.com/wiki/Poe\'s_law') + match(linksRe.any(), 'https://example.com\'', 'https://example.com') + match(linksRe.any(), '"https://example.com', 'https://example.com') + match(linksRe.any(), 'https://example.com\"', 'https://example.com') + match(linksRe.any(), 'https://example.com\'', 'https://example.com') + match(linksRe.any(), 'https://example.com<', 'https://example.com') + match(linksRe.any(), 'https://example.com>', 'https://example.com') + match(linksRe.any(), 'https://example.com\n', 'https://example.com') + match(linksRe.any(), ' https://example.com ', 'https://example.com') + match(linksRe.any(), 'https://example.com ', 'https://example.com') + match(linksRe.any(), 'https://example.com.', 'https://example.com') + match(linksRe.any(), 'https://example.com/page.', 'https://example.com/page') + match(linksRe.any(), 'https://example.com,', 'https://example.com') + match(linksRe.any(), 'https://example.com/page,', 'https://example.com/page') + }) + it('multiple matches', () => { + const all = linksRe.any('ig') + let match = all.exec('\nhttps://example.com/1\nhttps://example.com/2') + assert.equal(match[0], 'https://example.com/1') + match = all.exec('https://example.com/1 https://example.com/2') + assert.equal(match[0], 'https://example.com/2') + }) + it('by domain', () => { + const locals = ['https://localhost/', 'http://steemit.com', 'http://steemit.com/group'] + match(linksRe.local(), locals) + matchNot(linksRe.remote(), locals) + + const remotes = ['https://example.com/', 'http://abc.co'] + match(linksRe.remote(), remotes) + matchNot(linksRe.local(), remotes) + // match(linksRe({external: false}), largeData + 'https://steemit.com2/next', 'https://steemit.com2/next') + }) + it('by image', () => { + match(linksRe.image(), 'https://example.com/a.jpeg') + match(linksRe.image(), 'https://example.com/a/b.jpeg') + match(linksRe.image(), '![](https://example.com/img2/nehoshtanit.jpg)', 'https://example.com/img2/nehoshtanit.jpg') + match(linksRe.image(), ' { + const largeData = secureRandom.randomBuffer(1024 * 10).toString('hex') + it('any, ' + largeData.length + ' bytes x 10,000', () => { + for (let i = 0; i < 10000; i++) { + const match = (largeData + 'https://example.com').match(linksRe.any()) + assert(match, 'no match') + assert(match[0] === 'https://example.com', 'no match') + } + }) + it('image (large), ' + largeData.length + ' bytes x 10,000', () => { + for (let i = 0; i < 10000; i++) { + matchNot(linksRe.image(), 'https://lh3.googleusercontent.com/OehcduRZPcVIX_2tlOKgYHADtBvorTfL4JtjfGAPWZyiiI9p_g2ZKEUKfuv3By-aiVfirXaYvEsViJEbxts6IeVYqidnpgkkkXAe0Q79_ARXX6CU5hBK2sZaHKa20U3jBzYbMxT-OVNX8-JYf-GYa2geUQa6pVpUDY35iaiiNBObF-TMIUOqm0P61gCdukTFwLgld2BBlxoVNNt_w6VglYHJP0W4izVNkEu7ugrU-qf2Iw9hb22SGIFNpbzL_ldomDMthIuYfKSYGsqe2ClvNKRz-_vVCQr7ggRXra16uQOdUUv5IVnkK67p9yR8ioajJ4tiGdzazYVow46pbeZ76i9_NoEYnOEX2_a7niofnC5BgAjoQEeoes1cMWVM7V8ZSexBA-cxmi0EVLds4RBkInvaUZjVL7h3oJ5I19GugPTzlyVyYtkf1ej6LNttkagqHgMck87UQGvCbwDX9ECTngffwQPYZlZKnthW0DlkFGgHN8T9uqEpl-3ki50gTa6gC0Q16mEeDRKZe7_g5Sw52OjMsfWxmBBWWMSHzlQKKAIKMKKaD6Td0O_zpiXXp7Fyl7z_iESvCpOAUAIKnyJyF_Y0UYktEmw=w2066-h1377-no') + } + }) + it('image, ' + largeData.length + ' bytes x 10,000', () => { + for (let i = 0; i < 10000; i++) { + const match = (largeData + 'https://example.com/img.jpeg').match(linksRe.image()) + assert(match, 'no match') + assert(match[0] === 'https://example.com/img.jpeg', 'no match') + } + }) + it('remote, ' + largeData.length + ' bytes x 10,000', () => { + for (let i = 0; i < 10000; i++) { + const match = (largeData + 'https://example.com').match(linksRe.remote()) + assert(match, 'no match') + assert(match[0] === 'https://example.com', 'no match') + } + }) + it('youTube', () => { + match(linksRe.youTube(), 'https://youtu.be/xG7ajrbj4zs?t=7s') + match(linksRe.youTube(), 'https://www.youtube.com/watch?v=xG7ajrbj4zs&t=14s') + match(linksRe.youTube(), 'https://www.youtube.com/watch?v=xG7ajrbj4zs&feature=youtu.be&t=14s') + }) + it('youTubeId', () => { + match(links.youTubeId, 'https://youtu.be/xG7ajrbj4zs?t=7s', 'xG7ajrbj4zs', 1) + match(links.youTubeId, 'https://www.youtube.com/watch?v=xG7ajrbj4zs&t=14s', 'xG7ajrbj4zs', 1) + match(links.youTubeId, 'https://www.youtube.com/watch?v=xG7ajrbj4zs&feature=youtu.be&t=14s', 'xG7ajrbj4zs', 1) + }) +}) + +const match = (...args) => compare(true, ...args) +const matchNot = (...args) => compare(false, ...args) +const compare = (matching, re, input, output = input, pos = 0) => { + if (Array.isArray(input)) { + for (let i = 0; i < input.length; i++) + compare(matching, re, input[i], output[i]) + return + } + // console.log('compare, input', input) + // console.log('compare, output', output) + const m = input.match(re) + if(matching) { + assert(m, `No match --> ${input} --> output ${output} --> using ${re.toString()}`) + // console.log('m', m) + assert.equal(m[pos], output, `Unmatched ${m[pos]} --> input ${input} --> output ${output} --> using ${re.toString()}`) + } else { + assert(!m, `False match --> input ${input} --> output ${output} --> using ${re.toString()}`) + } +} diff --git a/src/app/utils/MarketClasses.js b/src/app/utils/MarketClasses.js new file mode 100644 index 0000000..678eb10 --- /dev/null +++ b/src/app/utils/MarketClasses.js @@ -0,0 +1,118 @@ +import {roundDown, roundUp} from "./MarketUtils"; +import { LIQUID_TICKER, DEBT_TICKER } from 'app/client_config' +const precision = 1000; + +class Order { + constructor(order, side) { + this.side = side; + this.price = parseFloat(order.real_price); + this.price = side === 'asks' ? roundUp(this.price, 6) : Math.max(roundDown(this.price, 6), 0.000001); + this.stringPrice = this.price.toFixed(6); + this.steem = parseInt(order.steem, 10); + this.sbd = parseInt(order.sbd, 10); + this.date = order.created; + } + + getSteemAmount() { + return this.steem / precision; + } + + getStringSteem() { + return this.getSteemAmount().toFixed(3); + } + + getPrice() { + return this.price; + } + + getStringPrice() { + return this.stringPrice; + } + + getStringSBD() { + return this.getSBDAmount().toFixed(3); + } + + getSBDAmount() { + return this.sbd / precision; + } + + add(order) { + return new Order({ + real_price: this.price, + steem: this.steem + order.steem, + sbd: this.sbd + order.sbd, + date: this.date + }, this.type); + } + + equals(order) { + return ( + this.getStringSBD() === order.getStringSBD() && + this.getStringSteem() === order.getStringSteem() && + this.getStringPrice() === order.getStringPrice() + ); + } +} + +class TradeHistory { + + constructor(fill) { + // Norm date (FF bug) + var zdate = fill.date; + if(!/Z$/.test(zdate)) + zdate = zdate + 'Z' + + this.date = new Date(zdate); + this.type = fill.current_pays.indexOf(DEBT_TICKER) !== -1 ? "bid" : "ask"; + this.color = this.type == "bid" ? "buy-color" : "sell-color"; + if (this.type === "bid") { + this.sbd = parseFloat(fill.current_pays.split(" " + DEBT_TICKER)[0]); + this.steem = parseFloat(fill.open_pays.split(" " + LIQUID_TICKER)[0]); + } else { + this.sbd = parseFloat(fill.open_pays.split(" " + DEBT_TICKER)[0]); + this.steem = parseFloat(fill.current_pays.split(" " + LIQUID_TICKER)[0]); + } + + this.price = this.sbd / this.steem; + this.price = this.type === 'ask' ? roundUp(this.price, 6) : Math.max(roundDown(this.price, 6), 0.000001); + this.stringPrice = this.price.toFixed(6); + } + + getSteemAmount() { + return this.steem; + } + + getStringSteem() { + return this.getSteemAmount().toFixed(3); + } + + getSBDAmount() { + return this.sbd; + } + + getStringSBD() { + return this.getSBDAmount().toFixed(3); + } + + getPrice() { + return this.price; + } + + getStringPrice() { + return this.stringPrice; + } + + equals(order) { + return ( + this.getStringSBD() === order.getStringSBD() && + this.getStringSteem() === order.getStringSteem() && + this.getStringPrice() === order.getStringPrice() + ); + } +} + +module.exports = { + Order, + TradeHistory +} diff --git a/src/app/utils/MarketUtils.js b/src/app/utils/MarketUtils.js new file mode 100644 index 0000000..6b6e921 --- /dev/null +++ b/src/app/utils/MarketUtils.js @@ -0,0 +1,24 @@ +function roundUp(num, precision) { + let satoshis = parseFloat(num) * Math.pow(10, precision) + + // Attempt to correct floating point: 1.0001 satoshis should not round up. + satoshis = satoshis - 0.0001 + + // Round up, restore precision + return Math.ceil(satoshis) / Math.pow(10, precision) +} + +function roundDown(num, precision) { + let satoshis = parseFloat(num) * Math.pow(10, precision) + + // Attempt to correct floating point: 1.9999 satoshis should not round down. + satoshis = satoshis + 0.0001 + + // Round down, restore precision + return Math.floor(satoshis) / Math.pow(10, precision) +} + +module.exports = { + roundUp, + roundDown +} diff --git a/src/app/utils/NormalizeProfile.js b/src/app/utils/NormalizeProfile.js new file mode 100644 index 0000000..1fef9b0 --- /dev/null +++ b/src/app/utils/NormalizeProfile.js @@ -0,0 +1,68 @@ +import linksRe from 'app/utils/Links' + +function truncate(str, len) { + if(str) { + str = str.trim() + if(str.length > len) { + str = str.substring(0, len - 1) + '...' + } + } + return str +} + +/** + * Enforce profile data length & format standards. + */ +export default function normalizeProfile(account) { + + if(! account) return {} + + // Parse + let profile = {}; + if(account.json_metadata) { + try { + const md = JSON.parse(account.json_metadata); + if(md.profile) { + profile = md.profile; + } + if(!(typeof profile == 'object')) { + console.error('Expecting object in account.json_metadata.profile:', profile); + profile = {}; + } + } catch (e) { + console.error('Invalid json metadata string', account.json_metadata, 'in account', account.name); + } + } + + // Read & normalize + let {name, about, location, website, profile_image, cover_image} = profile + + name = truncate(name, 20) + about = truncate(about, 160) + location = truncate(location, 30) + + if(/^@/.test(name)) name = null; + if(website && website.length > 100) website = null; + if (website && website.indexOf("http") === -1) { + website = 'http://' + website; + } + if(website) { + // enforce that the url regex matches, and fully + const m = website.match(linksRe.any) + if(!m || m[0] !== website) { + website = null; + } + } + + if(profile_image && !/^https?:\/\//.test(profile_image)) profile_image = null; + if(cover_image && !/^https?:\/\//.test(cover_image)) cover_image = null; + + return { + name, + about, + location, + website, + profile_image, + cover_image, + }; +} diff --git a/src/app/utils/Notifications.js b/src/app/utils/Notifications.js new file mode 100644 index 0000000..d3f8761 --- /dev/null +++ b/src/app/utils/Notifications.js @@ -0,0 +1,9 @@ +export const NTYPES = ['total', 'feed', 'reward', 'send', 'mention', 'follow', 'vote', 'comment_reply', 'post_reply', 'account_update', 'message', 'receive']; + +export function notificationsArrayToMap(data) { + const notifications = data && data.length ? (data.length === 1 ? data[0].slice(1) : data) : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + return notifications.reduce((result, n, i) => { + result[NTYPES[i]] = n; + return result; + }, {}); +} diff --git a/src/app/utils/ParsersAndFormatters.js b/src/app/utils/ParsersAndFormatters.js new file mode 100644 index 0000000..98e8737 --- /dev/null +++ b/src/app/utils/ParsersAndFormatters.js @@ -0,0 +1,96 @@ +import tt from 'counterpart'; + +function fractional_part_len(value) { + const parts = (Number(value) + '').split('.'); + return parts.length < 2 ? 0 : parts[1].length; +} + +// FIXME this should be unit tested.. here is one bug: 501,695,.505 +export function formatDecimal(value, decPlaces = 2, truncate0s = true) { + let decSeparator, fl, i, j, sign, thouSeparator, abs_value; + if (value === null || value === void 0 || isNaN(value)) { + return 'NaN'; + } + if (truncate0s) { + fl = fractional_part_len(value); + if (fl < 2) fl = 2; + if (fl < decPlaces) decPlaces = fl; + } + decSeparator = '.'; + thouSeparator = ','; + sign = value < 0 ? '-' : ''; + abs_value = Math.abs(value); + i = parseInt(abs_value.toFixed(decPlaces), 10) + ''; + j = i.length; + j = i.length > 3 ? j % 3 : 0; + const decPart = (decPlaces ? decSeparator + Math.abs(abs_value - i).toFixed(decPlaces).slice(2) : ''); + return [sign + (j ? i.substr(0, j) + thouSeparator : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + thouSeparator), decPart]; +} + +export function parsePayoutAmount(amount) { + return parseFloat(String(amount).replace(/\s[A-Z]*$/, '')); +} + +/** + This is a rough approximation of log10 that works with huge digit-strings. + Warning: Math.log10(0) === NaN + The 0.00000001 offset fixes cases of Math.log(1000)/Math.LN10 = 2.99999999~ +*/ +function log10(str) { + const leadingDigits = parseInt(str.substring(0, 4)); + const log = Math.log(leadingDigits) / Math.LN10 + 0.00000001 + const n = str.length - 1; + return n + (log - parseInt(log)); +} + +export const repLog10 = rep2 => { + if(rep2 == null) return rep2 + let rep = String(rep2) + const neg = rep.charAt(0) === '-' + rep = neg ? rep.substring(1) : rep + + let out = log10(rep) + if(isNaN(out)) out = 0 + out = Math.max(out - 9, 0); // @ -9, $0.50 earned is approx magnitude 1 + out = (neg ? -1 : 1) * out + out = (out * 9) + 25 // 9 points per magnitude. center at 25 + // base-line 0 to darken and < 0 to auto hide (grep rephide) + out = parseInt(out) + return out +} + +export function countDecimals(amount) { + if(amount == null) return amount + amount = String(amount).match(/[\d\.]+/g).join('') // just dots and digits + const parts = amount.split('.') + return parts.length > 2 ? undefined : parts.length === 1 ? 0 : parts[1].length +} + +// this function searches for right translation of provided error (usually from back-end) +export function translateError(string) { + if (typeof(string) != 'string') return string; + switch (string) { + case 'Account not found': + return tt('g.account_not_found') + case 'Incorrect Password': + return tt('g.incorrect_password') + case 'Username does not exist': + return tt('g.username_does_not_exist') + case 'Account name should be longer.': + return tt('g.account_name_should_be_longer') + case 'Account name should be shorter.': + return tt('g.account_name_should_be_shorter') + case 'Account name should start with a letter.': + return tt('g.account_name_should_start_with_a_letter') + case 'Account name should have only letters, digits, or dashes.': + return tt('g.account_name_should_have_only_letters_digits_or_dashes') + case 'vote currently exists, user must be indicate a desire to reject witness': + return tt('g.vote_currently_exists_user_must_be_indicate_a_to_reject_witness') + case 'Only one Steem account allowed per IP address every 10 minutes': + return tt('g.only_one_APP_NAME_account_allowed_per_ip_address_every_10_minutes') + case 'Cannot increase reward of post within the last minute before payout': + return tt('g.cannot_increase_reward_of_post_within_the_last_minute_before_payout') + default: + return string + } +} diff --git a/src/app/utils/ProxifyUrl.js b/src/app/utils/ProxifyUrl.js new file mode 100644 index 0000000..9f5f6af --- /dev/null +++ b/src/app/utils/ProxifyUrl.js @@ -0,0 +1,43 @@ +/*global $STM_Config:false*/ +/** + * this regular expression should capture all possible proxy domains + * Possible URL schemes are: + * / + * /{int}x{int}/ + * /{int}x{int}/[.../{int}x{int}/] + * /{int}x{int}/[/{int}x{int}/]/ + * @type {RegExp} + */ +const rProxyDomain = /^http(s)?:\/\/steemit(dev|stage)?images.com\//g; +const rProxyDomainsDimensions = /http(s)?:\/\/steemit(dev|stage)?images.com\/([0-9]+x[0-9]+)\//g; +const NATURAL_SIZE = '0x0/'; + +export const imageProxy = () => $STM_Config.img_proxy_prefix; + +/** + * Strips all proxy domains from the beginning of the url. Adds the global proxy if dimension is specified + * @param {string} url + * @param {string|boolean} dimensions - optional - if provided. url is proxied && global var $STM_Config.img_proxy_prefix is avail. resp will be "$STM_Config.img_proxy_prefix{dimensions}/{sanitized url}" + * if falsy, all proxies are stripped. + * if true, preserves the first {int}x{int} in a proxy url. If not found, uses 0x0 + * @returns string + */ +export default (url, dimensions = false) => { + const proxyList = url.match(rProxyDomainsDimensions); + let respUrl = url; + if (proxyList) { + const lastProxy = proxyList[proxyList.length - 1]; + respUrl = url.substring(url.lastIndexOf(lastProxy) + lastProxy.length); + } + if (dimensions && $STM_Config && $STM_Config.img_proxy_prefix) { + let dims = dimensions + '/'; + if (typeof dimensions !== 'string') { + dims = (proxyList) ? proxyList.shift().match(/([0-9]+x[0-9]+)\//g)[0] : NATURAL_SIZE; + } + if (NATURAL_SIZE !== dims || !rProxyDomain.test(respUrl)) { + return $STM_Config.img_proxy_prefix + dims + respUrl; + } + } + return respUrl; +} + diff --git a/src/app/utils/ProxifyUrl.test.js b/src/app/utils/ProxifyUrl.test.js new file mode 100644 index 0000000..e1f8db7 --- /dev/null +++ b/src/app/utils/ProxifyUrl.test.js @@ -0,0 +1,61 @@ +/*global describe, global, before:false, it*/ +import assert from 'assert' +import proxifyImageUrl from './ProxifyUrl' + +describe('ProxifyUrl', () => { + before(() => { + global.$STM_Config = {img_proxy_prefix: 'https://steemitimages.com/'}; + }); + it('naked URL', () => { + testCase('https://example.com/img.png', '100x200', 'https://steemitimages.com/100x200/https://example.com/img.png') + testCase('https://example.com/img.png', '0x0', 'https://steemitimages.com/0x0/https://example.com/img.png') + testCase('https://example.com/img.png', true, 'https://steemitimages.com/0x0/https://example.com/img.png') + testCase('https://example.com/img.png', false, 'https://example.com/img.png') + }) + it('naked steemit hosted URL', () => { + testCase('https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg', '256x512', 'https://steemitimages.com/256x512/https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg') + testCase('https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg', false, 'https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg') + }) + it('proxied steemit hosted URL', () => { + testCase('https://steemitimages.com/0x0/https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg', '256x512', 'https://steemitimages.com/256x512/https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg') + testCase('https://steemitimages.com/256x512/https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg', false, 'https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg') + }) + it('proxied URL', () => { + testCase('https://steemitimages.com/0x0/https://example.com/img.png', '100x200', 'https://steemitimages.com/100x200/https://example.com/img.png') + testCase('https://steemitimages.com/256x512/https://peopledotcom.files.wordpress.com/2017/09/grumpy-harvey-cat.jpg?w=2000', '100x200', 'https://steemitimages.com/100x200/https://peopledotcom.files.wordpress.com/2017/09/grumpy-harvey-cat.jpg?w=2000') + testCase('https://steemitimages.com/0x0/https://example.com/img.png', false, 'https://example.com/img.png') + }) + it('double-proxied URL', () => { + testCase('https://steemitimages.com/0x0/https://steemitimages.com/0x0/https://example.com/img.png', '100x200', 'https://steemitimages.com/100x200/https://example.com/img.png') + testCase('https://steemitimages.com/0x0/https://steemitimages.com/256x512/https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg', false, 'https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg') + }) + it('preserve dimensions - single-proxied URL', () => { + //simple preservation + testCase('https://steemitdevimages.com/100x200/https://example.com/img.png', true, 'https://steemitimages.com/100x200/https://example.com/img.png') + testCase('https://steemitdevimages.com/1001x2001/https://example.com/img.png', true, 'https://steemitimages.com/1001x2001/https://example.com/img.png') + }) + it('preserve dimensions - double-proxied URL', () => { + //simple preservation at a 2 nesting level + //foreign domain + testCase('https://steemitimages.com/100x200/https://steemitimages.com/0x0/https://example.com/img.png', true, 'https://steemitimages.com/100x200/https://example.com/img.png') + testCase('https://steemitdevimages.com/1001x2001/https://steemitimages.com/0x0/https://example.com/img.png', true, 'https://steemitimages.com/1001x2001/https://example.com/img.png') + //steemit domain + testCase('https://steemitdevimages.com/1001x2001/https://steemitimages.com/0x0/https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg', true, 'https://steemitimages.com/1001x2001/https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg') + }) + it('preserve dimensions - strip proxies & dimensions when appropriate', () => { + //simple preservation at a 2 nesting level + //steemit domain + testCase('https://steemitimages.com/0x0/https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg', true, 'https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg') + //foreign domain + testCase('https://steemitimages.com/0x0/https://example.com/img.png', true, 'https://steemitimages.com/0x0/https://example.com/img.png') + //case where last is natural sizing, assumes natural sizing - straight to direct steemit file url + testCase('https://steemitimages.com/0x0/https://steemitimages.com/100x100/https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg', true, 'https://steemitimages.com/DQmaJe2Tt5kmVUaFhse1KTEr4N1g9piMgD3YjPEQhkZi3HR/30day-positivity-challenge.jpg') + //case where last is natural sizing, assumes natural sizing - straight to direct steemit /0x0/ domain host url + testCase('https://steemitimages.com/0x0/https://steemitimages.com/100x100/https://example.com/img.png', true, 'https://steemitimages.com/0x0/https://example.com/img.png') + }) +}) + +const testCase = (inputUrl, outputDims, expectedUrl) => { + const outputUrl = proxifyImageUrl(inputUrl, outputDims); + assert.equal(outputUrl, expectedUrl, `(${inputUrl}, ${outputDims}) should return ${expectedUrl}. output was ${outputUrl}`) +} diff --git a/src/app/utils/ReactForm.js b/src/app/utils/ReactForm.js new file mode 100644 index 0000000..fecc7a1 --- /dev/null +++ b/src/app/utils/ReactForm.js @@ -0,0 +1,193 @@ +/** + @arg {string} name - form state will appear in this.state[name] + @arg {object} instance - `this` for the component + @arg {array} fields - ['username', 'save', ...] + @arg {object} initialValues required for checkboxes {save: false, ...} + @arg {function} validation - values => ({ username: ! values.username ? 'Required' : null, ... }) +*/ +export default function reactForm({name, instance, fields, initialValues, validation = () => {}}) { + if(typeof instance !== 'object') throw new TypeError('instance is a required object') + if(!Array.isArray(fields)) throw new TypeError('fields is a required array') + if(typeof initialValues !== 'object') throw new TypeError('initialValues is a required object') + + // Give API users access to this.props, this.state, this.etc.. + validation = validation.bind(instance) + + const formState = instance.state = instance.state || {} + formState[name] = { + // validate: () => setFormState(instance, fields, validation), + handleSubmit: submitCallback => event => { + event.preventDefault() + const {valid} = setFormState(name, instance, fields, validation) + if(!valid) return + const data = getData(fields, instance.state) + let formValid = true + const fs = instance.state[name] || {} + fs.submitting = true + + // User can call this function upon successful submission + const updateInitialValues = () => { + setInitialValuesFromForm(name, instance, fields, initialValues) + formState[name].resetForm() + } + + instance.setState( + {[name]: fs}, + () => { + // TODO, support promise ret + const ret = submitCallback({data, event, updateInitialValues}) || {} + // Look for field level errors + for(const fieldName of Object.keys(ret)) { + const error = ret[fieldName] + if(!error) continue + const value = instance.state[fieldName] || {} + value.error = error + value.touched = true + if(error) formValid = false + instance.setState({[fieldName]: value}) + } + fs.submitting = false + fs.valid = formValid + instance.setState({[name]: fs}) + } + ) + }, + resetForm: () => { + for(const field of fields) { + const fieldName = n(field) + const f = instance.state[fieldName] + const def = initialValues[fieldName] + f.props.onChange(def) + } + }, + clearForm: () => { + for(const field of fields) { + const fieldName = n(field) + const f = instance.state[fieldName] + f.props.onChange() + } + }, + } + + for(const field of fields) { + const fieldName = n(field) + const fieldType = t(field) + + const fs = formState[fieldName] = { + value: null, + error: null, + touched: false, + } + + // Caution: fs.props is expanded , so only add valid props for the component + fs.props = {name: fieldName} + + { + const initialValue = initialValues[fieldName] + if(fieldType === 'checked') { + fs.value = toString(initialValue) + fs.props.checked = toBoolean(initialValue) + } else if(fieldType === 'selected') { + fs.props.selected = toString(initialValue) + fs.value = fs.props.selected + } else { + fs.props.value = toString(initialValue) + fs.value = fs.props.value + } + } + + fs.props.onChange = e => { + const value = e && e.target ? e.target.value : e // API may pass value directly + const v = {...(instance.state[fieldName] || {})} + const initialValue = initialValues[fieldName] + + if(fieldType === 'checked') { + v.touched = toString(value) !== toString(initialValue) + v.value = v.props.checked = toBoolean(value) + v.value = value + } else if(fieldType === 'selected') { + v.touched = toString(value) !== toString(initialValue) + v.value = v.props.selected = toString(value) + } else { + v.touched = toString(value) !== toString(initialValue) + v.value = v.props.value = toString(value) + } + + instance.setState( + {[fieldName]: v}, + () => {setFormState(name, instance, fields, validation)} + ) + } + + fs.props.onBlur = () => { + // Some errors are better shown only after blur === true + const v = {...(instance.state[fieldName] || {})} + v.blur = true + instance.setState({[fieldName]: v}) + } + } +} + +function setFormState(name, instance, fields, validation) { + let formValid = true + let formTouched = false + const v = validation(getData(fields, instance.state)) + for(const field of fields) { + const fieldName = n(field) + const validate = v[fieldName] + const error = validate ? validate : null + const value = {...(instance.state[fieldName] || {})} + value.error = error + formTouched = formTouched || value.touched + if(error) formValid = false + instance.setState({[fieldName]: value}) + } + const fs = {...(instance.state[name] || {})} + fs.valid = formValid + fs.touched = formTouched + instance.setState({[name]: fs}) + return fs +} + +function setInitialValuesFromForm(name, instance, fields, initialValues) { + const data = getData(fields, instance.state) + for(const field of fields) { + const fieldName = n(field) + initialValues[fieldName] = data[fieldName] + } +} + +function getData(fields, state) { + const data = {} + for(const field of fields) { + const fieldName = n(field) + data[fieldName] = state[fieldName].value + } + return data +} + +/* + @arg {string} field - field:type +
      +        type = checked (for checkbox or radio)
      +        type = selected (for seelct option)
      +        type = string
      +    
      + @return {string} type +*/ +function t(field) { + const [, type = 'string'] = field.split(':') + return type +} + +/** + @return {string} name +*/ +function n(field) { + const [name] = field.split(':') + return name +} + +const hasValue = v => v == null ? false : (typeof v === 'string' ? v.trim() : v) === '' ? false : true +const toString = v => hasValue(v) ? v : '' +const toBoolean = v => hasValue(v) ? JSON.parse(v) : '' diff --git a/src/app/utils/ReduxForms.js b/src/app/utils/ReduxForms.js new file mode 100644 index 0000000..8e8e2a0 --- /dev/null +++ b/src/app/utils/ReduxForms.js @@ -0,0 +1,9 @@ +export const cleanReduxInput = i => { + // Remove all props that don't belong. Triggers React warnings. + const {name, placeholder, label, value, checked, onChange, onBlur, onFocus} = i + const ret = {name, placeholder, label, value, checked, onChange, onBlur, onFocus} + if(ret.value == null) delete ret.value + if(ret.label == null) delete ret.label + if(ret.type == null) delete ret.type + return ret +} diff --git a/src/app/utils/RegisterServiceWorker.js b/src/app/utils/RegisterServiceWorker.js new file mode 100644 index 0000000..4da112e --- /dev/null +++ b/src/app/utils/RegisterServiceWorker.js @@ -0,0 +1,34 @@ +export default function registerServiceWorker() { + if (!navigator.serviceWorker) return Promise.resolve(false); + return navigator.serviceWorker.register('/service-worker.js', {scope: '/'}) + .then(function (registration) { + navigator.serviceWorker.ready.catch(e => console.error('-- registerServiceWorker error -->', e)); + return navigator.serviceWorker.ready.then(function (serviceWorkerRegistration) { + let subscription = serviceWorkerRegistration.pushManager.getSubscription(); + return subscription.then(function (subscription) { + if (subscription) { + return subscription; + } + return serviceWorkerRegistration.pushManager.subscribe({ + userVisibleOnly: true + }); + }); + }); + }).then(function (subscription) { + const rawKey = subscription.getKey ? subscription.getKey('p256dh') : ''; + const key = rawKey ? + btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : + ''; + const rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : ''; + const authSecret = rawAuthSecret ? + btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : + ''; + return { + endpoint: subscription.endpoint, + keys: { + p256dh: key, + auth: authSecret + } + }; + }); +} diff --git a/src/app/utils/RemarkablePlugin.js b/src/app/utils/RemarkablePlugin.js new file mode 100644 index 0000000..61bceda --- /dev/null +++ b/src/app/utils/RemarkablePlugin.js @@ -0,0 +1,180 @@ +// import {validate_account_name} from 'app/utils/ChainValidation' +// import linksRe from 'app/utils/Links' +// import config from 'config' + +/** usage + +import {plugin, tagRules} from 'app/utils/RemarkablePlugin' +import Remarkable from 'remarkable' +const remarkable = new Remarkable() +remarkable.use(plugin(tagRules)) +const renderedText = remarkable.render(`#jsc @user`) +console.log('hashtags', tagRules.hashtags()) +console.log('usertags', tagRules.usertags()) +console.log('renderedText', renderedText) + +*/ +// export const tagRules = { +// done: () => { +// const ret = { +// tags: Array.from(hashtags), +// users: Array.from(usertags), +// } +// hashtags = new Set() +// usertags = new Set() +// return ret +// }, +// link_open: (tokens, i, options, env, renderer) => { +// tagLinkOpen = true +// const link = tokens[i + 1].content +// tagRules.youtubeId = null +// tagRules.youtubeTime = null +// if(link) link.replace(linksRe.youTube, url => { +// const match = url.match(linksRe.youTubeId) +// if(match && match.length >= 2) { +// const id = match[1] +// tagRules.youtubeId = id +// const [, query] = link.split('?') +// if(query) { +// const params = query.split('&') +// for(const param of params) { +// if(/^t=/.test(param)) tagRules.youtubeTime = param.substring(2) +// } +// } +// return +// } +// console.log("Youtube link without ID?", url); +// }) +// if(tagRules.youtubeId) return '' +// return renderer.rules.link_open(tokens, i, options, env, renderer) +// }, +// text: (tokens, i, options, env, renderer) => { +// if(tagRules.youtubeId) +// return `youtube:${tagRules.youtubeId}${tagRules.youtubeTime ? ',' + tagRules.youtubeTime : ''}` +// +// if(tagLinkOpen) +// return renderer.rules.text(tokens, i, options, env, renderer) +// let content = tokens[i].content +// // hashtag +// content = content.replace(/(^|\s)(#[-a-z\d]+)/ig, tag => { +// if(/#[\d]+$/.test(tag)) return tag // Don't allow numbers to be tags +// const space = /^\s/.test(tag) ? tag[0] : '' +// tag = tag.trim().substring(1) +// hashtags.add(tag) +// return space + `
      #${tag}` +// }) +// // usertag (mention) +// content = content.replace(/(^|\s)(@[-\.a-z\d]+)/ig, user => { +// const space = /^\s/.test(user) ? user[0] : '' +// user = user.trim().substring(1) +// const valid = validate_account_name(user) == null +// if(valid) usertags.add(user) +// return space + (valid ? +// `@${user}` : +// '@' + user +// ) +// }) +// // unescapted ipfs links (temp, until the reply editor categorizes the image) +// //if(config.ipfs_prefix) +// // content = content.replace(linksRe.ipfsPrefix, config.ipfs_prefix) +// +// return content +// }, +// link_close: (tokens, i, options, env, renderer) => { +// tagLinkOpen = false +// if(tagRules.youtubeId) { +// tagRules.youtubeId = null +// tagRules.youtubeTime = null +// return '' +// } +// return renderer.rules.link_close(tokens, i, options, env, renderer) +// }, +// } +// let hashtags = new Set() +// let usertags = new Set() +// let tagLinkOpen +// +// export const imageLinks = { +// done: () => { +// if(image.length > 1) links.delete(image[1]) +// const ret = { +// image: Array.from(image), +// links: Array.from(links) +// } +// image = [] +// links = new Set() +// return ret +// }, +// image: (tokens, i) => { +// // ![Image Alt](https://duckduckgo.com/assets/badges/logo_square.64.png) +// const token = tokens[i] +// const {content} = token +// if(image.length) return content // only first one +// image.push(token.src) +// return content +// }, +// link_open: (tokens, i) => { +// // [inline link](http://www.duckduckgo.com "Example Title") +// const token = tokens[i] +// const {content} = token +// // console.log('token(link_open)', token) +// const {href} = token +// if(linksRe.image.test(href) && !image.length) +// // looks like an image link, no markup though +// image.push(href) +// else +// links.add(href) +// // [![Foo](http://www.google.com.au/images/nav_logo7.png)](http://google.com.au/) +// if(image.length) return content // only the first image and link combo +// // link around an image +// const next = tokens[i + 1] +// if(next && next.type === 'image') { +// const {src} = next +// image.push(src) +// image.push(href) +// } +// return content +// } +// } +// let image = [] +// let links = new Set() + +/** +Usage... + +import Remarkable from 'remarkable' +const remarkable = new Remarkable() +const customRules = { + text: (tokens, i, options, env, ctx) => { + const token = tokens[i] + const content = token.content + console.log('token(2)', token) + return content + } +} +remarkable.use(plugin(customRules)) +const renderedText = remarkable.render(`#jsc`) +console.log('renderedText', renderedText) + +*/ +// export const plugin = rules => md => { +// // const render = md.renderer.render +// md.renderer.render = (tokens, options, env) => { +// let str = '' +// for (let i = 0; i < tokens.length; i++) { +// if (tokens[i].type === 'inline') { +// str += md.renderer.render(tokens[i].children, options, env); +// } else { +// const token = tokens[i] +// // console.log('token(plugin)', token, rules) +// const customRule = rules[token.type] +// if(customRule) +// str += customRule(tokens, i, options, env, md.renderer) +// else { +// str += md.renderer.rules[token.type](tokens, i, options, env, md.renderer) +// } +// } +// } +// return str +// } +// } diff --git a/src/app/utils/RemarkableStripper.js b/src/app/utils/RemarkableStripper.js new file mode 100644 index 0000000..f0c2a82 --- /dev/null +++ b/src/app/utils/RemarkableStripper.js @@ -0,0 +1,23 @@ +import Remarkable from 'remarkable' + +const remarkable = new Remarkable() +export default remarkable + +/** Removes all markdown leaving just plain text */ +const remarkableStripper = md => { + md.renderer.render = (tokens, options, env) => { + let str = '' + for (let i = 0; i < tokens.length; i++) { + if (tokens[i].type === 'inline') { + str += md.renderer.render(tokens[i].children, options, env); + } else { + // console.log('content', tokens[i]) + const content = tokens[i].content + str += (content || '') + ' ' + } + } + return str + } +} + +remarkable.use(remarkableStripper) // removes all markdown diff --git a/src/app/utils/SanitizeConfig.js b/src/app/utils/SanitizeConfig.js new file mode 100644 index 0000000..b4206a9 --- /dev/null +++ b/src/app/utils/SanitizeConfig.js @@ -0,0 +1,138 @@ +import { getPhishingWarningMessage } from 'shared/HtmlReady'; // the only allowable title attribute for a div + +const iframeWhitelist = [ + { + re: /^(https?:)?\/\/player.vimeo.com\/video\/.*/i, + fn: (src) => { + // + if(!src) return null + const m = src.match(/https:\/\/player\.vimeo\.com\/video\/([0-9]+)/) + if(!m || m.length !== 2) return null + return 'https://player.vimeo.com/video/' + m[1] + } + }, + { re: /^(https?:)?\/\/www.youtube.com\/embed\/.*/i, + fn: (src) => { + return src.replace(/\?.+$/, ''); // strip query string (yt: autoplay=1,controls=0,showinfo=0, etc) + } + }, + { + re: /^https:\/\/w.soundcloud.com\/player\/.*/i, + fn: (src) => { + if(!src) return null + // + const m = src.match(/url=(.+?)&/) + if(!m || m.length !== 2) return null + return 'https://w.soundcloud.com/player/?url=' + m[1] + + '&auto_play=false&hide_related=false&show_comments=true' + + '&show_user=true&show_reposts=false&visual=true' + } + } +]; +export const noImageText = '(Image not shown due to low ratings)' +export const allowedTags = ` + div, iframe, del, + a, p, b, i, q, br, ul, li, ol, img, h1, h2, h3, h4, h5, h6, hr, + blockquote, pre, code, em, strong, center, table, thead, tbody, tr, th, td, + strike, sup, sub +`.trim().split(/,\s*/) + +// Medium insert plugin uses: div, figure, figcaption, iframe +export default ({large = true, highQualityPost = true, noImage = false, sanitizeErrors = []}) => ({ + allowedTags, + // figure, figcaption, + + // SEE https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet + allowedAttributes: { + // "src" MUST pass a whitelist (below) + iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen', + 'webkitallowfullscreen', 'mozallowfullscreen'], + + // class attribute is strictly whitelisted (below) + // and title is only set in the case of a phishing warning + div: ['class', 'title'], + + // style is subject to attack, filtering more below + td: ['style'], + img: ['src', 'alt'], + a: ['href', 'rel'], + }, + transformTags: { + iframe: (tagName, attribs) => { + const srcAtty = attribs.src; + for(const item of iframeWhitelist) + if(item.re.test(srcAtty)) { + const src = typeof item.fn === 'function' ? item.fn(srcAtty, item.re) : srcAtty + if(!src) break + return { + tagName: 'iframe', + attribs: { + frameborder: '0', + allowfullscreen: 'allowfullscreen', + webkitallowfullscreen: 'webkitallowfullscreen', // deprecated but required for vimeo : https://vimeo.com/forums/help/topic:278181 + mozallowfullscreen: 'mozallowfullscreen', // deprecated but required for vimeo + src, + width: large ? '640' : '480', + height: large ? '360' : '270', + }, + } + } + console.log('Blocked, did not match iframe "src" white list urls:', tagName, attribs) + sanitizeErrors.push('Invalid iframe URL: ' + srcAtty) + return {tagName: 'div', text: `(Unsupported ${srcAtty})`} + }, + img: (tagName, attribs) => { + if(noImage) return {tagName: 'div', text: noImageText} + //See https://github.com/punkave/sanitize-html/issues/117 + let {src, alt} = attribs + if(!/^(https?:)?\/\//i.test(src)) { + console.log('Blocked, image tag src does not appear to be a url', tagName, attribs) + sanitizeErrors.push('An image in this post did not save properly.') + return {tagName: 'img', attribs: {src: 'brokenimg.jpg'}} + } + + // replace http:// with // to force https when needed + src = src.replace(/^http:\/\//i, '//') + let atts = {src} + if(alt && alt !== '') atts.alt = alt + return {tagName, attribs: atts} + }, + div: (tagName, attribs) => { + const attys = {} + const classWhitelist = ['pull-right', 'pull-left', 'text-justify', 'text-rtl', 'text-center', 'text-right', 'videoWrapper', 'phishy'] + const validClass = classWhitelist.find(e => attribs.class == e) + if(validClass) + attys.class = validClass + if (validClass === 'phishy' && attribs.title === getPhishingWarningMessage()) + attys.title = attribs.title + return { + tagName, + attribs: attys + } + }, + td: (tagName, attribs) => { + const attys = {} + if(attribs.style === 'text-align:right') + attys.style = 'text-align:right' + return { + tagName, + attribs: attys + } + }, + a: (tagName, attribs) => { + let {href} = attribs + if(!href) href = '#' + href = href.trim() + const attys = {href} + // If it's not a (relative or absolute) steemit URL... + if (!href.match(/^(\/(?!\/)|https:\/\/steemit.com)/)) { + // attys.target = '_blank' // pending iframe impl https://mathiasbynens.github.io/rel-noopener/ + attys.rel = highQualityPost ? 'noopener' : 'nofollow noopener' + } + return { + tagName, + attribs: attys + } + }, + } +}) diff --git a/src/app/utils/ServerApiClient.js b/src/app/utils/ServerApiClient.js new file mode 100644 index 0000000..d4bae6d --- /dev/null +++ b/src/app/utils/ServerApiClient.js @@ -0,0 +1,101 @@ +import {NTYPES, notificationsArrayToMap} from 'app/utils/Notifications'; +import {api} from 'steem'; + +const request_base = { + method: 'post', + mode: 'no-cors', + credentials: 'same-origin', + headers: { + Accept: 'application/json', + 'Content-type': 'application/json' + } +}; + +export function serverApiLogin(account, signatures) { + if (!process.env.BROWSER || window.$STM_ServerBusy) return; + const request = Object.assign({}, request_base, {body: JSON.stringify({account, signatures, csrf: $STM_csrf})}); + fetch('/api/v1/login_account', request); +} + +export function serverApiLogout() { + if (!process.env.BROWSER || window.$STM_ServerBusy) return; + const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf})}); + fetch('/api/v1/logout_account', request); +} + +let last_call; +export function serverApiRecordEvent(type, val, rate_limit_ms = 5000) { + if (!process.env.BROWSER || window.$STM_ServerBusy) return; + if (last_call && (new Date() - last_call) < rate_limit_ms) return; + last_call = new Date(); + const value = val && val.stack ? `${val.toString()} | ${val.stack}` : val; + const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, type, value})}); + fetch('/api/v1/record_event', request); + api.call('overseer.collect', {collection: 'event', metadata: {type, value}}, (error) => { + // if (error) console.warn('overseer error', error, error.data); + }); +} + +export function getNotifications(account) { + if (!process.env.BROWSER || window.$STM_ServerBusy) return Promise.resolve(null); + const request = Object.assign({}, request_base, {method: 'get'}); + return fetch(`/api/v1/notifications/${account}`, request).then(r => r.json()).then(res => { + return notificationsArrayToMap(res); +}); +} + +export function markNotificationRead(account, fields) { + if (!process.env.BROWSER || window.$STM_ServerBusy) return Promise.resolve(null); + const request = Object.assign({}, request_base, {method: 'put', mode: 'cors'}); + const field_nums_str = fields.map(f => NTYPES.indexOf(f)).join('-'); + return fetch(`/api/v1/notifications/${account}/${field_nums_str}`, request).then(r => r.json()).then(res => { + return notificationsArrayToMap(res); +}); +} + +let last_page, last_views, last_page_promise; +export function recordPageView(page, ref, account) { + if (last_page_promise && page === last_page) return last_page_promise; + if (window.ga) { // virtual pageview + window.ga('set', 'page', page); + window.ga('send', 'pageview'); + } + api.call('overseer.pageview', {page, referer: ref, account}, (error) => { + // if (error) console.warn('overseer error', error, error.data); + }); + if (!process.env.BROWSER || window.$STM_ServerBusy) return Promise.resolve(0); + const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, page, ref})}); + last_page_promise = fetch(`/api/v1/page_view`, request).then(r => r.json()).then(res => { + last_views = res.views; + return last_views; +}); + last_page = page; + return last_page_promise; +} + +export function webPushRegister(account, webpush_params) { + if (!process.env.BROWSER || window.$STM_ServerBusy) return; + const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, account, webpush_params})}); + fetch('/api/v1/notifications/register', request); +} + +export function sendConfirmEmail(account) { + const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, account})}); + fetch('/api/v1/notifications/send_confirm', request); +} + +export function saveCords(x, y) { + const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: $STM_csrf, x: x, y: y})}); + fetch('/api/v1/save_cords', request); +} + +export function setUserPreferences(payload) { + if (!process.env.BROWSER || window.$STM_ServerBusy) return Promise.resolve(); + const request = Object.assign({}, request_base, {body: JSON.stringify({csrf: window.$STM_csrf, payload})}); + return fetch('/api/v1/setUserPreferences', request); +} + +if (process.env.BROWSER) { + window.getNotifications = getNotifications; + window.markNotificationRead = markNotificationRead; +} diff --git a/src/app/utils/SlateEditor/Align.js b/src/app/utils/SlateEditor/Align.js new file mode 100644 index 0000000..b1c7d15 --- /dev/null +++ b/src/app/utils/SlateEditor/Align.js @@ -0,0 +1,26 @@ +import React from 'react' + +export default class Align extends React.Component { + + getAlignClass = () => { + const {node} = this.props + switch(node.data.get('align')) { + case 'text-right': return 'text-right'; + case 'text-left': return 'text-left'; + case 'text-center': return 'text-center'; + case 'pull-right': return 'pull-right'; + case 'pull-left': return 'pull-left'; + } + } + + render = () => { + const { node, attributes, children } = this.props + const className = this.getAlignClass(); + + return ( +
      + {children} +
      + ) + } +} diff --git a/src/app/utils/SlateEditor/DemoState.js b/src/app/utils/SlateEditor/DemoState.js new file mode 100644 index 0000000..7dacf14 --- /dev/null +++ b/src/app/utils/SlateEditor/DemoState.js @@ -0,0 +1,14 @@ +export default { + "nodes": [ + { + "kind": "block", + "type": "paragraph", + "nodes": [ + { + "kind": "text", + "text": "" + }, + ] + } + ] +} diff --git a/src/app/utils/SlateEditor/HRule.js b/src/app/utils/SlateEditor/HRule.js new file mode 100644 index 0000000..774a7ef --- /dev/null +++ b/src/app/utils/SlateEditor/HRule.js @@ -0,0 +1,10 @@ +import React from 'react' + +export default class HRule extends React.Component { + render() { + const { node, state } = this.props + const isFocused = state.selection.hasEdgeIn(node) + const className = isFocused ? 'active' : null + return
      + } +} diff --git a/src/app/utils/SlateEditor/Helpers.js b/src/app/utils/SlateEditor/Helpers.js new file mode 100644 index 0000000..2e85d72 --- /dev/null +++ b/src/app/utils/SlateEditor/Helpers.js @@ -0,0 +1,18 @@ +const findParentTag = (el, tag, depth = 0) => { + if (!el) return null; + if (el.tagName == tag) return el; + return findParentTag(el.parentNode, tag, depth + 1); +} + +export const getCollapsedClientRect = () => { + const selection = document.getSelection(); + if (selection.rangeCount === 0 || !selection.getRangeAt || !selection.getRangeAt(0) || !selection.getRangeAt(0).startContainer || !selection.getRangeAt(0).startContainer.getBoundingClientRect) { + return null; + } + + const node = selection.getRangeAt(0).startContainer; + if(! findParentTag(node, 'P')) return; // only show sidebar at the beginning of an empty

      + + const rect = node.getBoundingClientRect(); + return rect; +} diff --git a/src/app/utils/SlateEditor/Iframe.js b/src/app/utils/SlateEditor/Iframe.js new file mode 100644 index 0000000..f1dd15e --- /dev/null +++ b/src/app/utils/SlateEditor/Iframe.js @@ -0,0 +1,114 @@ +import React from 'react' +import linksRe from 'app/utils/Links' + +export default class Iframe extends React.Component { + + normalizeEmbedUrl = (url) => { + let match; + + // Detect youtube URLs + match = url.match(linksRe.youTubeId) + if(match && match.length >= 2) { + return 'https://www.youtube.com/embed/' + match[1] + } + + // Detect vimeo + match = url.match(linksRe.vimeoId) + if(match && match.length >= 2) { + return 'https://player.vimeo.com/video/' + match[1] + } + + console.log("unable to auto-detect embed url", url) + return null + } + + onChange = (e) => { + const { node, state, editor } = this.props + const value = e.target.value + + const src = this.normalizeEmbedUrl(value) || value + + const next = editor + .getState() + .transform() + .setNodeByKey(node.key, {data: {src}}) + .apply() + + editor.onChange(next) + } + + onClick = (e) => { + // stop propagation so that the void node itself isn't focused, since that would unfocus the input. + e.stopPropagation() + } + + render = () => { + const { node, state, attributes } = this.props + const isFocused = state.selection.hasEdgeIn(node) + const className = isFocused ? 'active' : null + + const lockStyle = { + position: 'absolute', + top: '0px', + left: '0px', + width: '100%', + height: '100%', + background: 'rgba(0,0,0,0.1)', + } + + return ( +

      +
      + {this.renderFrame()} +
      + {isFocused && {this.renderInput()}} +
      +
      +
      + ) + } + + renderFrame = () => { + let src = this.props.node.data.get('src') + src = this.normalizeEmbedUrl(src) || src + + return ( +