commit f6c0f22db00784197457e23a7eaebe2e236774f0 Author: Toby Zerner Date: Tue Jul 28 15:35:55 2015 +0930 Initial commit diff --git a/extensions/lock/.editorconfig b/extensions/lock/.editorconfig new file mode 100644 index 000000000..5612a5e74 --- /dev/null +++ b/extensions/lock/.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/lock/.eslintignore b/extensions/lock/.eslintignore new file mode 100644 index 000000000..86b7c8854 --- /dev/null +++ b/extensions/lock/.eslintignore @@ -0,0 +1,5 @@ +**/bower_components/**/* +**/node_modules/**/* +vendor/**/* +**/Gulpfile.js +**/dist/**/* diff --git a/extensions/lock/.eslintrc b/extensions/lock/.eslintrc new file mode 100644 index 000000000..9cebc759d --- /dev/null +++ b/extensions/lock/.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/lock/.gitignore b/extensions/lock/.gitignore new file mode 100644 index 000000000..a4f3b125e --- /dev/null +++ b/extensions/lock/.gitignore @@ -0,0 +1,4 @@ +/vendor +composer.phar +.DS_Store +Thumbs.db diff --git a/extensions/lock/LICENSE.txt b/extensions/lock/LICENSE.txt new file mode 100644 index 000000000..aa1e5fb86 --- /dev/null +++ b/extensions/lock/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014-2015 Toby Zerner + +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/extensions/lock/bootstrap.php b/extensions/lock/bootstrap.php new file mode 100644 index 000000000..2f622b5d2 --- /dev/null +++ b/extensions/lock/bootstrap.php @@ -0,0 +1,5 @@ +=5.4.0", + "flarum": ">0.1.0" + }, + "support": { + "source": "https://github.com/flarum/lock", + "issues": "https://github.com/flarum/lock/issues" + } +} diff --git a/extensions/lock/js/.gitignore b/extensions/lock/js/.gitignore new file mode 100644 index 000000000..bae304483 --- /dev/null +++ b/extensions/lock/js/.gitignore @@ -0,0 +1,4 @@ +bower_components +node_modules +mithril.js +dist diff --git a/extensions/lock/js/forum/Gulpfile.js b/extensions/lock/js/forum/Gulpfile.js new file mode 100644 index 000000000..dcc422f3d --- /dev/null +++ b/extensions/lock/js/forum/Gulpfile.js @@ -0,0 +1,7 @@ +var gulp = require('flarum-gulp'); + +gulp({ + modules: { + 'lock': 'src/**/*.js' + } +}); diff --git a/extensions/lock/js/forum/package.json b/extensions/lock/js/forum/package.json new file mode 100644 index 000000000..3e0ef919d --- /dev/null +++ b/extensions/lock/js/forum/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "devDependencies": { + "gulp": "^3.8.11", + "flarum-gulp": "git+https://github.com/flarum/gulp.git" + } +} diff --git a/extensions/lock/js/forum/src/addLockBadge.js b/extensions/lock/js/forum/src/addLockBadge.js new file mode 100644 index 000000000..75482c851 --- /dev/null +++ b/extensions/lock/js/forum/src/addLockBadge.js @@ -0,0 +1,15 @@ +import { extend } from 'flarum/extend'; +import Discussion from 'flarum/models/Discussion'; +import Badge from 'flarum/components/Badge'; + +export default function addLockBadge() { + extend(Discussion.prototype, 'badges', function(badges) { + if (this.isLocked()) { + badges.add('locked', Badge.component({ + type: 'locked', + label: app.trans('lock.locked'), + icon: 'lock' + })); + } + }); +} diff --git a/extensions/lock/js/forum/src/addLockControl.js b/extensions/lock/js/forum/src/addLockControl.js new file mode 100644 index 000000000..5ee683411 --- /dev/null +++ b/extensions/lock/js/forum/src/addLockControl.js @@ -0,0 +1,26 @@ +import { extend } from 'flarum/extend'; +import DiscussionControls from 'flarum/utils/DiscussionControls'; +import DiscussionPage from 'flarum/components/DiscussionPage'; +import Button from 'flarum/components/Button'; + +export default function addLockControl() { + extend(DiscussionControls, 'moderationControls', function(items, discussion) { + if (discussion.canSticky()) { + items.add('lock', Button.component({ + children: app.trans(discussion.isLocked() ? 'lock.unlock' : 'lock.lock'), + icon: 'lock', + onclick: this.lockAction.bind(discussion) + })); + } + }); + + DiscussionControls.lockAction = function() { + this.save({isLocked: !this.isLocked()}).then(() => { + if (app.current instanceof DiscussionPage) { + app.current.stream.update(); + } + + m.redraw(); + }); + }; +} diff --git a/extensions/lock/js/forum/src/components/DiscussionLockedNotification.js b/extensions/lock/js/forum/src/components/DiscussionLockedNotification.js new file mode 100644 index 000000000..83b9bffa1 --- /dev/null +++ b/extensions/lock/js/forum/src/components/DiscussionLockedNotification.js @@ -0,0 +1,17 @@ +import Notification from 'flarum/components/Notification'; + +export default class DiscussionLockedNotification extends Notification { + icon() { + return 'lock'; + } + + href() { + const notification = this.props.notification; + + return app.route.discussion(notification.subject(), notification.content().postNumber); + } + + content() { + return app.trans('lock.discussion_locked_notification', {user: this.props.notification.sender()}); + } +} diff --git a/extensions/lock/js/forum/src/components/DiscussionLockedPost.js b/extensions/lock/js/forum/src/components/DiscussionLockedPost.js new file mode 100644 index 000000000..be2c628bd --- /dev/null +++ b/extensions/lock/js/forum/src/components/DiscussionLockedPost.js @@ -0,0 +1,15 @@ +import EventPost from 'flarum/components/EventPost'; + +export default class DiscussionLockedPost extends EventPost { + icon() { + return this.props.post.content().locked + ? 'lock' + : 'unlock'; + } + + descriptionKey() { + return this.props.post.content().locked + ? 'lock.discussion_locked_post' + : 'lock.discussion_unlocked_post'; + } +} diff --git a/extensions/lock/js/forum/src/main.js b/extensions/lock/js/forum/src/main.js new file mode 100644 index 000000000..f0e63d88e --- /dev/null +++ b/extensions/lock/js/forum/src/main.js @@ -0,0 +1,27 @@ +import { extend } from 'flarum/extend'; +import app from 'flarum/app'; +import Model from 'flarum/Model'; +import Discussion from 'flarum/models/Discussion'; +import NotificationGrid from 'flarum/components/NotificationGrid'; + +import DiscussionLockedPost from 'lock/components/DiscussionLockedPost'; +import DiscussionLockedNotification from 'lock/components/DiscussionLockedNotification'; +import addLockBadge from 'lock/addLockBadge'; +import addLockControl from 'lock/addLockControl'; + +app.postComponents.discussionLocked = DiscussionLockedPost; +app.notificationComponents.discussionLocked = DiscussionLockedNotification; + +Discussion.prototype.isLocked = Model.attribute('isLocked'); +Discussion.prototype.canLock = Model.attribute('canLock'); + +addLockBadge(); +addLockControl(); + +extend(NotificationGrid.prototype, 'notificationTypes', function(items) { + items.add('discussionLocked', { + name: 'discussionLocked', + icon: 'thumb-tack', + label: app.trans('lock.notify_discussion_locked') + }); +}); diff --git a/extensions/lock/less/forum/extension.less b/extensions/lock/less/forum/extension.less new file mode 100644 index 000000000..59d9575cf --- /dev/null +++ b/extensions/lock/less/forum/extension.less @@ -0,0 +1,10 @@ +.Badge--locked { + background: #888; +} +.DiscussionLockedPost { + .EventPost-icon, + .EventPost-info, + .EventPost-info a { + color: #888; + } +} diff --git a/extensions/lock/locale/en.yml b/extensions/lock/locale/en.yml new file mode 100644 index 000000000..bbd202e57 --- /dev/null +++ b/extensions/lock/locale/en.yml @@ -0,0 +1,8 @@ +lock: + discussion_locked_notification: "{username} locked" + discussion_locked_post: "{username} locked the discussion." + discussion_unlocked_post: "{username} unlocked the discussion." + notify_discussion_locked: Someone locks a discussion I started + locked: Locked + lock: Lock + unlock: Unlock diff --git a/extensions/lock/migrations/2015_02_24_000000_add_locked_to_discussions.php b/extensions/lock/migrations/2015_02_24_000000_add_locked_to_discussions.php new file mode 100644 index 000000000..d4f9ae535 --- /dev/null +++ b/extensions/lock/migrations/2015_02_24_000000_add_locked_to_discussions.php @@ -0,0 +1,31 @@ +boolean('is_locked')->default(0); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('discussions', function (Blueprint $table) { + $table->dropColumn('is_locked'); + }); + } +} diff --git a/extensions/lock/src/Events/DiscussionWasLocked.php b/extensions/lock/src/Events/DiscussionWasLocked.php new file mode 100644 index 000000000..c0cf2898d --- /dev/null +++ b/extensions/lock/src/Events/DiscussionWasLocked.php @@ -0,0 +1,27 @@ +discussion = $discussion; + $this->user = $user; + } +} diff --git a/extensions/lock/src/Events/DiscussionWasUnlocked.php b/extensions/lock/src/Events/DiscussionWasUnlocked.php new file mode 100644 index 000000000..07d1d1036 --- /dev/null +++ b/extensions/lock/src/Events/DiscussionWasUnlocked.php @@ -0,0 +1,27 @@ +discussion = $discussion; + $this->user = $user; + } +} diff --git a/extensions/lock/src/Extension.php b/extensions/lock/src/Extension.php new file mode 100644 index 000000000..e2a2de810 --- /dev/null +++ b/extensions/lock/src/Extension.php @@ -0,0 +1,16 @@ +subscribe('Flarum\Lock\Listeners\AddClientAssets'); + $events->subscribe('Flarum\Lock\Listeners\AddApiAttributes'); + $events->subscribe('Flarum\Lock\Listeners\PersistData'); + $events->subscribe('Flarum\Lock\Listeners\NotifyDiscussionLocked'); + $events->subscribe('Flarum\Lock\Listeners\ConfigurePermissions'); + } +} diff --git a/extensions/lock/src/Gambits/LockGambit.php b/extensions/lock/src/Gambits/LockGambit.php new file mode 100644 index 000000000..eb801a249 --- /dev/null +++ b/extensions/lock/src/Gambits/LockGambit.php @@ -0,0 +1,14 @@ +getQuery()->where('is_locked', ! $negate); + } +} diff --git a/extensions/lock/src/Listeners/AddApiAttributes.php b/extensions/lock/src/Listeners/AddApiAttributes.php new file mode 100755 index 000000000..0bf1df562 --- /dev/null +++ b/extensions/lock/src/Listeners/AddApiAttributes.php @@ -0,0 +1,21 @@ +listen(ApiAttributes::class, [$this, 'addAttributes']); + } + + public function addAttributes(ApiAttributes $event) + { + if ($event->serializer instanceof DiscussionSerializer) { + $event->attributes['isLocked'] = (bool) $event->model->is_locked; + $event->attributes['canLock'] = (bool) $event->model->can($event->actor, 'lock'); + } + } +} diff --git a/extensions/lock/src/Listeners/AddClientAssets.php b/extensions/lock/src/Listeners/AddClientAssets.php new file mode 100755 index 000000000..e5d381aa3 --- /dev/null +++ b/extensions/lock/src/Listeners/AddClientAssets.php @@ -0,0 +1,39 @@ +listen(RegisterLocales::class, [$this, 'addLocale']); + $events->listen(BuildClientView::class, [$this, 'addAssets']); + } + + public function addLocale(RegisterLocales $event) + { + $event->addTranslations('en', __DIR__.'/../../locale/en.yml'); + } + + public function addAssets(BuildClientView $event) + { + $event->forumAssets([ + __DIR__.'/../../js/forum/dist/extension.js', + __DIR__.'/../../less/forum/extension.less' + ]); + + $event->forumBootstrapper('lock/main'); + + $event->forumTranslations([ + 'lock.discussion_locked_notification', + 'lock.discussion_locked_post', + 'lock.discussion_unlocked_post', + 'lock.notify_discussion_locked', + 'lock.locked', + 'lock.lock', + 'lock.unlock' + ]); + } +} diff --git a/extensions/lock/src/Listeners/ConfigurePermissions.php b/extensions/lock/src/Listeners/ConfigurePermissions.php new file mode 100755 index 000000000..9384d4371 --- /dev/null +++ b/extensions/lock/src/Listeners/ConfigurePermissions.php @@ -0,0 +1,23 @@ +listen(ModelAllow::class, [$this, 'allowDiscussionPermissions'], 10); + } + + public function allowDiscussionPermissions(ModelAllow $event) + { + if ($event->model instanceof Discussion && + $event->model->is_locked && + $event->action === 'reply') { + if (! $event->model->can($event->actor, 'lock')) { + return false; + } + } + } +} diff --git a/extensions/lock/src/Listeners/NotifyDiscussionLocked.php b/extensions/lock/src/Listeners/NotifyDiscussionLocked.php new file mode 100755 index 000000000..0f8cd9a09 --- /dev/null +++ b/extensions/lock/src/Listeners/NotifyDiscussionLocked.php @@ -0,0 +1,71 @@ +notifications = $notifications; + } + + public function subscribe(Dispatcher $events) + { + $events->listen(RegisterPostTypes::class, [$this, 'registerPostType']); + $events->listen(RegisterNotificationTypes::class, [$this, 'registerNotificationType']); + $events->listen(DiscussionWasLocked::class, [$this, 'whenDiscussionWasLocked']); + $events->listen(DiscussionWasUnlocked::class, [$this, 'whenDiscussionWasUnlocked']); + } + + public function registerPostType(RegisterPostTypes $event) + { + $event->register('Flarum\Lock\Posts\DiscussionLockedPost'); + } + + public function registerNotificationType(RegisterNotificationTypes $event) + { + $event->register( + 'Flarum\Lock\Notifications\DiscussionLockedBlueprint', + 'Flarum\Api\Serializers\DiscussionBasicSerializer', + ['alert'] + ); + } + + public function whenDiscussionWasLocked(DiscussionWasLocked $event) + { + $this->stickyChanged($event->discussion, $event->user, true); + } + + public function whenDiscussionWasUnlocked(DiscussionWasUnlocked $event) + { + $this->stickyChanged($event->discussion, $event->user, false); + } + + protected function stickyChanged(Discussion $discussion, User $user, $isLocked) + { + $post = DiscussionLockedPost::reply( + $discussion->id, + $user->id, + $isLocked + ); + + $post = $discussion->mergePost($post); + + if ($discussion->start_user_id !== $user->id) { + $notification = new DiscussionLockedBlueprint($post); + + $this->notifications->sync($notification, $post->exists ? [$discussion->startUser] : []); + } + } +} diff --git a/extensions/lock/src/Listeners/PersistData.php b/extensions/lock/src/Listeners/PersistData.php new file mode 100755 index 000000000..d3dcfe9f6 --- /dev/null +++ b/extensions/lock/src/Listeners/PersistData.php @@ -0,0 +1,36 @@ +listen(DiscussionWillBeSaved::class, [$this, 'whenDiscussionWillBeSaved']); + } + + public function whenDiscussionWillBeSaved(DiscussionWillBeSaved $event) + { + if (isset($event->data['attributes']['isLocked'])) { + $isLocked = (bool) $event->data['attributes']['isLocked']; + $discussion = $event->discussion; + $actor = $event->actor; + + $discussion->assertCan($actor, 'lock'); + + if ((bool) $discussion->is_locked === $isLocked) { + return; + } + + $discussion->is_locked = $isLocked; + + $discussion->raise( + $discussion->is_locked + ? new DiscussionWasLocked($discussion, $actor) + : new DiscussionWasUnlocked($discussion, $actor) + ); + } + } +} diff --git a/extensions/lock/src/Notifications/DiscussionLockedBlueprint.php b/extensions/lock/src/Notifications/DiscussionLockedBlueprint.php new file mode 100644 index 000000000..405415c40 --- /dev/null +++ b/extensions/lock/src/Notifications/DiscussionLockedBlueprint.php @@ -0,0 +1,39 @@ +post = $post; + } + + public function getSender() + { + return $this->post->user; + } + + public function getSubject() + { + return $this->post->discussion; + } + + public function getData() + { + return ['postNumber' => (int) $this->post->number]; + } + + public static function getType() + { + return 'discussionLocked'; + } + + public static function getSubjectModel() + { + return 'Flarum\Core\Discussions\Discussion'; + } +} diff --git a/extensions/lock/src/Posts/DiscussionLockedPost.php b/extensions/lock/src/Posts/DiscussionLockedPost.php new file mode 100755 index 000000000..515826ddf --- /dev/null +++ b/extensions/lock/src/Posts/DiscussionLockedPost.php @@ -0,0 +1,64 @@ +user_id === $previous->user_id) { + if ($previous->content['locked'] != $this->content['locked']) { + $previous->delete(); + } else { + $previous->content = $this->content; + + $previous->save(); + } + + return $previous; + } + + $this->save(); + + return $this; + } + + /** + * Create a new instance in reply to a discussion. + * + * @param integer $discussionId + * @param integer $userId + * @param boolean $isLocked + * @return static + */ + public static function reply($discussionId, $userId, $isLocked) + { + $post = new static; + + $post->content = static::buildContent($isLocked); + $post->time = time(); + $post->discussion_id = $discussionId; + $post->user_id = $userId; + + return $post; + } + + /** + * Build the content attribute. + * + * @param boolean $isLocked Whether or not the discussion is stickied. + * @return array + */ + public static function buildContent($isLocked) + { + return ['locked' => (bool) $isLocked]; + } +}