Initial commit

This commit is contained in:
Toby Zerner
2015-09-04 13:26:51 +09:30
commit cb6347ef6a
44 changed files with 1633 additions and 0 deletions

3
extensions/flags/js/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
bower_components
node_modules
dist

View File

@ -0,0 +1,7 @@
var gulp = require('flarum-gulp');
gulp({
modules: {
'reports': 'src/**/*.js'
}
});

View File

@ -0,0 +1,7 @@
{
"private": true,
"devDependencies": {
"gulp": "^3.8.11",
"flarum-gulp": "^0.1.0"
}
}

View File

@ -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'
});
});
});

View File

@ -0,0 +1,7 @@
var gulp = require('flarum-gulp');
gulp({
modules: {
'reports': 'src/**/*.js'
}
});

View File

@ -0,0 +1,7 @@
{
"private": true,
"devDependencies": {
"gulp": "^3.8.11",
"flarum-gulp": "^0.1.0"
}
}

View File

@ -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',
<Button icon="flag" onclick={() => app.modal.show(new ReportPostModal({post}))}>Report</Button>
);
});
}

View File

@ -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', <ReportsDropdown/>, 15);
}
});
}

View File

@ -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',
<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

@ -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 (
<div className="NotificationList ReportList">
<div className="NotificationList-header">
<h4 className="App-titleControl App-titleControl--text">Reported Posts</h4>
</div>
<div className="NotificationList-content">
<ul className="NotificationGroup-content">
{reports.length
? reports.map(report => {
const post = report.post();
return (
<li>
<a href={app.route.post(post)} className="Notification Report" config={function(element, isInitialized) {
m.route.apply(this, arguments);
if (!isInitialized) $(element).on('click', () => app.cache.reportIndex = 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())}
<div className="Notification-excerpt">
{post.contentPlain()}
</div>
</a>
</li>
);
})
: !this.loading
? <div className="NotificationList-empty">{app.trans('reports.no_reports')}</div>
: LoadingIndicator.component({className: 'LoadingIndicator--block'})}
</ul>
</div>
</div>
);
}
/**
* 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();
});
}
}

View File

@ -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 (
<div className="Modal-body">
<div className="Form">
<div className="Form-group">
<label>Choose a Reason</label>
<div>
<label className="checkbox">
<input type="radio" name="reason" checked={this.reason() === 'off_topic'} value="off_topic" onclick={m.withAttr('value', this.reason)}/>
Off-topic
</label>
<label className="checkbox">
<input type="radio" name="reason" checked={this.reason() === 'inappropriate'} value="inappropriate" onclick={m.withAttr('value', this.reason)}/>
Inappropriate
</label>
<label className="checkbox">
<input type="radio" name="reason" checked={this.reason() === 'spam'} value="spam" onclick={m.withAttr('value', this.reason)}/>
Spam
</label>
<label className="checkbox">
<input type="radio" name="reason" checked={this.reason() === 'other'} value="other" onclick={m.withAttr('value', this.reason)}/>
Other
{this.reason() === 'other' ? (
<textarea className="FormControl" value={this.reasonDetail()} oninput={m.withAttr('value', this.reasonDetail)}></textarea>
) : ''}
</label>
</div>
</div>
<div className="Form-group">
{Button.component({
children: 'Report Post',
className: 'Button Button--primary',
loading: this.loading,
disabled: !this.reason()
})}
</div>
</div>
</div>
);
}
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();
}
);
}
}

View File

@ -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');
}
}

View File

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

View File

@ -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: <ReportsPage/>};
addReportControl();
addReportsDropdown();
addReportsToPosts();
});

View File

@ -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')
}) {}