diff --git a/extensions/likes/.editorconfig b/extensions/likes/.editorconfig new file mode 100644 index 000000000..5612a5e74 --- /dev/null +++ b/extensions/likes/.editorconfig @@ -0,0 +1,32 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +[*.js] +indent_style = space +indent_size = 2 + +[*.{css,less}] +indent_style = space +indent_size = 2 + +[*.html] +indent_style = space +indent_size = 2 + +[*.{diff,md}] +trim_trailing_whitespace = false + +[*.php] +indent_style = space +indent_size = 4 diff --git a/extensions/likes/.eslintignore b/extensions/likes/.eslintignore new file mode 100644 index 000000000..86b7c8854 --- /dev/null +++ b/extensions/likes/.eslintignore @@ -0,0 +1,5 @@ +**/bower_components/**/* +**/node_modules/**/* +vendor/**/* +**/Gulpfile.js +**/dist/**/* diff --git a/extensions/likes/.eslintrc b/extensions/likes/.eslintrc new file mode 100644 index 000000000..9cebc759d --- /dev/null +++ b/extensions/likes/.eslintrc @@ -0,0 +1,171 @@ +{ + "parser": "babel-eslint", // https://github.com/babel/babel-eslint + "env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments + "browser": true // browser global variables + }, + "ecmaFeatures": { + "arrowFunctions": true, + "blockBindings": true, + "classes": true, + "defaultParams": true, + "destructuring": true, + "forOf": true, + "generators": false, + "modules": true, + "objectLiteralComputedProperties": true, + "objectLiteralDuplicateProperties": false, + "objectLiteralShorthandMethods": true, + "objectLiteralShorthandProperties": true, + "spread": true, + "superInFunctions": true, + "templateStrings": true, + "jsx": true + }, + "globals": { + "m": true, + "app": true, + "$": true, + "moment": true + }, + "rules": { +/** + * Strict mode + */ + // babel inserts "use strict"; for us + "strict": [2, "never"], // http://eslint.org/docs/rules/strict + +/** + * ES6 + */ + "no-var": 2, // http://eslint.org/docs/rules/no-var + "prefer-const": 2, // http://eslint.org/docs/rules/prefer-const + +/** + * Variables + */ + "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow + "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names + "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars + "vars": "local", + "args": "after-used" + }], + "no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define + +/** + * Possible errors + */ + "comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle + "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign + "no-console": 1, // http://eslint.org/docs/rules/no-console + "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger + "no-alert": 1, // http://eslint.org/docs/rules/no-alert + "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition + "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys + "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case + "no-empty": 2, // http://eslint.org/docs/rules/no-empty + "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign + "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast + "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi + "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign + "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations + "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp + "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace + "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls + "no-reserved-keys": 2, // http://eslint.org/docs/rules/no-reserved-keys + "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays + "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable + "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan + "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var + +/** + * Best practices + */ + "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return + "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly + "default-case": 2, // http://eslint.org/docs/rules/default-case + "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation + "allowKeywords": true + }], + "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq + "no-caller": 2, // http://eslint.org/docs/rules/no-caller + "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return + "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null + "no-eval": 2, // http://eslint.org/docs/rules/no-eval + "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native + "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind + "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough + "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal + "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval + "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks + "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func + "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str + "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign + "no-new": 2, // http://eslint.org/docs/rules/no-new + "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func + "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers + "no-octal": 2, // http://eslint.org/docs/rules/no-octal + "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape + "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign + "no-proto": 2, // http://eslint.org/docs/rules/no-proto + "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare + "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign + "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare + "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences + "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal + "no-with": 2, // http://eslint.org/docs/rules/no-with + "radix": 2, // http://eslint.org/docs/rules/radix + "vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top + "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife + "yoda": 2, // http://eslint.org/docs/rules/yoda + +/** + * Style + */ + "indent": [2, 2], // http://eslint.org/docs/rules/indent + "brace-style": [2, // http://eslint.org/docs/rules/brace-style + "1tbs", { + "allowSingleLine": true + }], + "quotes": [ + 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes + ], + "camelcase": [2, { // http://eslint.org/docs/rules/camelcase + "properties": "never" + }], + "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing + "before": false, + "after": true + }], + "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style + "eol-last": 2, // http://eslint.org/docs/rules/eol-last + "func-names": 1, // http://eslint.org/docs/rules/func-names + "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing + "beforeColon": false, + "afterColon": true + }], + "new-cap": [2, { // http://eslint.org/docs/rules/new-cap + "newIsCap": true + }], + "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines + "max": 2 + }], + "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object + "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func + "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces + "no-wrap-func": 2, // http://eslint.org/docs/rules/no-wrap-func + "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle + "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var + "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks + "semi": [2, "always"], // http://eslint.org/docs/rules/semi + "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing + "before": false, + "after": true + }], + "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords + "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks + "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren + "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops + "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case + "spaced-line-comment": 2, // http://eslint.org/docs/rules/spaced-line-comment + } +} diff --git a/extensions/likes/bootstrap.php b/extensions/likes/bootstrap.php index b205492d2..15cfc3883 100644 --- a/extensions/likes/bootstrap.php +++ b/extensions/likes/bootstrap.php @@ -1,9 +1,5 @@ app->register('Flarum\Likes\LikesServiceProvider'); +return 'Flarum\Likes\Extension'; diff --git a/extensions/likes/flarum.json b/extensions/likes/flarum.json index 7d4a6a284..5cc2f97de 100644 --- a/extensions/likes/flarum.json +++ b/extensions/likes/flarum.json @@ -1,16 +1,21 @@ { - "name": "flarum-likes", + "name": "likes", "title": "Likes", "description": "Allows users to like posts.", - "tags": [], + "keywords": ["discussions"], "version": "0.1.0", "author": { "name": "Toby Zerner", - "email": "toby@flarum.org'" + "email": "toby@flarum.org", + "homepage": "http://tobyzerner.com" }, "license": "MIT", "require": { "php": ">=5.4.0", "flarum": ">0.1.0" + }, + "support": { + "source": "https://github.com/flarum/likes", + "issues": "https://github.com/flarum/likes/issues" } -} \ No newline at end of file +} diff --git a/extensions/likes/js/bootstrap.js b/extensions/likes/js/bootstrap.js deleted file mode 100644 index 440149814..000000000 --- a/extensions/likes/js/bootstrap.js +++ /dev/null @@ -1,109 +0,0 @@ -import { extend, override } from 'flarum/extension-utils'; -import app from 'flarum/app'; -import Post from 'flarum/models/post'; -import Model from 'flarum/model'; -import DiscussionPage from 'flarum/components/discussion-page'; -import SettingsPage from 'flarum/components/settings-page'; -import ActionButton from 'flarum/components/action-button'; -import CommentPost from 'flarum/components/comment-post'; -import punctuate from 'flarum/helpers/punctuate'; -import username from 'flarum/helpers/username'; -import icon from 'flarum/helpers/icon'; - -import PostLikedNotification from 'flarum-likes/components/post-liked-notification'; -import PostLikesModal from 'flarum-likes/components/post-likes-modal'; - -app.initializers.add('flarum-likes', function() { - - app.notificationComponentRegistry['postLiked'] = PostLikedNotification; - - Post.prototype.canLike = Model.prop('canLike'); - Post.prototype.likes = Model.many('likes'); - - extend(DiscussionPage.prototype, 'params', function(params) { - params.include.push('posts.likes'); - }); - - extend(CommentPost.prototype, 'footerItems', function(items) { - var post = this.props.post; - var likes = post.likes(); - - if (likes && likes.length) { - - var limit = 3; - - var names = likes.slice(0, limit).map(user => { - return m('a', { - href: app.route.user(user), - config: m.route - }, [ - app.session.user() && user === app.session.user() ? 'You' : username(user) - ]) - }); - - if (likes.length > limit + 1) { - names.push( - m('a', { - href: '#', - onclick: function(e) { - e.preventDefault(); - app.modal.show(new PostLikesModal({ post })); - } - }, (likes.length - limit)+' others') - ); - } - - items.add('liked', - m('div.liked-by', [ - icon('thumbs-o-up icon'), - punctuate(names), - names.length === 1 && (!app.session.user() || likes[0] !== app.session.user()) ? ' likes this.' : ' like this.' - ]), - {before: 'replies'} - ); - } - }); - - extend(CommentPost.prototype, 'actionItems', function(items) { - var post = this.props.post; - if (post.isHidden() || !post.canLike()) return; - - var isLiked = app.session.user() && post.likes().some(user => user === app.session.user()); - - items.add('like', - ActionButton.component({ - icon: 'thumbs-o-up', - label: isLiked ? 'Unlike' : 'Like', - onclick: () => { - isLiked = !isLiked; - - post.save({ isLiked }); - - var linkage = post.data().links.likes.linkage; - linkage.some((like, i) => { - if (like.id == app.session.user().id()) { - linkage.splice(i, 1); - return true; - } - }); - - if (isLiked) { - linkage.unshift({ type: 'users', id: app.session.user().id() }); - } - - m.redraw(); - } - }), - {before: 'reply'} - ); - }); - - // Add a notification preference. - extend(SettingsPage.prototype, 'notificationTypes', function(items) { - items.add('postLiked', { - name: 'postLiked', - label: [icon('thumbs-o-up'), ' Someone likes my post'] - }); - }); - -}); diff --git a/extensions/likes/js/Gulpfile.js b/extensions/likes/js/forum/Gulpfile.js similarity index 60% rename from extensions/likes/js/Gulpfile.js rename to extensions/likes/js/forum/Gulpfile.js index 7f1b72f96..5687a2f40 100644 --- a/extensions/likes/js/Gulpfile.js +++ b/extensions/likes/js/forum/Gulpfile.js @@ -1,5 +1,5 @@ var gulp = require('flarum-gulp'); gulp({ - modulePrefix: 'flarum-likes' + modulePrefix: 'likes' }); diff --git a/extensions/likes/js/package.json b/extensions/likes/js/forum/package.json similarity index 100% rename from extensions/likes/js/package.json rename to extensions/likes/js/forum/package.json diff --git a/extensions/likes/js/forum/src/addLikeAction.js b/extensions/likes/js/forum/src/addLikeAction.js new file mode 100644 index 000000000..5ae0a5bfc --- /dev/null +++ b/extensions/likes/js/forum/src/addLikeAction.js @@ -0,0 +1,41 @@ +import { extend } from 'flarum/extend'; +import app from 'flarum/app'; +import Button from 'flarum/components/Button'; +import CommentPost from 'flarum/components/CommentPost'; + +export default function() { + extend(CommentPost.prototype, 'actionItems', function(items) { + const post = this.props.post; + + if (post.isHidden() || !post.canLike()) return; + + let isLiked = app.session.user && post.likes().some(user => user === app.session.user); + + items.add('like', + Button.component({ + children: app.trans(isLiked ? 'likes.unlike_action' : 'likes.like_action'), + className: 'Button Button--text', + onclick: () => { + isLiked = !isLiked; + + post.save({isLiked}); + + // We've saved the fact that we do or don't like the post, but in order + // to provide instantaneous feedback to the user, we'll need to add or + // remove the like from the relationship data manually. + const data = post.data.relationships.likes.data; + data.some((like, i) => { + if (like.id === app.session.user.id()) { + data.splice(i, 1); + return true; + } + }); + + if (isLiked) { + data.unshift({type: 'users', id: app.session.user.id()}); + } + } + }) + ); + }); +} diff --git a/extensions/likes/js/forum/src/addLikesList.js b/extensions/likes/js/forum/src/addLikesList.js new file mode 100644 index 000000000..e4581af69 --- /dev/null +++ b/extensions/likes/js/forum/src/addLikesList.js @@ -0,0 +1,60 @@ +import { extend } from 'flarum/extend'; +import app from 'flarum/app'; +import DiscussionPage from 'flarum/components/DiscussionPage'; +import CommentPost from 'flarum/components/CommentPost'; +import punctuate from 'flarum/helpers/punctuate'; +import username from 'flarum/helpers/username'; +import icon from 'flarum/helpers/icon'; + +import PostLikesModal from 'likes/components/PostLikesModal'; + +export default function() { + extend(DiscussionPage.prototype, 'params', function(params) { + params.include.push('posts.likes'); + }); + + extend(CommentPost.prototype, 'footerItems', function(items) { + const post = this.props.post; + const likes = post.likes(); + + if (likes && likes.length) { + const limit = 3; + + // Construct a list of names of users who have like this post. Make sure the + // current user is first in the list, and cap a maximum of 3 names. + const names = likes.sort(a => a === app.session.user ? -1 : 1) + .slice(0, limit) + .map(user => { + return ( + + {user === app.session.user ? 'You' : username(user)} + + ); + }); + + // If there are more users that we've run out of room to display, add a "x + // others" name to the end of the list. Clicking on it will display a modal + // with a full list of names. + if (likes.length > limit + 1) { + names.push( + { + e.preventDefault(); + app.modal.show(new PostLikesModal({post})); + }}> + {app.trans('likes.others', {count: likes.length - limit})} + + ); + } + + items.add('liked', ( +