commit cb6347ef6ad302e96ef5f334ba39bc5320ee16f9 Author: Toby Zerner Date: Fri Sep 4 13:26:51 2015 +0930 Initial commit diff --git a/extensions/flags/.editorconfig b/extensions/flags/.editorconfig new file mode 100644 index 000000000..87694ddab --- /dev/null +++ b/extensions/flags/.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] +indent_size = 4 diff --git a/extensions/flags/.eslintignore b/extensions/flags/.eslintignore new file mode 100644 index 000000000..86b7c8854 --- /dev/null +++ b/extensions/flags/.eslintignore @@ -0,0 +1,5 @@ +**/bower_components/**/* +**/node_modules/**/* +vendor/**/* +**/Gulpfile.js +**/dist/**/* diff --git a/extensions/flags/.eslintrc b/extensions/flags/.eslintrc new file mode 100644 index 000000000..9e89e6ba6 --- /dev/null +++ b/extensions/flags/.eslintrc @@ -0,0 +1,175 @@ +{ + "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 + }, + "plugins": [ + "react" + ], + "rules": { + "react/jsx-uses-vars": 1, + +/** + * 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 + "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/flags/.gitignore b/extensions/flags/.gitignore new file mode 100644 index 000000000..a4f3b125e --- /dev/null +++ b/extensions/flags/.gitignore @@ -0,0 +1,4 @@ +/vendor +composer.phar +.DS_Store +Thumbs.db diff --git a/extensions/flags/.php_cs b/extensions/flags/.php_cs new file mode 100755 index 000000000..20d29c766 --- /dev/null +++ b/extensions/flags/.php_cs @@ -0,0 +1,26 @@ + + +For the full copyright and license information, please view the LICENSE +file that was distributed with this source code. +EOF; + +Symfony\CS\Fixer\Contrib\HeaderCommentFixer::setHeader($header); + +$finder = Symfony\CS\Finder\DefaultFinder::create() + ->exclude('js') + ->exclude('less') + ->in(__DIR__); + +return Symfony\CS\Config\Config::create() + ->level(Symfony\CS\FixerInterface::PSR2_LEVEL) + ->fixers([ + 'short_array_syntax', + 'header_comment', + '-psr0' + ]) + ->finder($finder); diff --git a/extensions/flags/.travis.yml b/extensions/flags/.travis.yml new file mode 100644 index 000000000..692e09f86 --- /dev/null +++ b/extensions/flags/.travis.yml @@ -0,0 +1,23 @@ +language: php + +php: + - 5.5 + - 5.6 + +matrix: + allow_failures: + - php: hhvm + fast_finish: true + +before_script: + - curl -s http://getcomposer.org/installer | php + - php composer.phar install + +script: + - php composer.phar style + +notifications: + email: + on_failure: change + +sudo: false diff --git a/extensions/flags/LICENSE b/extensions/flags/LICENSE new file mode 100644 index 000000000..aa1e5fb86 --- /dev/null +++ b/extensions/flags/LICENSE @@ -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/flags/bootstrap.php b/extensions/flags/bootstrap.php new file mode 100644 index 000000000..c296e3d99 --- /dev/null +++ b/extensions/flags/bootstrap.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +require __DIR__.'/vendor/autoload.php'; + +return 'Flarum\Reports\Extension'; diff --git a/extensions/flags/composer.json b/extensions/flags/composer.json new file mode 100644 index 000000000..374a4871c --- /dev/null +++ b/extensions/flags/composer.json @@ -0,0 +1,10 @@ +{ + "autoload": { + "psr-4": { + "Flarum\\Reports\\": "src/" + } + }, + "scripts": { + "style": "phpcs --standard=PSR2 -np src" + } +} diff --git a/extensions/flags/flarum.json b/extensions/flags/flarum.json new file mode 100644 index 000000000..de9b3c06c --- /dev/null +++ b/extensions/flags/flarum.json @@ -0,0 +1,25 @@ +{ + "name": "reports", + "title": "Reports", + "description": "Allow users to report posts for moderator review.", + "keywords": [], + "version": "0.1.0-beta.2", + "author": { + "name": "Toby Zerner", + "email": "toby@flarum.org", + "homepage": "http://tobyzerner.com" + }, + "license": "MIT", + "require": { + "flarum": ">=0.1.0-beta.2" + }, + "support": { + "source": "https://github.com/flarum/reports", + "issues": "https://github.com/flarum/core/issues" + }, + "icon": { + "name": "flag", + "backgroundColor": "#e92693", + "color": "#fff" + } +} diff --git a/extensions/flags/js/.gitignore b/extensions/flags/js/.gitignore new file mode 100644 index 000000000..372e20a51 --- /dev/null +++ b/extensions/flags/js/.gitignore @@ -0,0 +1,3 @@ +bower_components +node_modules +dist diff --git a/extensions/flags/js/admin/Gulpfile.js b/extensions/flags/js/admin/Gulpfile.js new file mode 100644 index 000000000..9db4f9477 --- /dev/null +++ b/extensions/flags/js/admin/Gulpfile.js @@ -0,0 +1,7 @@ +var gulp = require('flarum-gulp'); + +gulp({ + modules: { + 'reports': 'src/**/*.js' + } +}); diff --git a/extensions/flags/js/admin/package.json b/extensions/flags/js/admin/package.json new file mode 100644 index 000000000..62ea6c691 --- /dev/null +++ b/extensions/flags/js/admin/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "devDependencies": { + "gulp": "^3.8.11", + "flarum-gulp": "^0.1.0" + } +} diff --git a/extensions/flags/js/admin/src/main.js b/extensions/flags/js/admin/src/main.js new file mode 100644 index 000000000..8b72ed4fa --- /dev/null +++ b/extensions/flags/js/admin/src/main.js @@ -0,0 +1,19 @@ +import { extend } from 'flarum/extend'; +import app from 'flarum/app'; +import PermissionGrid from 'flarum/components/PermissionGrid'; + +app.initializers.add('reports', () => { + extend(PermissionGrid.prototype, 'moderateItems', items => { + items.add('viewReports', { + label: 'View reported posts', + permission: 'discussion.viewReports' + }); + }); + + extend(PermissionGrid.prototype, 'replyItems', items => { + items.add('reportPosts', { + label: 'Report posts', + permission: 'discussion.reportPosts' + }); + }); +}); diff --git a/extensions/flags/js/forum/Gulpfile.js b/extensions/flags/js/forum/Gulpfile.js new file mode 100644 index 000000000..9db4f9477 --- /dev/null +++ b/extensions/flags/js/forum/Gulpfile.js @@ -0,0 +1,7 @@ +var gulp = require('flarum-gulp'); + +gulp({ + modules: { + 'reports': 'src/**/*.js' + } +}); diff --git a/extensions/flags/js/forum/package.json b/extensions/flags/js/forum/package.json new file mode 100644 index 000000000..62ea6c691 --- /dev/null +++ b/extensions/flags/js/forum/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "devDependencies": { + "gulp": "^3.8.11", + "flarum-gulp": "^0.1.0" + } +} diff --git a/extensions/flags/js/forum/src/addReportControl.js b/extensions/flags/js/forum/src/addReportControl.js new file mode 100644 index 000000000..ae3f4aa46 --- /dev/null +++ b/extensions/flags/js/forum/src/addReportControl.js @@ -0,0 +1,16 @@ +import { extend } from 'flarum/extend'; +import app from 'flarum/app'; +import PostControls from 'flarum/utils/PostControls'; +import Button from 'flarum/components/Button'; + +import ReportPostModal from 'reports/components/ReportPostModal'; + +export default function() { + extend(PostControls, 'userControls', function(items, post) { + if (post.isHidden() || post.contentType() !== 'comment' || !post.canReport() || post.user() === app.session.user) return; + + items.add('report', + + ); + }); +} diff --git a/extensions/flags/js/forum/src/addReportsDropdown.js b/extensions/flags/js/forum/src/addReportsDropdown.js new file mode 100644 index 000000000..f887ba722 --- /dev/null +++ b/extensions/flags/js/forum/src/addReportsDropdown.js @@ -0,0 +1,12 @@ +import { extend } from 'flarum/extend'; +import app from 'flarum/app'; +import HeaderSecondary from 'flarum/components/HeaderSecondary'; +import ReportsDropdown from 'reports/components/ReportsDropdown'; + +export default function() { + extend(HeaderSecondary.prototype, 'items', function(items) { + if (app.forum.attribute('canViewReports')) { + items.add('reports', , 15); + } + }); +} diff --git a/extensions/flags/js/forum/src/addReportsToPosts.js b/extensions/flags/js/forum/src/addReportsToPosts.js new file mode 100644 index 000000000..bc35d0ed4 --- /dev/null +++ b/extensions/flags/js/forum/src/addReportsToPosts.js @@ -0,0 +1,134 @@ +import { extend } from 'flarum/extend'; +import app from 'flarum/app'; +import CommentPost from 'flarum/components/CommentPost'; +import Button from 'flarum/components/Button'; +import punctuate from 'flarum/helpers/punctuate'; +import username from 'flarum/helpers/username'; +import ItemList from 'flarum/utils/ItemList'; +import PostControls from 'flarum/utils/PostControls'; + +export default function() { + extend(CommentPost.prototype, 'attrs', function(attrs) { + if (this.props.post.reports().length) { + attrs.className += ' Post--reported'; + } + }); + + CommentPost.prototype.dismissReport = function(data) { + const post = this.props.post; + + delete post.data.relationships.reports; + + this.subtree.invalidate(); + + if (app.cache.reports) { + app.cache.reports.some((report, i) => { + if (report.post() === post) { + app.cache.reports.splice(i, 1); + + if (app.cache.reportIndex === post) { + let next = app.cache.reports[i]; + + if (!next) next = app.cache.reports[0]; + + if (next) { + const nextPost = next.post(); + app.cache.reportIndex = nextPost; + m.route(app.route.post(nextPost)); + } + } + + return true; + } + }); + } + + return app.request({ + url: app.forum.attribute('apiUrl') + post.apiEndpoint() + '/reports', + method: 'DELETE', + data + }); + }; + + CommentPost.prototype.reportActionItems = function() { + const items = new ItemList(); + + if (this.props.post.isHidden()) { + if (this.props.post.canDelete()) { + items.add('delete', + , + 100 + ); + } + } else { + items.add('hide', + , + 100 + ); + } + + items.add('dismiss', , -100); + + return items; + }; + + extend(CommentPost.prototype, 'content', function(vdom) { + const post = this.props.post; + const reports = post.reports(); + + if (!reports.length) return; + + if (post.isHidden()) this.revealContent = true; + + const users = reports.map(report => { + const user = report.user(); + + return user + ? {username(user)} + : report.reporter(); + }); + + const usedReasons = []; + const reasons = reports.map(report => report.reason()).filter(reason => { + if (reason && usedReasons.indexOf(reason) === -1) { + usedReasons.push(reason); + return true; + } + }); + + const details = reports.map(report => report.reasonDetail()).filter(detail => detail); + + vdom.unshift( +
+
+ {app.trans(reasons.length ? 'reports.reported_by_with_reason' : 'reports.reported_by', { + reasons: punctuate(reasons.map(reason => app.trans('reports.reason_' + reason, undefined, reason))), + users: punctuate(users) + })} + {details.map(detail =>
{detail}
)} +
+
+ {this.reportActionItems().toArray()} +
+
+ ); + }); +} diff --git a/extensions/flags/js/forum/src/components/ReportList.js b/extensions/flags/js/forum/src/components/ReportList.js new file mode 100644 index 000000000..7cb0f483b --- /dev/null +++ b/extensions/flags/js/forum/src/components/ReportList.js @@ -0,0 +1,83 @@ +import Component from 'flarum/Component'; +import LoadingIndicator from 'flarum/components/LoadingIndicator'; +import avatar from 'flarum/helpers/avatar'; +import username from 'flarum/helpers/username'; +import icon from 'flarum/helpers/icon'; +import humanTime from 'flarum/helpers/humanTime'; + +export default class ReportList extends Component { + constructor(...args) { + super(...args); + + /** + * Whether or not the notifications are loading. + * + * @type {Boolean} + */ + this.loading = false; + } + + view() { + const reports = app.cache.reports || []; + + return ( +
+
+

Reported Posts

+
+
+ +
+
+ ); + } + + /** + * Load reports into the application's cache if they haven't already + * been loaded. + */ + load() { + if (app.cache.reports && !app.forum.attribute('unreadReportsCount')) { + return; + } + + this.loading = true; + m.redraw(); + + app.store.find('reports').then(reports => { + app.forum.pushAttributes({unreadReportsCount: 0}); + app.cache.reports = reports.sort((a, b) => b.time() - a.time()); + + this.loading = false; + m.redraw(); + }); + } +} diff --git a/extensions/flags/js/forum/src/components/ReportPostModal.js b/extensions/flags/js/forum/src/components/ReportPostModal.js new file mode 100644 index 000000000..ce335caf4 --- /dev/null +++ b/extensions/flags/js/forum/src/components/ReportPostModal.js @@ -0,0 +1,85 @@ +import Modal from 'flarum/components/Modal'; +import Button from 'flarum/components/Button'; + +export default class ReportPostModal extends Modal { + constructor(...args) { + super(...args); + + this.reason = m.prop(''); + this.reasonDetail = m.prop(''); + } + + className() { + return 'ReportPostModal Modal--small'; + } + + title() { + return 'Report Post'; + } + + content() { + return ( +
+
+
+ +
+ + + + + + + +
+
+ +
+ {Button.component({ + children: 'Report Post', + className: 'Button Button--primary', + loading: this.loading, + disabled: !this.reason() + })} +
+
+
+ ); + } + + onsubmit(e) { + e.preventDefault(); + + this.loading = true; + + app.store.createRecord('reports').save({ + reason: this.reason() === 'other' ? null : this.reason(), + reasonDetail: this.reasonDetail(), + relationships: { + user: app.session.user, + post: this.props.post + } + }).then( + () => this.hide(), + () => { + this.loading = false; + m.redraw(); + } + ); + } +} diff --git a/extensions/flags/js/forum/src/components/ReportsDropdown.js b/extensions/flags/js/forum/src/components/ReportsDropdown.js new file mode 100644 index 000000000..8aadb271a --- /dev/null +++ b/extensions/flags/js/forum/src/components/ReportsDropdown.js @@ -0,0 +1,26 @@ +import NotificationsDropdown from 'flarum/components/NotificationsDropdown'; + +import ReportList from 'reports/components/ReportList'; + +export default class ReportsDropdown extends NotificationsDropdown { + static initProps(props) { + props.label = props.label || 'Reports'; + props.icon = props.icon || 'flag'; + + super.initProps(props); + } + + constructor(...args) { + super(...args); + + this.list = new ReportList(); + } + + goToRoute() { + m.route(app.route('reports')); + } + + getUnreadCount() { + return app.forum.attribute('unreadReportsCount'); + } +} diff --git a/extensions/flags/js/forum/src/components/ReportsPage.js b/extensions/flags/js/forum/src/components/ReportsPage.js new file mode 100644 index 000000000..e588496f5 --- /dev/null +++ b/extensions/flags/js/forum/src/components/ReportsPage.js @@ -0,0 +1,24 @@ +import Page from 'flarum/components/Page'; + +import ReportList from 'reports/components/ReportList'; + +/** + * The `ReportsPage` component shows the reports list. It is only + * used on mobile devices where the reports dropdown is within the drawer. + */ +export default class ReportsPage extends Page { + constructor(...args) { + super(...args); + + app.history.push('reports'); + + this.list = new ReportList(); + this.list.load(); + + this.bodyClass = 'App--reports'; + } + + view() { + return
{this.list.render()}
; + } +} diff --git a/extensions/flags/js/forum/src/main.js b/extensions/flags/js/forum/src/main.js new file mode 100644 index 000000000..19256b252 --- /dev/null +++ b/extensions/flags/js/forum/src/main.js @@ -0,0 +1,21 @@ +import app from 'flarum/app'; +import Model from 'flarum/Model'; + +import Report from 'reports/models/Report'; +import ReportsPage from 'reports/components/ReportsPage'; +import addReportControl from 'reports/addReportControl'; +import addReportsDropdown from 'reports/addReportsDropdown'; +import addReportsToPosts from 'reports/addReportsToPosts'; + +app.initializers.add('reports', () => { + app.store.models.posts.prototype.reports = Model.hasMany('reports'); + app.store.models.posts.prototype.canReport = Model.attribute('canReport'); + + app.store.models.reports = Report; + + app.routes.reports = {path: '/reports', component: }; + + addReportControl(); + addReportsDropdown(); + addReportsToPosts(); +}); diff --git a/extensions/flags/js/forum/src/models/Report.js b/extensions/flags/js/forum/src/models/Report.js new file mode 100644 index 000000000..f539cc42f --- /dev/null +++ b/extensions/flags/js/forum/src/models/Report.js @@ -0,0 +1,12 @@ +import Model from 'flarum/Model'; +import mixin from 'flarum/utils/mixin'; + +export default class Report extends mixin(Model, { + reporter: Model.attribute('reporter'), + reason: Model.attribute('reason'), + reasonDetail: Model.attribute('reasonDetail'), + time: Model.attribute('time', Model.transformDate), + + post: Model.hasOne('post'), + user: Model.hasOne('user') +}) {} diff --git a/extensions/flags/less/admin/extension.less b/extensions/flags/less/admin/extension.less new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/flags/less/forum/extension.less b/extensions/flags/less/forum/extension.less new file mode 100644 index 000000000..5139037f3 --- /dev/null +++ b/extensions/flags/less/forum/extension.less @@ -0,0 +1,59 @@ +.Post--reported { + padding-top: 0 !important; + border: 2px solid @primary-color; + + .Post-controls { + display: none; + } +} + +.Post-header .item-reported { + display: block; + margin: 0; +} +.Post-reported { + background: @primary-color; + margin-top: -2px; + margin-bottom: 20px; + margin-left: -22px; + margin-right: -22px; + + @media @tablet-up { + margin-left: -22px - 90px; + text-align: right; + } + + padding: 10px; + border-radius: @border-radius @border-radius 0 0; + overflow: hidden; + .light-contents(@color: @body-bg; @control-color: @body-bg); + + &, a { + color: @body-bg !important; + } +} +.Post-reported-summary { + @media @tablet-up { + float: left; + } + + font-size: 14px; + margin: 7px 10px; + text-align: left; + font-weight: bold; +} +.Post-reported-detail { + font-size: 12px; + margin-top: 5px; + font-weight: normal; +} +.Post-reported-actions .Button { + margin-left: 5px; +} + +.ReportsDropdown .Dropdown-toggle { + .Button-label, + .Button-caret { + display: none; + } +} diff --git a/extensions/flags/locale/en.yml b/extensions/flags/locale/en.yml new file mode 100644 index 000000000..3a43bcf5c --- /dev/null +++ b/extensions/flags/locale/en.yml @@ -0,0 +1,8 @@ +reports: + reason_off_topic: Off-topic + reason_spam: Spam + reason_inappropriate: Inappropriate + reason_other: Other + reported_by: "Reported by {users}" + reported_by_with_reason: "Reported as {reasons} by {users}" + no_reports: No Reports diff --git a/extensions/flags/migrations/2015_09_02_000000_add_reports_read_time_to_users_table.php b/extensions/flags/migrations/2015_09_02_000000_add_reports_read_time_to_users_table.php new file mode 100644 index 000000000..2d6cf6d20 --- /dev/null +++ b/extensions/flags/migrations/2015_09_02_000000_add_reports_read_time_to_users_table.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Illuminate\Database\Schema\Blueprint; +use Flarum\Migrations\Migration; + +class AddReportsReadTimeToUsersTable extends Migration +{ + public function up() + { + $this->schema->table('users', function (Blueprint $table) { + $table->dateTime('reports_read_time')->nullable(); + }); + } + + public function down() + { + $this->schema->drop('reports_read_time'); + } +} diff --git a/extensions/flags/migrations/2015_09_02_000000_create_reports_table.php b/extensions/flags/migrations/2015_09_02_000000_create_reports_table.php new file mode 100644 index 000000000..914037250 --- /dev/null +++ b/extensions/flags/migrations/2015_09_02_000000_create_reports_table.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Illuminate\Database\Schema\Blueprint; +use Flarum\Migrations\Migration; + +class CreateReportsTable extends Migration +{ + public function up() + { + $this->schema->create('reports', function (Blueprint $table) { + $table->increments('id'); + $table->integer('post_id')->unsigned(); + $table->integer('user_id')->unsigned(); + $table->string('reporter')->nullable(); + $table->string('reason')->nullable(); + $table->string('reason_detail')->nullable(); + $table->dateTime('time'); + }); + } + + public function down() + { + $this->schema->drop('reports'); + } +} diff --git a/extensions/flags/src/Api/CreateAction.php b/extensions/flags/src/Api/CreateAction.php new file mode 100644 index 000000000..d9c3a2d62 --- /dev/null +++ b/extensions/flags/src/Api/CreateAction.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Reports\Api; + +use Flarum\Reports\Commands\CreateReport; +use Flarum\Api\Actions\CreateAction as BaseCreateAction; +use Flarum\Api\JsonApiRequest; +use Illuminate\Contracts\Bus\Dispatcher; + +class CreateAction extends BaseCreateAction +{ + /** + * @var Dispatcher + */ + protected $bus; + + /** + * @inheritdoc + */ + public $serializer = 'Flarum\Reports\Api\ReportSerializer'; + + /** + * @inheritdoc + */ + public $include = [ + 'post' => true, + 'post.reports' => true + ]; + + /** + * @param Dispatcher $bus + */ + public function __construct(Dispatcher $bus) + { + $this->bus = $bus; + } + + /** + * Create a report according to input from the API request. + * + * @param JsonApiRequest $request + * @return \Flarum\Reports\Report + */ + protected function create(JsonApiRequest $request) + { + return $this->bus->dispatch( + new CreateReport($request->actor, $request->get('data')) + ); + } +} diff --git a/extensions/flags/src/Api/DeleteAction.php b/extensions/flags/src/Api/DeleteAction.php new file mode 100644 index 000000000..90c527a86 --- /dev/null +++ b/extensions/flags/src/Api/DeleteAction.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Reports\Api; + +use Flarum\Reports\Commands\DeleteReports; +use Flarum\Api\Actions\DeleteAction as BaseDeleteAction; +use Flarum\Api\Request; +use Illuminate\Contracts\Bus\Dispatcher; + +class DeleteAction extends BaseDeleteAction +{ + /** + * @var Dispatcher + */ + protected $bus; + + /** + * @param Dispatcher $bus + */ + public function __construct(Dispatcher $bus) + { + $this->bus = $bus; + } + + /** + * Delete reports for a post. + * + * @param Request $request + */ + protected function delete(Request $request) + { + $this->bus->dispatch( + new DeleteReports($request->get('id'), $request->actor, $request->all()) + ); + } +} diff --git a/extensions/flags/src/Api/IndexAction.php b/extensions/flags/src/Api/IndexAction.php new file mode 100644 index 000000000..8c39f3857 --- /dev/null +++ b/extensions/flags/src/Api/IndexAction.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Reports\Api; + +use Flarum\Api\Actions\SerializeCollectionAction; +use Flarum\Api\JsonApiRequest; +use Flarum\Reports\Report; +use Tobscure\JsonApi\Document; + +class IndexAction extends SerializeCollectionAction +{ + /** + * @inheritdoc + */ + public $serializer = 'Flarum\Reports\Api\ReportSerializer'; + + /** + * @inheritdoc + */ + public $include = [ + 'user' => true, + 'post' => true, + 'post.user' => true, + 'post.discussion' => true + ]; + + /** + * @inheritdoc + */ + public $link = []; + + protected function data(JsonApiRequest $request, Document $document) + { + $actor = $request->actor; + + $actor->reports_read_time = time(); + $actor->save(); + + return Report::whereVisibleTo($actor) + ->with($request->include) + ->latest('reports.time') + ->groupBy('post_id') + ->get(); + } +} diff --git a/extensions/flags/src/Api/ReportSerializer.php b/extensions/flags/src/Api/ReportSerializer.php new file mode 100644 index 000000000..8d05a536c --- /dev/null +++ b/extensions/flags/src/Api/ReportSerializer.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Reports\Api; + +use Flarum\Api\Serializers\Serializer; + +class ReportSerializer extends Serializer +{ + protected $type = 'reports'; + + protected function getDefaultAttributes($report) + { + return [ + 'reporter' => $report->reporter, + 'reason' => $report->reason, + 'reasonDetail' => $report->reason_detail, + ]; + } + + protected function post() + { + return $this->hasOne('Flarum\Api\Serializers\PostSerializer'); + } + + protected function user() + { + return $this->hasOne('Flarum\Api\Serializers\UserBasicSerializer'); + } +} diff --git a/extensions/flags/src/Commands/CreateReport.php b/extensions/flags/src/Commands/CreateReport.php new file mode 100644 index 000000000..92c1ae2cf --- /dev/null +++ b/extensions/flags/src/Commands/CreateReport.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Reports\Commands; + +use Flarum\Core\Users\User; + +class CreateReport +{ + /** + * The user performing the action. + * + * @var User + */ + public $actor; + + /** + * The attributes of the new report. + * + * @var array + */ + public $data; + + /** + * @param User $actor The user performing the action. + * @param array $data The attributes of the new report. + */ + public function __construct(User $actor, array $data) + { + $this->actor = $actor; + $this->data = $data; + } +} diff --git a/extensions/flags/src/Commands/CreateReportHandler.php b/extensions/flags/src/Commands/CreateReportHandler.php new file mode 100644 index 000000000..8a9040ee7 --- /dev/null +++ b/extensions/flags/src/Commands/CreateReportHandler.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Reports\Commands; + +use Flarum\Reports\Report; +use Flarum\Core\Posts\PostRepository; +use Flarum\Core\Posts\CommentPost; +use Exception; + +class CreateReportHandler +{ + private $posts; + + public function __construct(PostRepository $posts) + { + $this->posts = $posts; + } + + /** + * @param CreateReport $command + * @return Report + */ + public function handle(CreateReport $command) + { + $actor = $command->actor; + $data = $command->data; + + $postId = array_get($data, 'relationships.post.data.id'); + $post = $this->posts->findOrFail($postId, $actor); + + if (! ($post instanceof CommentPost)) { + // TODO: throw 400(?) error + throw new Exception; + } + + $post->assertCan($actor, 'report'); + + Report::unguard(); + + $report = Report::firstOrNew([ + 'post_id' => $post->id, + 'user_id' => $actor->id + ]); + + $report->post_id = $post->id; + $report->user_id = $actor->id; + $report->reason = array_get($data, 'attributes.reason'); + $report->reason_detail = array_get($data, 'attributes.reasonDetail'); + $report->time = time(); + + $report->save(); + + return $report; + } +} diff --git a/extensions/flags/src/Commands/DeleteReports.php b/extensions/flags/src/Commands/DeleteReports.php new file mode 100644 index 000000000..e1ffd6b09 --- /dev/null +++ b/extensions/flags/src/Commands/DeleteReports.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Reports\Commands; + +use Flarum\Reports\Report; +use Flarum\Core\Users\User; + +class DeleteReports +{ + /** + * The ID of the post to delete reports for. + * + * @var int + */ + public $postId; + + /** + * The user performing the action. + * + * @var User + */ + public $actor; + + /** + * @var array + */ + public $data; + + /** + * @param int $postId The ID of the post to delete reports for. + * @param User $actor The user performing the action. + * @param array $data + */ + public function __construct($postId, User $actor, array $data = []) + { + $this->postId = $postId; + $this->actor = $actor; + $this->data = $data; + } +} diff --git a/extensions/flags/src/Commands/DeleteReportsHandler.php b/extensions/flags/src/Commands/DeleteReportsHandler.php new file mode 100644 index 000000000..3cebff1de --- /dev/null +++ b/extensions/flags/src/Commands/DeleteReportsHandler.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Reports\Commands; + +use Flarum\Reports\Report; +use Flarum\Core\Posts\PostRepository; +use Flarum\Reports\Events\ReportsWillBeDeleted; + +class DeleteReportsHandler +{ + protected $posts; + + public function __construct(PostRepository $posts) + { + $this->posts = $posts; + } + + /** + * @param DeleteReport $command + * @return Report + * @throws \Flarum\Core\Exceptions\PermissionDeniedException + */ + public function handle(DeleteReports $command) + { + $actor = $command->actor; + + $post = $this->posts->findOrFail($command->postId, $actor); + + $post->discussion->assertCan($actor, 'viewReports'); + + event(new ReportsWillBeDeleted($post, $actor, $command->data)); + + $post->reports()->delete(); + + return $post; + } +} diff --git a/extensions/flags/src/Events/ReportsWillBeDeleted.php b/extensions/flags/src/Events/ReportsWillBeDeleted.php new file mode 100644 index 000000000..92dafe6a9 --- /dev/null +++ b/extensions/flags/src/Events/ReportsWillBeDeleted.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Reports\Events; + +use Flarum\Core\Posts\Post; +use Flarum\Core\Users\User; + +class ReportsWillBeDeleted +{ + /** + * @var Post + */ + public $post; + + /** + * @var User + */ + public $actor; + + /** + * @var array + */ + public $data; + + /** + * @param Post $post + * @param User $actor + * @param array $data + */ + public function __construct(Post $post, User $actor, array $data = []) + { + $this->post = $post; + $this->actor = $actor; + $this->data = $data; + } +} diff --git a/extensions/flags/src/Extension.php b/extensions/flags/src/Extension.php new file mode 100644 index 000000000..bcee8fe97 --- /dev/null +++ b/extensions/flags/src/Extension.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Reports; + +use Flarum\Support\Extension as BaseExtension; +use Illuminate\Events\Dispatcher; + +class Extension extends BaseExtension +{ + public function listen(Dispatcher $events) + { + $events->subscribe('Flarum\Reports\Listeners\AddClientAssets'); + $events->subscribe('Flarum\Reports\Listeners\AddApiAttributes'); + $events->subscribe('Flarum\Reports\Listeners\AddModelRelationship'); + } +} diff --git a/extensions/flags/src/Listeners/AddApiAttributes.php b/extensions/flags/src/Listeners/AddApiAttributes.php new file mode 100755 index 000000000..a24cd3557 --- /dev/null +++ b/extensions/flags/src/Listeners/AddApiAttributes.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Reports\Listeners; + +use Flarum\Events\ApiRelationship; +use Flarum\Events\WillSerializeData; +use Flarum\Events\BuildApiAction; +use Flarum\Events\ApiAttributes; +use Flarum\Events\RegisterApiRoutes; +use Flarum\Api\Serializers\PostSerializer; +use Flarum\Api\Serializers\ForumSerializer; +use Flarum\Api\Actions\Posts; +use Flarum\Api\Actions\Discussions; +use Flarum\Reports\Report; +use Flarum\Reports\Api\CreateAction as ReportsCreateAction; +use Illuminate\Database\Eloquent\Collection; + +class AddApiAttributes +{ + public function subscribe($events) + { + $events->listen(ApiRelationship::class, [$this, 'addReportsRelationship']); + $events->listen(WillSerializeData::class, [$this, 'loadReportsRelationship']); + $events->listen(BuildApiAction::class, [$this, 'includeReportsRelationship']); + $events->listen(ApiAttributes::class, [$this, 'addAttributes']); + $events->listen(RegisterApiRoutes::class, [$this, 'addRoutes']); + } + + public function loadReportsRelationship(WillSerializeData $event) + { + // For any API action that allows the 'reports' relationship to be + // included, we need to preload this relationship onto the data (Post + // models) so that we can selectively expose only the reports that the + // user has permission to view. + if ($event->action instanceof Discussions\ShowAction) { + $discussion = $event->data; + $posts = $discussion->posts->all(); + } + + if ($event->action instanceof Posts\IndexAction) { + $posts = $event->data->all(); + } + + if ($event->action instanceof Posts\ShowAction) { + $posts = [$event->data]; + } + + if ($event->action instanceof ReportsCreateAction) { + $report = $event->data; + $posts = [$report->post]; + } + + if (isset($posts)) { + $actor = $event->request->actor; + $postsWithPermission = []; + + foreach ($posts as $post) { + $post->setRelation('reports', null); + + if ($post->discussion->can($actor, 'viewReports')) { + $postsWithPermission[] = $post; + } + } + + if (count($postsWithPermission)) { + (new Collection($postsWithPermission)) + ->load('reports', 'reports.user'); + } + } + } + + public function addReportsRelationship(ApiRelationship $event) + { + if ($event->serializer instanceof PostSerializer && + $event->relationship === 'reports') { + return $event->serializer->hasMany('Flarum\Reports\Api\ReportSerializer', 'reports'); + } + } + + public function includeReportsRelationship(BuildApiAction $event) + { + if ($event->action instanceof Discussions\ShowAction) { + $event->addInclude('posts.reports'); + $event->addInclude('posts.reports.user'); + } + + if ($event->action instanceof Posts\IndexAction || + $event->action instanceof Posts\ShowAction) { + $event->addInclude('reports'); + $event->addInclude('reports.user'); + } + } + + public function addAttributes(ApiAttributes $event) + { + if ($event->serializer instanceof ForumSerializer) { + $event->attributes['canViewReports'] = $event->actor->hasPermissionLike('discussion.viewReports'); + + if ($event->attributes['canViewReports']) { + $query = Report::whereVisibleTo($event->actor); + + if ($time = $event->actor->reports_read_time) { + $query->where('reports.time', '>', $time); + } + + $event->attributes['unreadReportsCount'] = $query->distinct('reports.post_id')->count(); + } + } + + if ($event->serializer instanceof PostSerializer) { + $event->attributes['canReport'] = $event->model->can($event->actor, 'report'); + } + } + + public function addRoutes(RegisterApiRoutes $event) + { + $event->get('/reports', 'reports.index', 'Flarum\Reports\Api\IndexAction'); + $event->post('/reports', 'reports.create', 'Flarum\Reports\Api\CreateAction'); + $event->delete('/posts/{id}/reports', 'reports.delete', 'Flarum\Reports\Api\DeleteAction'); + } +} diff --git a/extensions/flags/src/Listeners/AddClientAssets.php b/extensions/flags/src/Listeners/AddClientAssets.php new file mode 100644 index 000000000..0171b79ad --- /dev/null +++ b/extensions/flags/src/Listeners/AddClientAssets.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Reports\Listeners; + +use Flarum\Events\RegisterLocales; +use Flarum\Events\BuildClientView; +use Illuminate\Contracts\Events\Dispatcher; + +class AddClientAssets +{ + public function subscribe(Dispatcher $events) + { + $events->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('reports/main'); + + $event->forumTranslations([ + 'reports.reason_off_topic', + 'reports.reason_spam', + 'reports.reason_inappropriate', + 'reports.reason_other', + 'reports.reported_by', + 'reports.reported_by_with_reason', + 'reports.no_reports' + ]); + + $event->adminAssets([ + __DIR__.'/../../js/admin/dist/extension.js', + __DIR__.'/../../less/admin/extension.less' + ]); + + $event->adminBootstrapper('reports/main'); + + $event->adminTranslations([ + // 'report.hello_world' + ]); + } +} diff --git a/extensions/flags/src/Listeners/AddModelRelationship.php b/extensions/flags/src/Listeners/AddModelRelationship.php new file mode 100755 index 000000000..8df04624c --- /dev/null +++ b/extensions/flags/src/Listeners/AddModelRelationship.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Reports\Listeners; + +use Flarum\Events\ModelRelationship; +use Flarum\Events\ModelDates; +use Flarum\Core\Posts\Post; +use Flarum\Core\Users\User; +use Flarum\Reports\Report; + +class AddModelRelationship +{ + public function subscribe($events) + { + $events->listen(ModelRelationship::class, [$this, 'addReportsRelationship']); + $events->listen(ModelDates::class, [$this, 'modelDates']); + } + + public function addReportsRelationship(ModelRelationship $event) + { + if ($event->model instanceof Post && $event->relationship === 'reports') { + return $event->model->hasMany('Flarum\Reports\Report', 'post_id'); + } + } + + public function modelDates(ModelDates $event) + { + if ($event->model instanceof User) { + $event->dates[] = 'reports_read_time'; + } + } +} diff --git a/extensions/flags/src/Report.php b/extensions/flags/src/Report.php new file mode 100644 index 000000000..32766725a --- /dev/null +++ b/extensions/flags/src/Report.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Reports; + +use Flarum\Core\Model; +use Flarum\Core\Support\VisibleScope; + +class Report extends Model +{ + use VisibleScope; + + protected $table = 'reports'; + + protected $dates = ['time']; + + public function post() + { + return $this->belongsTo('Flarum\Core\Posts\Post'); + } + + public function user() + { + return $this->belongsTo('Flarum\Core\Users\User'); + } +}