diff --git a/extensions/suspend/.editorconfig b/extensions/suspend/.editorconfig new file mode 100644 index 000000000..a61a3ab36 --- /dev/null +++ b/extensions/suspend/.editorconfig @@ -0,0 +1,19 @@ +# 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 + +[*.{diff,md}] +trim_trailing_whitespace = false + +[*.{php,xml,json}] +indent_size = 4 diff --git a/extensions/suspend/.gitattributes b/extensions/suspend/.gitattributes new file mode 100644 index 000000000..ca0f40972 --- /dev/null +++ b/extensions/suspend/.gitattributes @@ -0,0 +1,18 @@ +.gitattributes export-ignore +.gitignore export-ignore +.gitmodules export-ignore +.github export-ignore +.travis export-ignore +.travis.yml export-ignore +.editorconfig export-ignore +.styleci.yml export-ignore + +phpunit.xml export-ignore +tests export-ignore + +js/dist/* -diff +js/dist/* linguist-generated +js/dist-typings/* linguist-generated +js/yarn.lock -diff + +* text=auto eol=lf diff --git a/extensions/suspend/.github/workflows/backend.yml b/extensions/suspend/.github/workflows/backend.yml new file mode 100644 index 000000000..55e80cd97 --- /dev/null +++ b/extensions/suspend/.github/workflows/backend.yml @@ -0,0 +1,15 @@ +name: Suspend PHP + +on: [workflow_dispatch, push, pull_request] + +# The reusable workflow definitions will be moved to the `flarum/framework` repo soon. +# This will break your current script. +# When this happens, run `flarum-cli audit infra --fix` to update your infrastructure. + +jobs: + run: + uses: flarum/.github/.github/workflows/REUSABLE_backend.yml@main + with: + enable_backend_testing: false + + backend_directory: . \ No newline at end of file diff --git a/extensions/suspend/.github/workflows/frontend.yml b/extensions/suspend/.github/workflows/frontend.yml new file mode 100644 index 000000000..d22e02ce6 --- /dev/null +++ b/extensions/suspend/.github/workflows/frontend.yml @@ -0,0 +1,21 @@ +name: Suspend JS + +on: [workflow_dispatch, push, pull_request] + +# The reusable workflow definitions will be moved to the `flarum/framework` repo soon. +# This will break your current script. +# When this happens, run `flarum-cli audit infra --fix` to update your infrastructure. + +jobs: + run: + uses: flarum/.github/.github/workflows/REUSABLE_frontend.yml@main + with: + enable_bundlewatch: false + enable_prettier: true + enable_typescript: false + + frontend_directory: ./js + main_git_branch: master + + secrets: + bundlewatch_github_token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} \ No newline at end of file diff --git a/extensions/suspend/.gitignore b/extensions/suspend/.gitignore new file mode 100644 index 000000000..4ebb168a0 --- /dev/null +++ b/extensions/suspend/.gitignore @@ -0,0 +1,12 @@ +/vendor +composer.lock +composer.phar + +.DS_Store +Thumbs.db +tests/.phpunit.result.cache +/tests/integration/tmp +.vagrant +.idea/* +.vscode +js/coverage-ts diff --git a/extensions/suspend/.styleci.yml b/extensions/suspend/.styleci.yml new file mode 100644 index 000000000..8806a5402 --- /dev/null +++ b/extensions/suspend/.styleci.yml @@ -0,0 +1,14 @@ +preset: recommended + +enabled: + - logical_not_operators_with_successor_space + +disabled: + - align_double_arrow + - blank_line_after_opening_tag + - multiline_array_trailing_comma + - new_with_braces + - phpdoc_align + - phpdoc_order + - phpdoc_separation + - phpdoc_types diff --git a/extensions/suspend/CHANGELOG.md b/extensions/suspend/CHANGELOG.md new file mode 100644 index 000000000..040428e09 --- /dev/null +++ b/extensions/suspend/CHANGELOG.md @@ -0,0 +1,62 @@ +# Changelog + +## [1.2.0](https://github.com/flarum/suspend/compare/v1.1.0...v1.2.0) + +### Added +- Display suspension to user on first visit (https://github.com/flarum/suspend/pull/41). + +## [1.1.0](https://github.com/flarum/suspend/compare/v1.0.0...v1.1.0) + +### Changed +- Suspensions are now anonymous (https://github.com/flarum/suspend/pulls/39) +- General repo maintenance (https://github.com/flarum/suspend/pulls/37) + +## [1.0.0](https://github.com/flarum/suspend/compare/v0.1.0-beta.16...v1.0.0) + +### Changed +- Compatibility with Flarum v1.0.0. + +## [0.1.0-beta.16](https://github.com/flarum/suspend/compare/v0.1.0-beta.15...v0.1.0-beta.16) + +### Changed +- Updated admin category from moderation to feature (https://github.com/flarum/suspend/pull/33) +- Moved locale files from translation pack to extension (https://github.com/flarum/suspend/pull/28) + +### Fixes +- Incorrect suspension length in suspended notification (https://github.com/flarum/suspend/pull/32) +- Unable to view suspended users with permission granted (https://github.com/flarum/suspend/pull/35) + +## [0.1.0-beta.15](https://github.com/flarum/suspend/compare/v0.1.0-beta.14...v0.1.0-beta.15) + +### Changed +- Updated composer.json and admin javascript for new admin area. +- Updated to use newest extenders. +- Implement new authorization layer ([87a5182](https://github.com/flarum/suspend/commit/87a518286b87064d1919f5a8a4b9f2cb384f44fe). + +## [0.1.0-beta.14](https://github.com/flarum/suspend/compare/v0.1.0-beta.13...v0.1.0-beta.14) + +### Added +- Ability added to filter users by their suspended state (#23) + +### Changed +- Updated mithril to version 2 +- Load language strings correctly on en-/disable +- Updated JS dependencies +- Replace momentjs with dayjs + +## [0.1.0-beta.13](https://github.com/flarum/suspend/compare/v0.1.0-beta.12...v0.1.0-beta.13) + +### Changed +- Updated JS dependencies +- Stop using deprecated core events, use extenders instead + +## [0.1.0-beta.10](https://github.com/flarum/suspend/compare/v0.1.0-beta.9...v0.1.0-beta.10) + +### Changed +- Replace the Font Awesome 4 with its version 5 counterpart ([d52f9e9](https://github.com/flarum/suspend/pull/21/commits/d52f9e9b810c51f294fafb4a3f580e5bd8c3ded8)) + +## [0.1.0-beta.9](https://github.com/flarum/suspend/compare/v0.1.0-beta.8...v0.1.0-beta.9) + +### Changed +- Replace event subscribers (that resolve services too early) with listeners ([e84082e](https://github.com/flarum/suspend/commit/e84082ecb41262aa0a48001396759c72a892219e)) + diff --git a/extensions/suspend/LICENSE b/extensions/suspend/LICENSE new file mode 100644 index 000000000..54ac29ab2 --- /dev/null +++ b/extensions/suspend/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation) +Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com) + +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/suspend/composer.json b/extensions/suspend/composer.json new file mode 100644 index 000000000..313778ef3 --- /dev/null +++ b/extensions/suspend/composer.json @@ -0,0 +1,60 @@ +{ + "name": "flarum/suspend", + "description": "Suspend users so they can't post.", + "type": "flarum-extension", + "keywords": [ + "moderation" + ], + "license": "MIT", + "support": { + "issues": "https://github.com/flarum/core/issues", + "source": "https://github.com/flarum/suspend", + "forum": "https://discuss.flarum.org" + }, + "homepage": "https://flarum.org", + "funding": [ + { + "type": "website", + "url": "https://flarum.org/donate/" + } + ], + "require": { + "flarum/core": "^1.2" + }, + "autoload": { + "psr-4": { + "Flarum\\Suspend\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + }, + "flarum-extension": { + "title": "Suspend", + "category": "feature", + "icon": { + "name": "fas fa-ban", + "backgroundColor": "#ddd", + "color": "#666" + } + }, + "flarum-cli": { + "modules": { + "admin": true, + "forum": true, + "js": true, + "jsCommon": false, + "css": true, + "gitConf": true, + "githubActions": true, + "prettier": true, + "typescript": false, + "bundlewatch": false, + "backendTesting": false, + "editorConfig": true, + "styleci": true + } + } + } +} diff --git a/extensions/suspend/extend.php b/extensions/suspend/extend.php new file mode 100644 index 000000000..7e66e7609 --- /dev/null +++ b/extensions/suspend/extend.php @@ -0,0 +1,67 @@ +js(__DIR__.'/js/dist/forum.js') + ->css(__DIR__.'/less/forum.less'), + + (new Extend\Frontend('admin')) + ->js(__DIR__.'/js/dist/admin.js') + ->css(__DIR__.'/less/admin.less'), + + (new Extend\Model(User::class)) + ->dateAttribute('suspended_until'), + + (new Extend\ApiSerializer(UserSerializer::class)) + ->attributes(AddUserSuspendAttributes::class), + + new Extend\Locales(__DIR__.'/locale'), + + (new Extend\Notification()) + ->type(UserSuspendedBlueprint::class, BasicUserSerializer::class, ['alert', 'email']) + ->type(UserUnsuspendedBlueprint::class, BasicUserSerializer::class, ['alert', 'email']), + + (new Extend\Event()) + ->listen(Saving::class, Listener\SaveSuspensionToDatabase::class) + ->listen(Suspended::class, Listener\SendNotificationWhenUserIsSuspended::class) + ->listen(Unsuspended::class, Listener\SendNotificationWhenUserIsUnsuspended::class), + + (new Extend\Policy()) + ->modelPolicy(User::class, UserPolicy::class), + + (new Extend\User()) + ->permissionGroups(RevokeAccessFromSuspendedUsers::class), + + (new Extend\Filter(UserFilterer::class)) + ->addFilter(SuspendedFilterGambit::class), + + (new Extend\SimpleFlarumSearch(UserSearcher::class)) + ->addGambit(SuspendedFilterGambit::class), + + (new Extend\View()) + ->namespace('flarum-suspend', __DIR__.'/views'), +]; diff --git a/extensions/suspend/js/.gitignore b/extensions/suspend/js/.gitignore new file mode 100644 index 000000000..adc90f312 --- /dev/null +++ b/extensions/suspend/js/.gitignore @@ -0,0 +1,9 @@ +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +node_modules diff --git a/extensions/suspend/js/admin.js b/extensions/suspend/js/admin.js new file mode 100644 index 000000000..3e69ff3b9 --- /dev/null +++ b/extensions/suspend/js/admin.js @@ -0,0 +1 @@ +export * from './src/admin'; diff --git a/extensions/suspend/js/dist/admin.js b/extensions/suspend/js/dist/admin.js new file mode 100644 index 000000000..d69c30923 --- /dev/null +++ b/extensions/suspend/js/dist/admin.js @@ -0,0 +1,2 @@ +(()=>{var e={n:r=>{var a=r&&r.__esModule?()=>r.default:()=>r;return e.d(a,{a}),a},d:(r,a)=>{for(var o in a)e.o(a,o)&&!e.o(r,o)&&Object.defineProperty(r,o,{enumerable:!0,get:a[o]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},r={};(()=>{"use strict";e.r(r);const a=flarum.core.compat.app;var o=e.n(a);o().initializers.add("flarum-suspend",(function(){o().extensionData.for("flarum-suspend").registerPermission({icon:"fas fa-ban",label:o().translator.trans("flarum-suspend.admin.permissions.suspend_users_label"),permission:"user.suspend"},"moderate")}))})(),module.exports=r})(); +//# sourceMappingURL=admin.js.map \ No newline at end of file diff --git a/extensions/suspend/js/dist/admin.js.map b/extensions/suspend/js/dist/admin.js.map new file mode 100644 index 000000000..9a941f363 --- /dev/null +++ b/extensions/suspend/js/dist/admin.js.map @@ -0,0 +1 @@ +{"version":3,"file":"admin.js","mappings":"MACA,IAAIA,EAAsB,CCA1BA,EAAyBC,IACxB,IAAIC,EAASD,GAAUA,EAAOE,WAC7B,IAAOF,EAAiB,QACxB,IAAM,EAEP,OADAD,EAAoBI,EAAEF,EAAQ,CAAEG,IACzBH,GCLRF,EAAwB,CAACM,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXP,EAAoBS,EAAEF,EAAYC,KAASR,EAAoBS,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,MCJ3ER,EAAwB,CAACc,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFf,EAAyBM,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,M,+BCLvD,MAAM,EAA+BC,OAAOC,KAAKC,OAAY,I,aCE7DC,IAAAA,aAAAA,IAAqB,kBAAkB,WACrCA,IAAAA,cAAAA,IAAsB,kBAAkBC,mBACtC,CACEC,KAAM,aACNC,MAAOH,IAAAA,WAAAA,MAAqB,wDAC5BI,WAAY,gBAEd,gB","sources":["webpack://@flarum/suspend/webpack/bootstrap","webpack://@flarum/suspend/webpack/runtime/compat get default export","webpack://@flarum/suspend/webpack/runtime/define property getters","webpack://@flarum/suspend/webpack/runtime/hasOwnProperty shorthand","webpack://@flarum/suspend/webpack/runtime/make namespace object","webpack://@flarum/suspend/external root \"flarum.core.compat['app']\"","webpack://@flarum/suspend/./src/admin/index.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['app'];","import app from 'flarum/app';\n\napp.initializers.add('flarum-suspend', () => {\n app.extensionData.for('flarum-suspend').registerPermission(\n {\n icon: 'fas fa-ban',\n label: app.translator.trans('flarum-suspend.admin.permissions.suspend_users_label'),\n permission: 'user.suspend',\n },\n 'moderate'\n );\n});\n"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","core","compat","app","registerPermission","icon","label","permission"],"sourceRoot":""} \ No newline at end of file diff --git a/extensions/suspend/js/dist/forum.js b/extensions/suspend/js/dist/forum.js new file mode 100644 index 000000000..f34c599f9 --- /dev/null +++ b/extensions/suspend/js/dist/forum.js @@ -0,0 +1,2 @@ +(()=>{var t={555:function(t){t.exports=function(){"use strict";var t=6e4,e=36e5,n="millisecond",s="second",r="minute",i="hour",a="day",o="week",u="month",c="quarter",l="year",d="date",f="Invalid Date",m=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,h=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,p={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_")},v=function(t,e,n){var s=String(t);return!s||s.length>=e?t:""+Array(e+1-s.length).join(n)+t},$={s:v,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),s=Math.floor(n/60),r=n%60;return(e<=0?"+":"-")+v(s,2,"0")+":"+v(r,2,"0")},m:function t(e,n){if(e.date(){var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var s in e)n.o(e,s)&&!n.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:e[s]})},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),n.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var s={};(()=>{"use strict";n.r(s);const t=flarum.core.compat.extend,e=flarum.core.compat.app;var r=n.n(e);const i=flarum.core.compat["utils/UserControls"];var a=n.n(i);const o=flarum.core.compat["components/Button"];var u=n.n(o);const c=flarum.core.compat["components/Badge"];var l=n.n(c);const d=flarum.core.compat.Model;var f=n.n(d);const h=flarum.core.compat["models/User"];var p=n.n(h);function v(t,e){return v=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t},v(t,e)}function $(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,v(t,e)}const g=flarum.core.compat["forum/app"];var y=n.n(g);const M=flarum.core.compat["components/Modal"];var _=n.n(M);const S=flarum.core.compat["utils/Stream"];var b=n.n(S);const D=flarum.core.compat["utils/withAttr"];var w=n.n(D);const O=flarum.core.compat["common/utils/ItemList"];var T=n.n(O),x=n(555),U=n.n(x),N=n(911),k=n.n(N);function C(t){return U().utc(t).isSame(U().utc("2038-01-01"))}U().extend(k());var Y=function(t){function e(){return t.apply(this,arguments)||this}$(e,t);var n=e.prototype;return n.oninit=function(e){t.prototype.oninit.call(this,e);var n=this.attrs.user.suspendedUntil(),s=this.attrs.user.suspendReason(),r=this.attrs.user.suspendMessage(),i=null;new Date>n&&(n=null),n&&(i=9999===n.getFullYear()?"indefinitely":"limited"),this.status=b()(i),this.reason=b()(s),this.message=b()(r),this.daysRemaining=b()("limited"===i&&1-dayjs().diff(n,"days"))},n.className=function(){return"SuspendUserModal Modal--medium"},n.title=function(){return y().translator.trans("flarum-suspend.forum.suspend_user.title",{user:this.attrs.user})},n.content=function(){return m("div",{className:"Modal-body"},m("div",{className:"Form"},m("div",{className:"Form-group"},m("label",null,y().translator.trans("flarum-suspend.forum.suspend_user.status_heading")),m("div",null,this.formItems().toArray())),m("div",{className:"Form-group"},m(u(),{className:"Button Button--primary",loading:this.loading,type:"submit"},y().translator.trans("flarum-suspend.forum.suspend_user.submit_button")))))},n.radioItems=function(){var t=this,e=new(T());return e.add("not-suspended",m("label",{className:"checkbox"},m("input",{type:"radio",name:"status",checked:!this.status(),value:"",onclick:w()("value",this.status)}),y().translator.trans("flarum-suspend.forum.suspend_user.not_suspended_label")),100),e.add("indefinitely",m("label",{className:"checkbox"},m("input",{type:"radio",name:"status",checked:"indefinitely"===this.status(),value:"indefinitely",onclick:w()("value",this.status)}),y().translator.trans("flarum-suspend.forum.suspend_user.indefinitely_label")),90),e.add("time-suspension",m("label",{className:"checkbox SuspendUserModal-days"},m("input",{type:"radio",name:"status",checked:"limited"===this.status(),value:"limited",onclick:function(e){t.status(e.target.value),m.redraw.sync(),t.$(".SuspendUserModal-days-input input").select(),e.redraw=!1}}),y().translator.trans("flarum-suspend.forum.suspend_user.limited_time_label"),"limited"===this.status()&&m("div",{className:"SuspendUserModal-days-input"},m("input",{type:"number",min:"0",value:this.daysRemaining(),oninput:w()("value",this.daysRemaining),className:"FormControl"}),y().translator.trans("flarum-suspend.forum.suspend_user.limited_time_days_text"))),80),e},n.formItems=function(){var t=new(T());return t.add("radioItems",m("div",{className:"Form-group"},this.radioItems().toArray()),100),t.add("reason",m("div",{className:"Form-group"},m("label",null,y().translator.trans("flarum-suspend.forum.suspend_user.reason"),m("textarea",{className:"FormControl",bidi:this.reason,placeholder:y().translator.trans("flarum-suspend.forum.suspend_user.placeholder_optional"),rows:"2"}))),90),t.add("message",m("div",{className:"Form-group"},m("label",null,y().translator.trans("flarum-suspend.forum.suspend_user.display_message"),m("textarea",{className:"FormControl",bidi:this.message,placeholder:y().translator.trans("flarum-suspend.forum.suspend_user.placeholder_optional"),rows:"2"}))),80),t},n.onsubmit=function(t){var e=this;t.preventDefault(),this.loading=!0;var n=null;switch(this.status()){case"indefinitely":n=new Date("2038-01-01");break;case"limited":n=dayjs().add(this.daysRemaining(),"days").toDate()}this.attrs.user.save({suspendedUntil:n,suspendReason:this.reason(),suspendMessage:this.message()}).then((function(){return e.hide()}),this.loaded.bind(this))},e}(_());const I=flarum.core.compat["components/Notification"];var F=n.n(I),H=function(t){function e(){return t.apply(this,arguments)||this}$(e,t);var n=e.prototype;return n.icon=function(){return"fas fa-ban"},n.href=function(){return y().route.user(this.attrs.notification.subject())},n.content=function(){var t=this.attrs.notification,e=t.content(),n=dayjs(e).from(t.createdAt(),!0);return C(e)?y().translator.trans("flarum-suspend.forum.notifications.user_suspended_indefinite_text"):y().translator.trans("flarum-suspend.forum.notifications.user_suspended_text",{timeReadable:n})},e}(F()),j=function(t){function e(){return t.apply(this,arguments)||this}$(e,t);var n=e.prototype;return n.icon=function(){return"fas fa-ban"},n.href=function(){return y().route.user(this.attrs.notification.subject())},n.content=function(){return this.attrs.notification,y().translator.trans("flarum-suspend.forum.notifications.user_unsuspended_text")},e}(F());const A=flarum.core.compat["common/components/Modal"];var L=n.n(A);const W=flarum.core.compat["common/components/Button"];var B=n.n(W);const R=flarum.core.compat["common/helpers/fullTime"];var z=n.n(R),P=function(t){function e(){return t.apply(this,arguments)||this}$(e,t);var n=e.prototype;return n.oninit=function(e){t.prototype.oninit.call(this,e),this.message=this.attrs.message,this.until=this.attrs.until},n.className=function(){return"SuspensionInfoModal Modal"},n.title=function(){return y().translator.trans("flarum-suspend.forum.suspension_info.title")},n.content=function(){var t=C(new Date(this.until))?y().translator.trans("flarum-suspend.forum.suspension_info.indefinite"):y().translator.trans("flarum-suspend.forum.suspension_info.limited",{date:z()(this.until)});return m("div",{className:"Modal-body"},m("div",{className:"Form Form--centered"},m("p",{className:"helpText"},this.message),m("p",{className:"helpText"},t),m("div",{className:"Form-group"},m(B(),{className:"Button Button--primary Button--block",onclick:this.hide.bind(this)},y().translator.trans("flarum-suspend.forum.suspension_info.dismiss_button")))))},n.hide=function(){localStorage.setItem("flarum-suspend.acknowledge-suspension",this.attrs.until.getTime()),this.attrs.state.close()},e}(L());function Z(){return setTimeout((function(){if(y().session.user){var t=y().session.user.suspendMessage(),e=y().session.user.suspendedUntil(),n=localStorage.getItem("flarum-suspend.acknowledge-suspension")===(null==e?void 0:e.getTime().toString());t&&!n?y().modal.show(P,{message:t,until:e}):!e&&localStorage.getItem("flarum-suspend.acknowledge-suspension")&&localStorage.removeItem("flarum-suspend.acknowledge-suspension")}}),0)}const J={"suspend/components/suspendUserModal":Y,"suspend/components/suspensionInfoModal":P,"suspend/components/UserSuspendedNotification":H,"suspend/components/UserUnsuspendedNotification":j,"suspend/checkForSuspension":Z},V=flarum.core;r().initializers.add("flarum-suspend",(function(){r().notificationComponents.userSuspended=H,r().notificationComponents.userUnsuspended=j,p().prototype.canSuspend=f().attribute("canSuspend"),p().prototype.suspendedUntil=f().attribute("suspendedUntil",f().transformDate),p().prototype.suspendReason=f().attribute("suspendReason"),p().prototype.suspendMessage=f().attribute("suspendMessage"),(0,t.extend)(a(),"moderationControls",(function(t,e){e.canSuspend()&&t.add("suspend",u().component({icon:"fas fa-ban",onclick:function(){return r().modal.show(Y,{user:e})}},r().translator.trans("flarum-suspend.forum.user_controls.suspend_button")))})),(0,t.extend)(p().prototype,"badges",(function(t){var e=this.suspendedUntil();new Date=e?t:\"\"+Array(e+1-r.length).join(n)+t},g={s:m,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?\"+\":\"-\")+m(r,2,\"0\")+\":\"+m(i,2,\"0\")},m:function t(e,n){if(e.date() {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['extend'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['app'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['utils/UserControls'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['components/Button'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['components/Badge'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['Model'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['models/User'];","export default function _setPrototypeOf(o, p) {\n _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {\n o.__proto__ = p;\n return o;\n };\n\n return _setPrototypeOf(o, p);\n}","import setPrototypeOf from \"./setPrototypeOf.js\";\nexport default function _inheritsLoose(subClass, superClass) {\n subClass.prototype = Object.create(superClass.prototype);\n subClass.prototype.constructor = subClass;\n setPrototypeOf(subClass, superClass);\n}","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/app'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['components/Modal'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['utils/Stream'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['utils/withAttr'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/ItemList'];","import dayjs from 'dayjs';\nimport utc from 'dayjs/plugin/utc';\n\ndayjs.extend(utc);\n\nexport function getPermanentSuspensionDate(): Date {\n return new Date('2038-01-01');\n}\n\nexport function isPermanentSuspensionDate(date: Date): boolean {\n return dayjs.utc(date).isSame(dayjs.utc('2038-01-01'));\n}\n\nexport function localStorageKey(): string {\n return 'flarum-suspend.acknowledge-suspension';\n}\n","import app from 'flarum/forum/app';\nimport Modal from 'flarum/components/Modal';\nimport Button from 'flarum/components/Button';\n\nimport Stream from 'flarum/utils/Stream';\nimport withAttr from 'flarum/utils/withAttr';\nimport ItemList from 'flarum/common/utils/ItemList';\nimport { getPermanentSuspensionDate } from '../helpers/suspensionHelper';\n\nexport default class SuspendUserModal extends Modal {\n oninit(vnode) {\n super.oninit(vnode);\n\n let until = this.attrs.user.suspendedUntil();\n const reason = this.attrs.user.suspendReason();\n const message = this.attrs.user.suspendMessage();\n let status = null;\n\n if (new Date() > until) until = null;\n\n if (until) {\n if (until.getFullYear() === 9999) status = 'indefinitely';\n else status = 'limited';\n }\n\n this.status = Stream(status);\n this.reason = Stream(reason);\n this.message = Stream(message);\n this.daysRemaining = Stream(status === 'limited' && -dayjs().diff(until, 'days') + 1);\n }\n\n className() {\n return 'SuspendUserModal Modal--medium';\n }\n\n title() {\n return app.translator.trans('flarum-suspend.forum.suspend_user.title', { user: this.attrs.user });\n }\n\n content() {\n return (\n
\n
\n
\n \n
{this.formItems().toArray()}
\n
\n\n
\n \n
\n
\n
\n );\n }\n\n radioItems() {\n const items = new ItemList();\n\n items.add(\n 'not-suspended',\n ,\n 100\n );\n\n items.add(\n 'indefinitely',\n ,\n 90\n );\n\n items.add(\n 'time-suspension',\n ,\n 80\n );\n\n return items;\n }\n\n formItems() {\n const items = new ItemList();\n\n items.add('radioItems',
{this.radioItems().toArray()}
, 100);\n\n items.add(\n 'reason',\n
\n \n
,\n 90\n );\n\n items.add(\n 'message',\n
\n \n
,\n 80\n );\n\n return items;\n }\n\n onsubmit(e) {\n e.preventDefault();\n\n this.loading = true;\n\n let suspendedUntil = null;\n switch (this.status()) {\n case 'indefinitely':\n suspendedUntil = getPermanentSuspensionDate();\n break;\n\n case 'limited':\n suspendedUntil = dayjs().add(this.daysRemaining(), 'days').toDate();\n break;\n\n default:\n // no default\n }\n\n this.attrs.user\n .save({ suspendedUntil, suspendReason: this.reason(), suspendMessage: this.message() })\n .then(() => this.hide(), this.loaded.bind(this));\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['components/Notification'];","import app from 'flarum/forum/app';\nimport Notification from 'flarum/components/Notification';\nimport { isPermanentSuspensionDate } from '../helpers/suspensionHelper';\n\nexport default class UserSuspendedNotification extends Notification {\n icon() {\n return 'fas fa-ban';\n }\n\n href() {\n return app.route.user(this.attrs.notification.subject());\n }\n\n content() {\n const notification = this.attrs.notification;\n const suspendedUntil = notification.content();\n const timeReadable = dayjs(suspendedUntil).from(notification.createdAt(), true);\n\n return isPermanentSuspensionDate(suspendedUntil)\n ? app.translator.trans('flarum-suspend.forum.notifications.user_suspended_indefinite_text')\n : app.translator.trans('flarum-suspend.forum.notifications.user_suspended_text', {\n timeReadable,\n });\n }\n}\n","import app from 'flarum/forum/app';\nimport Notification from 'flarum/components/Notification';\n\nexport default class UserUnsuspendedNotification extends Notification {\n icon() {\n return 'fas fa-ban';\n }\n\n href() {\n return app.route.user(this.attrs.notification.subject());\n }\n\n content() {\n const notification = this.attrs.notification;\n\n return app.translator.trans('flarum-suspend.forum.notifications.user_unsuspended_text');\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Modal'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Button'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/fullTime'];","import app from 'flarum/forum/app';\nimport Modal from 'flarum/common/components/Modal';\nimport Button from 'flarum/common/components/Button';\nimport fullTime from 'flarum/common/helpers/fullTime';\nimport { isPermanentSuspensionDate, localStorageKey } from '../helpers/suspensionHelper';\n\nexport default class SuspensionInfoModal extends Modal {\n oninit(vnode) {\n super.oninit(vnode);\n\n this.message = this.attrs.message;\n this.until = this.attrs.until;\n }\n\n className() {\n return 'SuspensionInfoModal Modal';\n }\n\n title() {\n return app.translator.trans('flarum-suspend.forum.suspension_info.title');\n }\n\n content() {\n const timespan = isPermanentSuspensionDate(new Date(this.until))\n ? app.translator.trans('flarum-suspend.forum.suspension_info.indefinite')\n : app.translator.trans('flarum-suspend.forum.suspension_info.limited', { date: fullTime(this.until) });\n\n return (\n
\n
\n

{this.message}

\n

{timespan}

\n\n
\n \n
\n
\n
\n );\n }\n\n hide() {\n localStorage.setItem(localStorageKey(), this.attrs.until.getTime());\n this.attrs.state.close();\n }\n}\n","import app from 'flarum/forum/app';\nimport SuspensionInfoModal from './components/SuspensionInfoModal';\nimport { localStorageKey } from './helpers/suspensionHelper';\n\nexport default function () {\n return setTimeout(() => {\n if (app.session.user) {\n const message = app.session.user.suspendMessage();\n const until = app.session.user.suspendedUntil();\n const alreadyDisplayed = localStorage.getItem(localStorageKey()) === until?.getTime().toString();\n\n if (message && !alreadyDisplayed) {\n app.modal.show(SuspensionInfoModal, { message, until });\n } else if (!until && localStorage.getItem(localStorageKey())) {\n localStorage.removeItem(localStorageKey());\n }\n }\n }, 0);\n}\n","import SuspendUserModal from './components/SuspendUserModal';\nimport SuspensionInfoModal from './components/SuspensionInfoModal';\nimport UserSuspendedNotification from './components/UserSuspendedNotification';\nimport UserUnsuspendedNotification from './components/UserUnsuspendedNotification';\nimport checkForSuspension from './checkForSuspension';\n\nexport default {\n 'suspend/components/suspendUserModal': SuspendUserModal,\n 'suspend/components/suspensionInfoModal': SuspensionInfoModal,\n 'suspend/components/UserSuspendedNotification': UserSuspendedNotification,\n 'suspend/components/UserUnsuspendedNotification': UserUnsuspendedNotification,\n 'suspend/checkForSuspension': checkForSuspension,\n};\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core;","import { extend } from 'flarum/extend';\nimport app from 'flarum/app';\nimport UserControls from 'flarum/utils/UserControls';\nimport Button from 'flarum/components/Button';\nimport Badge from 'flarum/components/Badge';\nimport Model from 'flarum/Model';\nimport User from 'flarum/models/User';\n\nimport SuspendUserModal from './components/SuspendUserModal';\nimport UserSuspendedNotification from './components/UserSuspendedNotification';\nimport UserUnsuspendedNotification from './components/UserUnsuspendedNotification';\nimport checkForSuspension from './checkForSuspension';\n\napp.initializers.add('flarum-suspend', () => {\n app.notificationComponents.userSuspended = UserSuspendedNotification;\n app.notificationComponents.userUnsuspended = UserUnsuspendedNotification;\n\n User.prototype.canSuspend = Model.attribute('canSuspend');\n User.prototype.suspendedUntil = Model.attribute('suspendedUntil', Model.transformDate);\n User.prototype.suspendReason = Model.attribute('suspendReason');\n User.prototype.suspendMessage = Model.attribute('suspendMessage');\n\n extend(UserControls, 'moderationControls', (items, user) => {\n if (user.canSuspend()) {\n items.add(\n 'suspend',\n Button.component(\n {\n icon: 'fas fa-ban',\n onclick: () => app.modal.show(SuspendUserModal, { user }),\n },\n app.translator.trans('flarum-suspend.forum.user_controls.suspend_button')\n )\n );\n }\n });\n\n extend(User.prototype, 'badges', function (items) {\n const until = this.suspendedUntil();\n\n if (new Date() < until) {\n items.add(\n 'suspended',\n Badge.component({\n icon: 'fas fa-ban',\n type: 'suspended',\n label: app.translator.trans('flarum-suspend.forum.user_badge.suspended_tooltip'),\n })\n );\n }\n });\n\n checkForSuspension();\n});\n\n// Expose compat API\nimport suspendCompat from './compat';\nimport { compat } from '@flarum/core/forum';\n\nObject.assign(compat, suspendCompat);\n"],"names":["module","exports","e","n","r","i","s","u","a","o","f","h","c","d","$","l","y","M","name","weekdays","split","months","m","t","String","length","Array","join","g","z","utcOffset","Math","abs","floor","date","year","month","clone","add","ceil","p","w","D","ms","Q","toLowerCase","replace","v","_","S","args","arguments","O","locale","$L","utc","$u","x","$x","$offset","this","parse","prototype","$d","Date","NaN","test","match","substring","UTC","init","$y","getFullYear","$M","getMonth","$D","getDate","$W","getDay","$H","getHours","$m","getMinutes","$s","getSeconds","$ms","getMilliseconds","$utils","isValid","toString","isSame","startOf","endOf","isAfter","isBefore","$g","set","unix","valueOf","getTime","toDate","apply","slice","$locale","weekStart","$set","min","daysInMonth","get","Number","round","subtract","format","invalidDate","substr","meridiem","YY","YYYY","MM","MMM","monthsShort","MMMM","DD","dd","weekdaysMin","ddd","weekdaysShort","dddd","H","HH","hh","A","mm","ss","SSS","Z","getTimezoneOffset","diff","toJSON","toISOString","toUTCString","b","forEach","extend","$i","isDayjs","en","Ls","local","call","getUTCFullYear","getUTCMonth","getUTCDate","getUTCDay","getUTCHours","getUTCMinutes","getUTCSeconds","getUTCMilliseconds","$localOffset","isUTC","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","__webpack_modules__","getter","__esModule","definition","key","Object","defineProperty","enumerable","obj","prop","hasOwnProperty","Symbol","toStringTag","value","flarum","core","compat","_setPrototypeOf","setPrototypeOf","__proto__","_inheritsLoose","subClass","superClass","create","constructor","isPermanentSuspensionDate","dayjs","SuspendUserModal","oninit","vnode","until","attrs","user","suspendedUntil","reason","suspendReason","message","suspendMessage","status","Stream","daysRemaining","className","title","app","content","formItems","toArray","loading","type","radioItems","items","ItemList","checked","onclick","withAttr","target","redraw","sync","select","oninput","bidi","placeholder","rows","onsubmit","preventDefault","save","then","hide","loaded","bind","Modal","UserSuspendedNotification","icon","href","notification","subject","timeReadable","from","createdAt","Notification","UserUnsuspendedNotification","SuspensionInfoModal","timespan","fullTime","localStorage","setItem","state","close","setTimeout","alreadyDisplayed","getItem","removeItem","checkForSuspension","User","Model","UserControls","canSuspend","Button","Badge","label","assign","suspendCompat"],"sourceRoot":""} \ No newline at end of file diff --git a/extensions/suspend/js/forum.js b/extensions/suspend/js/forum.js new file mode 100644 index 000000000..facb26fab --- /dev/null +++ b/extensions/suspend/js/forum.js @@ -0,0 +1 @@ +export * from './src/forum'; diff --git a/extensions/suspend/js/package.json b/extensions/suspend/js/package.json new file mode 100644 index 000000000..374dce85e --- /dev/null +++ b/extensions/suspend/js/package.json @@ -0,0 +1,20 @@ +{ + "private": true, + "name": "@flarum/suspend", + "prettier": "@flarum/prettier-config", + "scripts": { + "dev": "webpack --mode development --watch", + "build": "webpack --mode production", + "format": "prettier --write src", + "analyze": "cross-env ANALYZER=true yarn build", + "format-check": "prettier --check src" + }, + "devDependencies": { + "@flarum/prettier-config": "^1.0.0", + "flarum-tsconfig": "^1.0.0", + "flarum-webpack-config": "^2.0.0", + "prettier": "^2.5.1", + "webpack": "^5.65.0", + "webpack-cli": "^4.9.1" + } +} diff --git a/extensions/suspend/js/src/admin/index.js b/extensions/suspend/js/src/admin/index.js new file mode 100644 index 000000000..74de04587 --- /dev/null +++ b/extensions/suspend/js/src/admin/index.js @@ -0,0 +1,12 @@ +import app from 'flarum/app'; + +app.initializers.add('flarum-suspend', () => { + app.extensionData.for('flarum-suspend').registerPermission( + { + icon: 'fas fa-ban', + label: app.translator.trans('flarum-suspend.admin.permissions.suspend_users_label'), + permission: 'user.suspend', + }, + 'moderate' + ); +}); diff --git a/extensions/suspend/js/src/forum/checkForSuspension.js b/extensions/suspend/js/src/forum/checkForSuspension.js new file mode 100644 index 000000000..ec9133661 --- /dev/null +++ b/extensions/suspend/js/src/forum/checkForSuspension.js @@ -0,0 +1,19 @@ +import app from 'flarum/forum/app'; +import SuspensionInfoModal from './components/SuspensionInfoModal'; +import { localStorageKey } from './helpers/suspensionHelper'; + +export default function () { + return setTimeout(() => { + if (app.session.user) { + const message = app.session.user.suspendMessage(); + const until = app.session.user.suspendedUntil(); + const alreadyDisplayed = localStorage.getItem(localStorageKey()) === until?.getTime().toString(); + + if (message && !alreadyDisplayed) { + app.modal.show(SuspensionInfoModal, { message, until }); + } else if (!until && localStorage.getItem(localStorageKey())) { + localStorage.removeItem(localStorageKey()); + } + } + }, 0); +} diff --git a/extensions/suspend/js/src/forum/compat.js b/extensions/suspend/js/src/forum/compat.js new file mode 100644 index 000000000..f1931b7c3 --- /dev/null +++ b/extensions/suspend/js/src/forum/compat.js @@ -0,0 +1,13 @@ +import SuspendUserModal from './components/SuspendUserModal'; +import SuspensionInfoModal from './components/SuspensionInfoModal'; +import UserSuspendedNotification from './components/UserSuspendedNotification'; +import UserUnsuspendedNotification from './components/UserUnsuspendedNotification'; +import checkForSuspension from './checkForSuspension'; + +export default { + 'suspend/components/suspendUserModal': SuspendUserModal, + 'suspend/components/suspensionInfoModal': SuspensionInfoModal, + 'suspend/components/UserSuspendedNotification': UserSuspendedNotification, + 'suspend/components/UserUnsuspendedNotification': UserUnsuspendedNotification, + 'suspend/checkForSuspension': checkForSuspension, +}; diff --git a/extensions/suspend/js/src/forum/components/SuspendUserModal.js b/extensions/suspend/js/src/forum/components/SuspendUserModal.js new file mode 100644 index 000000000..d45cf3de6 --- /dev/null +++ b/extensions/suspend/js/src/forum/components/SuspendUserModal.js @@ -0,0 +1,172 @@ +import app from 'flarum/forum/app'; +import Modal from 'flarum/components/Modal'; +import Button from 'flarum/components/Button'; + +import Stream from 'flarum/utils/Stream'; +import withAttr from 'flarum/utils/withAttr'; +import ItemList from 'flarum/common/utils/ItemList'; +import { getPermanentSuspensionDate } from '../helpers/suspensionHelper'; + +export default class SuspendUserModal extends Modal { + oninit(vnode) { + super.oninit(vnode); + + let until = this.attrs.user.suspendedUntil(); + const reason = this.attrs.user.suspendReason(); + const message = this.attrs.user.suspendMessage(); + let status = null; + + if (new Date() > until) until = null; + + if (until) { + if (until.getFullYear() === 9999) status = 'indefinitely'; + else status = 'limited'; + } + + this.status = Stream(status); + this.reason = Stream(reason); + this.message = Stream(message); + this.daysRemaining = Stream(status === 'limited' && -dayjs().diff(until, 'days') + 1); + } + + className() { + return 'SuspendUserModal Modal--medium'; + } + + title() { + return app.translator.trans('flarum-suspend.forum.suspend_user.title', { user: this.attrs.user }); + } + + content() { + return ( +
+
+
+ +
{this.formItems().toArray()}
+
+ +
+ +
+
+
+ ); + } + + radioItems() { + const items = new ItemList(); + + items.add( + 'not-suspended', + , + 100 + ); + + items.add( + 'indefinitely', + , + 90 + ); + + items.add( + 'time-suspension', + , + 80 + ); + + return items; + } + + formItems() { + const items = new ItemList(); + + items.add('radioItems',
{this.radioItems().toArray()}
, 100); + + items.add( + 'reason', +
+