diff --git a/.gitignore b/.gitignore
index 6f3ac70b8..6f3104629 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,17 @@ plugins/*
!plugins/talk-plugin-remember-sort
!plugins/talk-plugin-respect
!plugins/talk-plugin-slack-notifications
+!plugins/talk-plugin-sort-most-downvoted
!plugins/talk-plugin-sort-most-liked
!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
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..56f43029d
--- /dev/null
+++ b/plugins/talk-plugin-downvote/client/components/Icon.js
@@ -0,0 +1,10 @@
+import React from 'react';
+import cn from 'classnames';
+
+// @TODO change icon when we deprecate FA
+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..dc60178db
--- /dev/null
+++ b/plugins/talk-plugin-downvote/client/index.js
@@ -0,0 +1,7 @@
+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-downvote/package.json b/plugins/talk-plugin-downvote/package.json
new file mode 100644
index 000000000..440d18b6a
--- /dev/null
+++ b/plugins/talk-plugin-downvote/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@coralproject/talk-plugin-downvote",
+ "pluginName": "talk-plugin-downvote",
+ "version": "0.0.1",
+ "description": "Downvote comments",
+ "main": "index.js",
+ "author": "The Coral Project Team ",
+ "license": "Apache-2.0"
+}
diff --git a/plugins/talk-plugin-respect/client/Icon.js b/plugins/talk-plugin-respect/client/Icon.js
index 8b45615aa..9af30b86a 100644
--- a/plugins/talk-plugin-respect/client/Icon.js
+++ b/plugins/talk-plugin-respect/client/Icon.js
@@ -1,6 +1,7 @@
import React from 'react';
import cn from 'classnames';
+// @TODO change icon when we deprecate FA
export default ({ className }) => (
);
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-downvoted/package.json b/plugins/talk-plugin-sort-most-downvoted/package.json
new file mode 100644
index 000000000..5f79e0de9
--- /dev/null
+++ b/plugins/talk-plugin-sort-most-downvoted/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@coralproject/talk-plugin-sort-most-downvoted",
+ "pluginName": "talk-plugin-sort-most-downvoted",
+ "version": "0.0.1",
+ "description": "Sort by most downvotes",
+ "main": "index.js",
+ "author": "The Coral Project Team ",
+ "license": "Apache-2.0"
+}
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-sort-most-upvoted/package.json b/plugins/talk-plugin-sort-most-upvoted/package.json
new file mode 100644
index 000000000..221e22c22
--- /dev/null
+++ b/plugins/talk-plugin-sort-most-upvoted/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@coralproject/talk-plugin-sort-most-upvoted",
+ "pluginName": "talk-plugin-sort-most-upvoted",
+ "version": "0.0.1",
+ "description": "Sort by most upvotes",
+ "main": "index.js",
+ "author": "The Coral Project Team ",
+ "license": "Apache-2.0"
+}
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..1b34c40cd
--- /dev/null
+++ b/plugins/talk-plugin-upvote/client/components/Icon.js
@@ -0,0 +1,7 @@
+import React from 'react';
+import cn from 'classnames';
+
+// @TODO change icon when we deprecate FA
+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..fd81298da
--- /dev/null
+++ b/plugins/talk-plugin-upvote/client/index.js
@@ -0,0 +1,7 @@
+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');
diff --git a/plugins/talk-plugin-upvote/package.json b/plugins/talk-plugin-upvote/package.json
new file mode 100644
index 000000000..bf32d911a
--- /dev/null
+++ b/plugins/talk-plugin-upvote/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@coralproject/talk-plugin-upvote",
+ "pluginName": "talk-plugin-upvote",
+ "version": "0.0.1",
+ "description": "Upvote comments",
+ "main": "index.js",
+ "author": "The Coral Project Team ",
+ "license": "Apache-2.0"
+}