diff --git a/client/lib/main.less b/client/lib/main.less
index 5b28a5c8..770ff250 100644
--- a/client/lib/main.less
+++ b/client/lib/main.less
@@ -1345,6 +1345,39 @@ iframe.js {
opacity: 0;
margin-left: 0;
}
+
+ .inline-voting {
+ .approve, .disapprove {
+ margin-left: 0.2em;
+ padding: 0 0.2em;
+ border-radius: 2px;
+ }
+
+ .approve {
+ background: #e0ffe0;
+ color: #008000;
+ .touchable-bg(#e0ffe0);
+ }
+
+ .disapprove {
+ background: #ffe0e0;
+ color: #800000;
+ .touchable-bg(#ffe0e0);
+ }
+
+ .approved {
+ color: #008000;
+ }
+
+ .rejected {
+ color: #800000;
+ }
+
+ small, small * {
+ vertical-align: baseline;
+ color: #808080;
+ }
+ }
}
.message-tall {
diff --git a/client/lib/ui/InlineVoting.js b/client/lib/ui/InlineVoting.js
new file mode 100644
index 00000000..53e39fff
--- /dev/null
+++ b/client/lib/ui/InlineVoting.js
@@ -0,0 +1,71 @@
+import React from 'react'
+import Reflux from 'reflux'
+import Immutable from 'immutable'
+
+import actions from '../actions'
+import chat from '../stores/chat'
+import Tree from '../Tree'
+import FastButton from './FastButton'
+import MessageText from './MessageText'
+
+export default React.createClass({
+ displayName: 'InlineVoting',
+
+ propTypes: {
+ message: React.PropTypes.instanceOf(Immutable.Map).isRequired,
+ tree: React.PropTypes.instanceOf(Tree).isRequired,
+ className: React.PropTypes.string,
+ title: React.PropTypes.string,
+ style: React.PropTypes.string,
+ },
+
+ mixins: [
+ Reflux.connect(chat.store, 'chat'),
+ ],
+
+ sendMessageIfPossible(text) {
+ if (this.state.chat.joined && this.state.chat.nick) {
+ actions.sendMessage(text, this.props.message.get('id'))
+ }
+ },
+
+ upvote(evt) {
+ this.sendMessageIfPossible('+1')
+ if (evt) evt.stopPropagation()
+ },
+
+ downvote(evt) {
+ this.sendMessageIfPossible('-1')
+ if (evt) evt.stopPropagation()
+ },
+
+ render() {
+ let upvotes = 0
+ let downvotes = 0
+
+ this.props.message.get('children').map(id => {
+ const content = this.props.tree.get(id).get('content')
+
+ if (/\s*\+1\s*/.test(content)) upvotes++
+ if (/\s*-1\s*/.test(content)) downvotes++
+ })
+
+ const result = upvotes - downvotes
+ const resultClass = (result > 0) ? 'approved' : (result < 0) ? 'rejected' : 'neutral' // eslint-disable-line
+
+ const majorityPercent = Math.max(upvotes, downvotes) * 100 / (upvotes + downvotes)
+ const percentText = ' (' + Math.round(majorityPercent) + '% ' + ((result > 0) ? '+' : '-') + ')'
+
+ return (
+