1
0
mirror of https://github.com/flarum/core.git synced 2025-07-30 21:20:24 +02:00

Rename to Flags, tweak flag controls

- Display post "destructiveControls" in flag instead of custom buttons
- Make flags more versatile/extensible
- Delete associated flags when a post is deleted

Uninstall the Reports extension before installing.
This commit is contained in:
Toby Zerner
2015-09-22 18:14:33 +09:30
parent 8da417c5b5
commit a1e01938ef
36 changed files with 390 additions and 401 deletions

View File

@@ -2,6 +2,6 @@ var gulp = require('flarum-gulp');
gulp({
modules: {
'reports': 'src/**/*.js'
'flags': 'src/**/*.js'
}
});

View File

@@ -2,18 +2,20 @@ import { extend } from 'flarum/extend';
import app from 'flarum/app';
import PermissionGrid from 'flarum/components/PermissionGrid';
app.initializers.add('reports', () => {
app.initializers.add('flags', () => {
extend(PermissionGrid.prototype, 'moderateItems', items => {
items.add('viewReports', {
label: 'View reported posts',
permission: 'discussion.viewReports'
});
items.add('viewFlags', {
icon: 'flag',
label: 'View flagged posts',
permission: 'discussion.viewFlags'
}, 65);
});
extend(PermissionGrid.prototype, 'replyItems', items => {
items.add('reportPosts', {
label: 'Report posts',
permission: 'discussion.reportPosts'
});
items.add('flagPosts', {
icon: 'flag',
label: 'Flag posts',
permission: 'discussion.flagPosts'
}, 70);
});
});

View File

@@ -2,6 +2,6 @@ var gulp = require('flarum-gulp');
gulp({
modules: {
'reports': 'src/**/*.js'
'flags': 'src/**/*.js'
}
});

View File

@@ -3,14 +3,14 @@ import app from 'flarum/app';
import PostControls from 'flarum/utils/PostControls';
import Button from 'flarum/components/Button';
import ReportPostModal from 'reports/components/ReportPostModal';
import FlagPostModal from 'flags/components/FlagPostModal';
export default function() {
extend(PostControls, 'userControls', function(items, post) {
if (post.isHidden() || post.contentType() !== 'comment' || !post.canReport() || post.user() === app.session.user) return;
if (post.isHidden() || post.contentType() !== 'comment' || !post.canFlag() || post.user() === app.session.user) return;
items.add('report',
<Button icon="flag" onclick={() => app.modal.show(new ReportPostModal({post}))}>Report</Button>
items.add('flag',
<Button icon="flag" onclick={() => app.modal.show(new FlagPostModal({post}))}>Flag</Button>
);
});
}

View File

@@ -1,12 +1,12 @@
import { extend } from 'flarum/extend';
import app from 'flarum/app';
import HeaderSecondary from 'flarum/components/HeaderSecondary';
import ReportsDropdown from 'reports/components/ReportsDropdown';
import FlagsDropdown from 'flags/components/FlagsDropdown';
export default function() {
extend(HeaderSecondary.prototype, 'items', function(items) {
if (app.forum.attribute('canViewReports')) {
items.add('reports', <ReportsDropdown/>, 15);
if (app.forum.attribute('canViewFlags')) {
items.add('flags', <FlagsDropdown/>, 15);
}
});
}

View File

@@ -0,0 +1,109 @@
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.flags().length) {
attrs.className += ' Post--flagged';
}
});
CommentPost.prototype.dismissFlag = function(data) {
const post = this.props.post;
delete post.data.relationships.flags;
this.subtree.invalidate();
if (app.cache.flags) {
app.cache.flags.some((flag, i) => {
if (flag.post() === post) {
app.cache.flags.splice(i, 1);
if (app.cache.flagIndex === post) {
let next = app.cache.flags[i];
if (!next) next = app.cache.flags[0];
if (next) {
const nextPost = next.post();
app.cache.flagIndex = nextPost;
m.route(app.route.post(nextPost));
}
}
return true;
}
});
}
return app.request({
url: app.forum.attribute('apiUrl') + post.apiEndpoint() + '/flags',
method: 'DELETE',
data
});
};
CommentPost.prototype.flagActionItems = function() {
const items = new ItemList();
const controls = PostControls.destructiveControls(this.props.post);
Object.keys(controls).forEach(k => {
const props = controls[k].content.props;
props.className = 'Button';
extend(props, 'onclick', () => this.dismissFlag());
});
items.merge(controls);
items.add('dismiss', <Button className="Button Button--icon Button--link" icon="times" onclick={this.dismissFlag.bind(this)} title="Dismiss Flag"/>, -100);
return items;
};
extend(CommentPost.prototype, 'content', function(vdom) {
const post = this.props.post;
const flags = post.flags();
if (!flags.length) return;
if (post.isHidden()) this.revealContent = true;
vdom.unshift(
<div className="Post-flagged">
<div className="Post-flagged-flags">
{flags.map(flag =>
<div className="Post-flagged-flag">
{this.flagReason(flag)}
</div>
)}
</div>
<div className="Post-flagged-actions">
{this.flagActionItems().toArray()}
</div>
</div>
);
});
CommentPost.prototype.flagReason = function(flag) {
if (flag.type() === 'user') {
const user = flag.user();
const reason = flag.reason();
const detail = flag.reasonDetail();
return [
app.trans(reason ? 'flags.flagged_by_with_reason' : 'flags.flagged_by', {user, reason}),
detail ? <span className="Post-flagged-detail">{detail}</span> : ''
];
}
};
}

