diff --git a/.gitignore b/.gitignore
index 7c0007dc9..50e490095 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,7 @@ plugins/*
!plugins/talk-plugin-author-menu
!plugins/talk-plugin-comment-content
!plugins/talk-plugin-deep-reply-count
+!plugins/talk-plugin-downvote
!plugins/talk-plugin-facebook-auth
!plugins/talk-plugin-featured-comments
!plugins/talk-plugin-flag-details
@@ -52,14 +53,16 @@ plugins/*
!plugins/talk-plugin-remember-sort
!plugins/talk-plugin-respect
!plugins/talk-plugin-slack-notifications
-!plugins/talk-plugin-sort-most-liked
+!plugins/talk-plugin-sort-most-downvoted
!plugins/talk-plugin-sort-most-loved
!plugins/talk-plugin-sort-most-replied
!plugins/talk-plugin-sort-most-respected
+!plugins/talk-plugin-sort-most-upvoted
!plugins/talk-plugin-sort-newest
!plugins/talk-plugin-sort-oldest
!plugins/talk-plugin-subscriber
!plugins/talk-plugin-toxic-comments
+!plugins/talk-plugin-upvote
!plugins/talk-plugin-viewing-options
!plugins/talk-plugin-rich-text
!plugins/talk-plugin-rich-text-pell
diff --git a/plugins/talk-plugin-downvote/client/.eslintrc.json b/plugins/talk-plugin-downvote/client/.eslintrc.json
new file mode 100644
index 000000000..9fe56bd14
--- /dev/null
+++ b/plugins/talk-plugin-downvote/client/.eslintrc.json
@@ -0,0 +1,23 @@
+{
+ "env": {
+ "browser": true,
+ "es6": true,
+ "mocha": true
+ },
+ "parserOptions": {
+ "sourceType": "module",
+ "ecmaFeatures": {
+ "experimentalObjectRestSpread": true,
+ "jsx": true
+ }
+ },
+ "parser": "babel-eslint",
+ "plugins": [
+ "react"
+ ],
+ "rules": {
+ "react/jsx-uses-react": "error",
+ "react/jsx-uses-vars": "error",
+ "no-console": ["warn", { "allow": ["warn", "error"] }]
+ }
+}
diff --git a/plugins/talk-plugin-downvote/client/components/DownvoteButton.css b/plugins/talk-plugin-downvote/client/components/DownvoteButton.css
new file mode 100644
index 000000000..71090306b
--- /dev/null
+++ b/plugins/talk-plugin-downvote/client/components/DownvoteButton.css
@@ -0,0 +1,39 @@
+.container {
+ display: inline-block;
+}
+
+.button {
+ color: #2a2a2a;
+ margin: 5px 10px 5px 0px;
+ background: none;
+ padding: 0px;
+ border: none;
+ font-size: inherit;
+ vertical-align: middle;
+
+ &:hover {
+ color: #767676;
+ cursor: pointer;
+ }
+
+ &.downvoted {
+ color: #cc0000;
+
+ &:hover {
+ color: #ff3232;
+ cursor: pointer;
+ }
+ }
+}
+
+.icon {
+ font-size: 12px;
+ padding: 0 3px;
+}
+
+@media (max-width: 425px) {
+ .label {
+ display: none;
+ }
+}
+
diff --git a/plugins/talk-plugin-downvote/client/components/DownvoteButton.js b/plugins/talk-plugin-downvote/client/components/DownvoteButton.js
new file mode 100644
index 000000000..2bf34260b
--- /dev/null
+++ b/plugins/talk-plugin-downvote/client/components/DownvoteButton.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import Icon from './Icon';
+import styles from './DownvoteButton.css';
+import { withReaction } from 'plugin-api/beta/client/hocs';
+import cn from 'classnames';
+
+const plugin = 'talk-plugin-downvote';
+
+class DownvoteButton extends React.Component {
+ handleClick = () => {
+ const {
+ postReaction,
+ deleteReaction,
+ showSignInDialog,
+ alreadyReacted,
+ user,
+ } = this.props;
+
+ // If the current user does not exist, trigger sign in dialog.
+ if (!user) {
+ showSignInDialog();
+ return;
+ }
+
+ if (alreadyReacted) {
+ deleteReaction();
+ } else {
+ postReaction();
+ }
+ };
+
+ render() {
+ const { count, alreadyReacted } = this.props;
+ return (
+
+
+
+ );
+ }
+}
+
+export default withReaction('downvote')(DownvoteButton);
diff --git a/plugins/talk-plugin-downvote/client/components/Icon.js b/plugins/talk-plugin-downvote/client/components/Icon.js
new file mode 100644
index 000000000..13d91fa9c
--- /dev/null
+++ b/plugins/talk-plugin-downvote/client/components/Icon.js
@@ -0,0 +1,9 @@
+import React from 'react';
+import cn from 'classnames';
+
+export default ({ className }) => (
+
+);
diff --git a/plugins/talk-plugin-downvote/client/index.js b/plugins/talk-plugin-downvote/client/index.js
new file mode 100644
index 000000000..11eee3bda
--- /dev/null
+++ b/plugins/talk-plugin-downvote/client/index.js
@@ -0,0 +1,25 @@
+/**
+ This is a client index example file and it could look like this:
+
+ ```
+ import LoveButton from './components/LoveButton';
+
+ export default {
+ slots: {
+ commentReactions: [LoveButton]
+ },
+ reducer,
+ translations
+ };
+ ```
+
+ To read more info on how to build client plugins. Please, go to: https://docs.coralproject.net/talk/plugins-client
+ */
+
+import DownvoteButton from './components/DownvoteButton';
+
+export default {
+ slots: {
+ commentReactions: [DownvoteButton],
+ },
+};
diff --git a/plugins/talk-plugin-downvote/index.js b/plugins/talk-plugin-downvote/index.js
new file mode 100644
index 000000000..c0e29c5ef
--- /dev/null
+++ b/plugins/talk-plugin-downvote/index.js
@@ -0,0 +1,2 @@
+const { getReactionConfig } = require('../../plugin-api/beta/server');
+module.exports = getReactionConfig('downvote');
diff --git a/plugins/talk-plugin-sort-most-downvoted/client/.eslintrc.json b/plugins/talk-plugin-sort-most-downvoted/client/.eslintrc.json
new file mode 100644
index 000000000..c8a6db18a
--- /dev/null
+++ b/plugins/talk-plugin-sort-most-downvoted/client/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "@coralproject/eslint-config-talk/client"
+}
diff --git a/plugins/talk-plugin-sort-most-downvoted/client/index.js b/plugins/talk-plugin-sort-most-downvoted/client/index.js
new file mode 100644
index 000000000..fb6ad544c
--- /dev/null
+++ b/plugins/talk-plugin-sort-most-downvoted/client/index.js
@@ -0,0 +1,19 @@
+import translations from './translations.yml';
+import { createSortOption } from 'talk-plugin-viewing-options/client/api/factories';
+import { t } from 'plugin-api/beta/client/services';
+
+const SortOption = createSortOption(
+ () => t('talk-plugin-sort-most-downvoted.label'),
+ { sortBy: 'DOWNVOTES', sortOrder: 'DESC' }
+);
+
+/**
+ * This plugin depends on talk-plugin-viewing-options.
+ */
+
+export default {
+ translations,
+ slots: {
+ viewingOptionsSort: [SortOption],
+ },
+};
diff --git a/plugins/talk-plugin-sort-most-downvoted/client/translations.yml b/plugins/talk-plugin-sort-most-downvoted/client/translations.yml
new file mode 100644
index 000000000..3bff2eed7
--- /dev/null
+++ b/plugins/talk-plugin-sort-most-downvoted/client/translations.yml
@@ -0,0 +1,3 @@
+en:
+ talk-plugin-sort-most-downvoted:
+ label: Most downvoted first
diff --git a/plugins/talk-plugin-sort-most-downvoted/index.js b/plugins/talk-plugin-sort-most-downvoted/index.js
new file mode 100644
index 000000000..f053ebf79
--- /dev/null
+++ b/plugins/talk-plugin-sort-most-downvoted/index.js
@@ -0,0 +1 @@
+module.exports = {};
diff --git a/plugins/talk-plugin-sort-most-upvoted/client/.eslintrc.json b/plugins/talk-plugin-sort-most-upvoted/client/.eslintrc.json
new file mode 100644
index 000000000..c8a6db18a
--- /dev/null
+++ b/plugins/talk-plugin-sort-most-upvoted/client/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "@coralproject/eslint-config-talk/client"
+}
diff --git a/plugins/talk-plugin-sort-most-upvoted/client/index.js b/plugins/talk-plugin-sort-most-upvoted/client/index.js
new file mode 100644
index 000000000..26744b6d6
--- /dev/null
+++ b/plugins/talk-plugin-sort-most-upvoted/client/index.js
@@ -0,0 +1,19 @@
+import translations from './translations.yml';
+import { createSortOption } from 'talk-plugin-viewing-options/client/api/factories';
+import { t } from 'plugin-api/beta/client/services';
+
+const SortOption = createSortOption(
+ () => t('talk-plugin-sort-most-upvoted.label'),
+ { sortBy: 'UPVOTES', sortOrder: 'DESC' }
+);
+
+/**
+ * This plugin depends on talk-plugin-viewing-options.
+ */
+
+export default {
+ translations,
+ slots: {
+ viewingOptionsSort: [SortOption],
+ },
+};
diff --git a/plugins/talk-plugin-sort-most-upvoted/client/translations.yml b/plugins/talk-plugin-sort-most-upvoted/client/translations.yml
new file mode 100644
index 000000000..7b3c7c29f
--- /dev/null
+++ b/plugins/talk-plugin-sort-most-upvoted/client/translations.yml
@@ -0,0 +1,3 @@
+en:
+ talk-plugin-sort-most-upvoted:
+ label: Most upvoted first
diff --git a/plugins/talk-plugin-sort-most-upvoted/index.js b/plugins/talk-plugin-sort-most-upvoted/index.js
new file mode 100644
index 000000000..f053ebf79
--- /dev/null
+++ b/plugins/talk-plugin-sort-most-upvoted/index.js
@@ -0,0 +1 @@
+module.exports = {};
diff --git a/plugins/talk-plugin-upvote/client/.eslintrc.json b/plugins/talk-plugin-upvote/client/.eslintrc.json
new file mode 100644
index 000000000..9fe56bd14
--- /dev/null
+++ b/plugins/talk-plugin-upvote/client/.eslintrc.json
@@ -0,0 +1,23 @@
+{
+ "env": {
+ "browser": true,
+ "es6": true,
+ "mocha": true
+ },
+ "parserOptions": {
+ "sourceType": "module",
+ "ecmaFeatures": {
+ "experimentalObjectRestSpread": true,
+ "jsx": true
+ }
+ },
+ "parser": "babel-eslint",
+ "plugins": [
+ "react"
+ ],
+ "rules": {
+ "react/jsx-uses-react": "error",
+ "react/jsx-uses-vars": "error",
+ "no-console": ["warn", { "allow": ["warn", "error"] }]
+ }
+}
diff --git a/plugins/talk-plugin-upvote/client/components/Icon.js b/plugins/talk-plugin-upvote/client/components/Icon.js
new file mode 100644
index 000000000..b715f68f7
--- /dev/null
+++ b/plugins/talk-plugin-upvote/client/components/Icon.js
@@ -0,0 +1,6 @@
+import React from 'react';
+import cn from 'classnames';
+
+export default ({ className }) => (
+
+);
diff --git a/plugins/talk-plugin-upvote/client/components/UpvoteButton.css b/plugins/talk-plugin-upvote/client/components/UpvoteButton.css
new file mode 100644
index 000000000..56caa60b1
--- /dev/null
+++ b/plugins/talk-plugin-upvote/client/components/UpvoteButton.css
@@ -0,0 +1,39 @@
+.container {
+ display: inline-block;
+}
+
+.button {
+ color: #2a2a2a;
+ margin: 5px 10px 5px 0px;
+ background: none;
+ padding: 0px;
+ border: none;
+ font-size: inherit;
+ vertical-align: middle;
+
+ &:hover {
+ color: #767676;
+ cursor: pointer;
+ }
+
+ &.upvoted {
+ color: #008000;
+
+ &:hover {
+ color: #66b266;
+ cursor: pointer;
+ }
+ }
+}
+
+.icon {
+ font-size: 12px;
+ padding: 0 3px;
+}
+
+@media (max-width: 425px) {
+ .label {
+ display: none;
+ }
+}
+
diff --git a/plugins/talk-plugin-upvote/client/components/UpvoteButton.js b/plugins/talk-plugin-upvote/client/components/UpvoteButton.js
new file mode 100644
index 000000000..c0db1fe01
--- /dev/null
+++ b/plugins/talk-plugin-upvote/client/components/UpvoteButton.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import Icon from './Icon';
+import styles from './UpvoteButton.css';
+import { withReaction } from 'plugin-api/beta/client/hocs';
+import cn from 'classnames';
+
+const plugin = 'talk-plugin-upvote';
+
+class UpvoteButton extends React.Component {
+ handleClick = () => {
+ const {
+ postReaction,
+ deleteReaction,
+ showSignInDialog,
+ alreadyReacted,
+ user,
+ } = this.props;
+
+ // If the current user does not exist, trigger sign in dialog.
+ if (!user) {
+ showSignInDialog();
+ return;
+ }
+
+ if (alreadyReacted) {
+ deleteReaction();
+ } else {
+ postReaction();
+ }
+ };
+
+ render() {
+ const { count, alreadyReacted } = this.props;
+ return (
+
+
+
+ );
+ }
+}
+
+export default withReaction('upvote')(UpvoteButton);
diff --git a/plugins/talk-plugin-upvote/client/index.js b/plugins/talk-plugin-upvote/client/index.js
new file mode 100644
index 000000000..4f88ee194
--- /dev/null
+++ b/plugins/talk-plugin-upvote/client/index.js
@@ -0,0 +1,25 @@
+/**
+ This is a client index example file and it could look like this:
+
+ ```
+ import LoveButton from './components/LoveButton';
+
+ export default {
+ slots: {
+ commentReactions: [LoveButton]
+ },
+ reducer,
+ translations
+ };
+ ```
+
+ To read more info on how to build client plugins. Please, go to: https://docs.coralproject.net/talk/plugins-client
+ */
+
+import UpvoteButton from './components/UpvoteButton';
+
+export default {
+ slots: {
+ commentReactions: [UpvoteButton],
+ },
+};
diff --git a/plugins/talk-plugin-upvote/index.js b/plugins/talk-plugin-upvote/index.js
new file mode 100644
index 000000000..119537ab3
--- /dev/null
+++ b/plugins/talk-plugin-upvote/index.js
@@ -0,0 +1,2 @@
+const { getReactionConfig } = require('../../plugin-api/beta/server');
+module.exports = getReactionConfig('upvote');