View File

@@ -1,134 +0,0 @@
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',
<Button className="Button"
icon="trash-o"
onclick={() => {
this.dismissReport().then(() => {
PostControls.deleteAction.apply(this.props.post);
m.redraw();
});
}}>
Delete Forever
</Button>,
100
);
}
} else {
items.add('hide',
<Button className="Button"
icon="trash-o"
onclick={() => {
this.dismissReport().then(() => {
PostControls.hideAction.apply(this.props.post);
m.redraw();
});
}}>
Delete Post
</Button>,
100
);
}
items.add('dismiss', <Button className="Button Button--icon Button--link" icon="times" onclick={this.dismissReport.bind(this)}>Dismiss Report</Button>, -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
? <a href={app.route.user(user)} config={m.route}>{username(user)}</a>
: 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(
<div className="Post-reported">
<div className="Post-reported-summary">
{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 => <div className="Post-reported-detail">{detail}</div>)}
</div>
<div className="Post-reported-actions">
{this.reportActionItems().toArray()}
</div>
</div>
);
});
}

View File

@@ -5,7 +5,7 @@ import username from 'flarum/helpers/username';
import icon from 'flarum/helpers/icon';
import humanTime from 'flarum/helpers/humanTime';
export default class ReportList extends Component {
export default class FlagList extends Component {
constructor(...args) {
super(...args);
@@ -18,32 +18,32 @@ export default class ReportList extends Component {
}
view() {
const reports = app.cache.reports || [];
const flags = app.cache.flags || [];
return (
<div className="NotificationList ReportList">
<div className="NotificationList FlagList">
<div className="NotificationList-header">
<h4 className="App-titleControl App-titleControl--text">Reported Posts</h4>
<h4 className="App-titleControl App-titleControl--text">Flagged Posts</h4>
</div>
<div className="NotificationList-content">
<ul className="NotificationGroup-content">
{reports.length
? reports.map(report => {
const post = report.post();
{flags.length
? flags.map(flag => {
const post = flag.post();
return (
<li>
<a href={app.route.post(post)} className="Notification Report" config={function(element, isInitialized) {
<a href={app.route.post(post)} className="Notification Flag" config={function(element, isInitialized) {
m.route.apply(this, arguments);
if (!isInitialized) $(element).on('click', () => app.cache.reportIndex = post);
if (!isInitialized) $(element).on('click', () => app.cache.flagIndex = post);
}}>
{avatar(post.user())}
{icon('flag', {className: 'Notification-icon'})}
<span className="Notification-content">
{username(post.user())} in <em>{post.discussion().title()}</em>
</span>
{humanTime(report.time())}
{humanTime(flag.time())}
<div className="Notification-excerpt">
{post.contentPlain()}
</div>
@@ -52,7 +52,7 @@ export default class ReportList extends Component {
);
})
: !this.loading
? <div className="NotificationList-empty">{app.trans('reports.no_reports')}</div>
? <div className="NotificationList-empty">{app.trans('flags.no_flags')}</div>
: LoadingIndicator.component({className: 'LoadingIndicator--block'})}
</ul>
</div>
@@ -61,20 +61,20 @@ export default class ReportList extends Component {
}
/**
* Load reports into the application's cache if they haven't already
* Load flags into the application's cache if they haven't already
* been loaded.
*/
load() {
if (app.cache.reports && !app.forum.attribute('unreadReportsCount')) {
if (app.cache.flags && !app.forum.attribute('unreadFlagsCount')) {
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());
app.store.find('flags').then(flags => {
app.forum.pushAttributes({unreadFlagsCount: 0});
app.cache.flags = flags.sort((a, b) => b.time() - a.time());
this.loading = false;
m.redraw();

View File

@@ -1,7 +1,7 @@
import Modal from 'flarum/components/Modal';
import Button from 'flarum/components/Button';
export default class ReportPostModal extends Modal {
export default class FlagPostModal extends Modal {
constructor(...args) {
super(...args);
@@ -10,11 +10,11 @@ export default class ReportPostModal extends Modal {
}
className() {
return 'ReportPostModal Modal--small';
return 'FlagPostModal Modal--small';
}
title() {
return 'Report Post';
return 'Flag Post';
}
content() {
@@ -55,7 +55,7 @@ export default class ReportPostModal extends Modal {
type="submit"
loading={this.loading}
disabled={!this.reason()}>
Report Post
Flag Post
</Button>
</div>
</div>
@@ -68,7 +68,7 @@ export default class ReportPostModal extends Modal {
this.loading = true;
app.store.createRecord('reports').save({
app.store.createRecord('flags').save({
reason: this.reason() === 'other' ? null : this.reason(),
reasonDetail: this.reasonDetail(),
relationships: {

View File

@@ -0,0 +1,26 @@
import NotificationsDropdown from 'flarum/components/NotificationsDropdown';
import FlagList from 'flags/components/FlagList';
export default class FlagsDropdown extends NotificationsDropdown {
static initProps(props) {
props.label = props.label || 'Flagged Posts';
props.icon = props.icon || 'flag';
super.initProps(props);
}
constructor(...args) {
super(...args);
this.list = new FlagList();
}
goToRoute() {
m.route(app.route('flags'));
}
getUnreadCount() {
return app.forum.attribute('unreadFlagsCount');
}
}

View File

@@ -0,0 +1,24 @@
import Page from 'flarum/components/Page';
import FlagList from 'flags/components/FlagList';
/**
* The `FlagsPage` component shows the flags list. It is only
* used on mobile devices where the flags dropdown is within the drawer.
*/
export default class FlagsPage extends Page {
constructor(...args) {
super(...args);
app.history.push('flags');
this.list = new FlagList();
this.list.load();
this.bodyClass = 'App--flags';
}
view() {
return <div className="FlagsPage">{this.list.render()}</div>;
}
}

View File

@@ -1,26 +0,0 @@
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');
}
}

View File

@@ -1,24 +0,0 @@
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 <div className="ReportsPage">{this.list.render()}</div>;
}
}

View File

@@ -1,21 +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';
import Flag from 'flags/models/Flag';
import FlagsPage from 'flags/components/FlagsPage';
import addFlagControl from 'flags/addFlagControl';
import addFlagsDropdown from 'flags/addFlagsDropdown';
import addFlagsToPosts from 'flags/addFlagsToPosts';
app.initializers.add('reports', () => {
app.store.models.posts.prototype.reports = Model.hasMany('reports');
app.store.models.posts.prototype.canReport = Model.attribute('canReport');
app.initializers.add('flags', () => {
app.store.models.posts.prototype.flags = Model.hasMany('flags');
app.store.models.posts.prototype.canFlag = Model.attribute('canFlag');
app.store.models.reports = Report;
app.store.models.flags = Flag;
app.routes.reports = {path: '/reports', component: <ReportsPage/>};
app.routes.flags = {path: '/flags', component: <FlagsPage/>};
addReportControl();
addReportsDropdown();
addReportsToPosts();
addFlagControl();
addFlagsDropdown();
addFlagsToPosts();
});

View File

@@ -1,8 +1,8 @@
import Model from 'flarum/Model';
import mixin from 'flarum/utils/mixin';
export default class Report extends mixin(Model, {
reporter: Model.attribute('reporter'),
export default class Flag extends mixin(Model, {
type: Model.attribute('type'),
reason: Model.attribute('reason'),
reasonDetail: Model.attribute('reasonDetail'),
time: Model.attribute('time', Model.transformDate),