feat: Display suspension to user (#41)

This commit is contained in:
Ian Morland 2021-12-12 23:48:10 +00:00 committed by GitHub
parent 984b553dc8
commit fc06dba1b3
17 changed files with 333 additions and 61 deletions

View File

@ -60,5 +60,8 @@ return [
->addFilter(SuspendedFilterGambit::class),
(new Extend\SimpleFlarumSearch(UserSearcher::class))
->addGambit(SuspendedFilterGambit::class)
->addGambit(SuspendedFilterGambit::class),
(new Extend\View())
->namespace('flarum-suspend', __DIR__.'/views'),
];

View File

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

View File

@ -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,
};

View File

@ -1,14 +1,19 @@
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;
@ -19,11 +24,13 @@ export default class SuspendUserModal extends Modal {
}
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--small';
return 'SuspendUserModal Modal--medium';
}
title() {
@ -36,53 +43,7 @@ export default class SuspendUserModal extends Modal {
<div className="Form">
<div className="Form-group">
<label>{app.translator.trans('flarum-suspend.forum.suspend_user.status_heading')}</label>
<div>
<label className="checkbox">
<input type="radio" name="status" checked={!this.status()} value="" onclick={withAttr('value', this.status)} />
{app.translator.trans('flarum-suspend.forum.suspend_user.not_suspended_label')}
</label>
<label className="checkbox">
<input
type="radio"
name="status"
checked={this.status() === 'indefinitely'}
value="indefinitely"
onclick={withAttr('value', this.status)}
/>
{app.translator.trans('flarum-suspend.forum.suspend_user.indefinitely_label')}
</label>
<label className="checkbox SuspendUserModal-days">
<input
type="radio"
name="status"
checked={this.status() === 'limited'}
value="limited"
onclick={(e) => {
this.status(e.target.value);
m.redraw.sync();
this.$('.SuspendUserModal-days-input input').select();
e.redraw = false;
}}
/>
{app.translator.trans('flarum-suspend.forum.suspend_user.limited_time_label')}
{this.status() === 'limited' ? (
<div className="SuspendUserModal-days-input">
<input
type="number"
min="0"
value={this.daysRemaining()}
oninput={withAttr('value', this.daysRemaining)}
className="FormControl"
/>
{app.translator.trans('flarum-suspend.forum.suspend_user.limited_time_days_text')}
</div>
) : (
''
)}
</label>
</div>
<div>{this.formItems().toArray()}</div>
</div>
<div className="Form-group">
@ -95,6 +56,96 @@ export default class SuspendUserModal extends Modal {
);
}
radioItems() {
const items = new ItemList();
items.add(
'not-suspended',
<label className="checkbox">
<input type="radio" name="status" checked={!this.status()} value="" onclick={withAttr('value', this.status)} />
{app.translator.trans('flarum-suspend.forum.suspend_user.not_suspended_label')}
</label>,
100
);
items.add(
'indefinitely',
<label className="checkbox">
<input type="radio" name="status" checked={this.status() === 'indefinitely'} value="indefinitely" onclick={withAttr('value', this.status)} />
{app.translator.trans('flarum-suspend.forum.suspend_user.indefinitely_label')}
</label>,
90
);
items.add(
'time-suspension',
<label className="checkbox SuspendUserModal-days">
<input
type="radio"
name="status"
checked={this.status() === 'limited'}
value="limited"
onclick={(e) => {
this.status(e.target.value);
m.redraw.sync();
this.$('.SuspendUserModal-days-input input').select();
e.redraw = false;
}}
/>
{app.translator.trans('flarum-suspend.forum.suspend_user.limited_time_label')}
{this.status() === 'limited' && (
<div className="SuspendUserModal-days-input">
<input type="number" min="0" value={this.daysRemaining()} oninput={withAttr('value', this.daysRemaining)} className="FormControl" />
{app.translator.trans('flarum-suspend.forum.suspend_user.limited_time_days_text')}
</div>
)}
</label>,
80
);
return items;
}
formItems() {
const items = new ItemList();
items.add('radioItems', <div className="Form-group">{this.radioItems().toArray()}</div>, 100);
items.add(
'reason',
<div className="Form-group">
<label>
{app.translator.trans('flarum-suspend.forum.suspend_user.reason')}
<textarea
className="FormControl"
bidi={this.reason}
placeholder={app.translator.trans('flarum-suspend.forum.suspend_user.placeholder_optional')}
rows="2"
/>
</label>
</div>,
90
);
items.add(
'message',
<div className="Form-group">
<label>
{app.translator.trans('flarum-suspend.forum.suspend_user.display_message')}
<textarea
className="FormControl"
bidi={this.message}
placeholder={app.translator.trans('flarum-suspend.forum.suspend_user.placeholder_optional')}
rows="2"
/>
</label>
</div>,
80
);
return items;
}
onsubmit(e) {
e.preventDefault();
@ -103,7 +154,7 @@ export default class SuspendUserModal extends Modal {
let suspendedUntil = null;
switch (this.status()) {
case 'indefinitely':
suspendedUntil = new Date('2038-01-01');
suspendedUntil = getPermanentSuspensionDate();
break;
case 'limited':
@ -114,6 +165,8 @@ export default class SuspendUserModal extends Modal {
// no default
}
this.attrs.user.save({ suspendedUntil }).then(() => this.hide(), this.loaded.bind(this));
this.attrs.user
.save({ suspendedUntil, suspendReason: this.reason(), suspendMessage: this.message() })
.then(() => this.hide(), this.loaded.bind(this));
}
}

View File

@ -0,0 +1,48 @@
import app from 'flarum/forum/app';
import Modal from 'flarum/common/components/Modal';
import Button from 'flarum/common/components/Button';
import fullTime from 'flarum/common/helpers/fullTime';
import { isPermanentSuspensionDate, localStorageKey } from '../helpers/suspensionHelper';
export default class SuspensionInfoModal extends Modal {
oninit(vnode) {
super.oninit(vnode);
this.message = this.attrs.message;
this.until = this.attrs.until;
}
className() {
return 'SuspensionInfoModal Modal';
}
title() {
return app.translator.trans('flarum-suspend.forum.suspension_info.title');
}
content() {
const timespan = isPermanentSuspensionDate(new Date(this.until))
? app.translator.trans('flarum-suspend.forum.suspension_info.indefinite')
: app.translator.trans('flarum-suspend.forum.suspension_info.limited', { date: fullTime(this.until) });
return (
<div className="Modal-body">
<div className="Form Form--centered">
<p className="helpText">{this.message}</p>
<p className="helpText">{timespan}</p>
<div className="Form-group">
<Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}>
{app.translator.trans('flarum-suspend.forum.suspension_info.dismiss_button')}
</Button>
</div>
</div>
</div>
);
}
hide() {
localStorage.setItem(localStorageKey(), this.attrs.until.getTime());
this.attrs.state.close();
}
}

View File

@ -1,4 +1,6 @@
import app from 'flarum/forum/app';
import Notification from 'flarum/components/Notification';
import { isPermanentSuspensionDate } from '../helpers/suspensionHelper';
export default class UserSuspendedNotification extends Notification {
icon() {
@ -14,8 +16,10 @@ export default class UserSuspendedNotification extends Notification {
const suspendedUntil = notification.content();
const timeReadable = dayjs(suspendedUntil).from(notification.createdAt(), true);
return app.translator.trans('flarum-suspend.forum.notifications.user_suspended_text', {
timeReadable,
});
return isPermanentSuspensionDate(suspendedUntil)
? app.translator.trans('flarum-suspend.forum.notifications.user_suspended_indefinite_text')
: app.translator.trans('flarum-suspend.forum.notifications.user_suspended_text', {
timeReadable,
});
}
}

View File

@ -1,3 +1,4 @@
import app from 'flarum/forum/app';
import Notification from 'flarum/components/Notification';
export default class UserUnsuspendedNotification extends Notification {

View File

@ -0,0 +1,16 @@
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
export function getPermanentSuspensionDate(): Date {
return new Date('2038-01-01');
}
export function isPermanentSuspensionDate(date: Date): boolean {
return dayjs.utc(date).isSame(dayjs.utc('2038-01-01'));
}
export function localStorageKey(): string {
return 'flarum-suspend.acknowledge-suspension';
}

View File

@ -9,6 +9,7 @@ import User from 'flarum/models/User';
import SuspendUserModal from './components/SuspendUserModal';
import UserSuspendedNotification from './components/UserSuspendedNotification';
import UserUnsuspendedNotification from './components/UserUnsuspendedNotification';
import checkForSuspension from './checkForSuspension';
app.initializers.add('flarum-suspend', () => {
app.notificationComponents.userSuspended = UserSuspendedNotification;
@ -16,6 +17,8 @@ app.initializers.add('flarum-suspend', () => {
User.prototype.canSuspend = Model.attribute('canSuspend');
User.prototype.suspendedUntil = Model.attribute('suspendedUntil', Model.transformDate);
User.prototype.suspendReason = Model.attribute('suspendReason');
User.prototype.suspendMessage = Model.attribute('suspendMessage');
extend(UserControls, 'moderationControls', (items, user) => {
if (user.canSuspend()) {
@ -46,4 +49,12 @@ app.initializers.add('flarum-suspend', () => {
);
}
});
checkForSuspension();
});
// Expose compat API
import suspendCompat from './compat';
import { compat } from '@flarum/core/forum';
Object.assign(compat, suspendCompat);

View File

@ -16,14 +16,25 @@ flarum-suspend:
# These translations are used in the suspension notifications
notifications:
user_suspended_text: "You have been suspended for {timeReadable}"
user_suspended_indefinite_text: You have been suspended indefinitely
user_unsuspended_text: You have been unsuspended
# These translations are used for the suspension reason informational modal to the suspended user.
suspension_info:
dismiss_button: Dismiss
indefinite: This is an indefinite suspension
limited: "This suspension will be in force until {date}"
title: This account is suspended
# These translations are used in the Suspend User modal dialog (admin function).
suspend_user:
display_message: Display message for user
indefinitely_label: Suspended indefinitely
limited_time_days_text: " days"
limited_time_label: Suspended for a limited time...
not_suspended_label: Not suspended
placeholder_optional: Optional
reason: Reason for suspension
status_heading: Suspension Status
submit_button: => core.ref.save_changes
title: "Suspend {username}"
@ -35,3 +46,25 @@ flarum-suspend:
# These translations are found on the user profile page (admin function).
user_controls:
suspend_button: Suspend
# Translations in this namespace are used by suspension email notifications
email:
suspended:
subject: Your account has been suspended
body: |
Hey {recipient_display_name},
You have been suspended for the following reason:
---
{suspension_message}
---
unsuspended:
subject: Your account has been unsuspended
body: |
Hey {recipient_display_name},
You have been unsuspended. You can head back to the forum by clicking on the following link:
{forum_url}

View File

@ -0,0 +1,15 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Flarum\Database\Migration;
return Migration::addColumns('users', [
'suspend_reason' => ['text', 'nullable' => true],
'suspend_message' => ['text', 'nullable' => true]
]);

View File

@ -20,6 +20,11 @@ class AddUserSuspendAttributes
$canSuspend = $serializer->getActor()->can('suspend', $user);
if ($canSuspend) {
$attributes['suspendReason'] = $user->suspend_reason;
}
if ($serializer->getActor()->id === $user->id || $canSuspend) {
$attributes['suspendMessage'] = $user->suspend_message;
$attributes['suspendedUntil'] = $serializer->formatDate($user->suspended_until);
}

View File

@ -53,11 +53,17 @@ class SaveSuspensionToDatabase
$actor->assertCan('suspend', $user);
$user->suspended_until = $attributes['suspendedUntil']
? new DateTime($attributes['suspendedUntil'])
: null;
if ($attributes['suspendedUntil']) {
$user->suspended_until = new DateTime($attributes['suspendedUntil']);
$user->suspend_reason = empty($attributes['suspendReason']) ? null : $attributes['suspendReason'];
$user->suspend_message = empty($attributes['suspendMessage']) ? null : $attributes['suspendMessage'];
} else {
$user->suspended_until = null;
$user->suspend_reason = null;
$user->suspend_message = null;
}
if ($user->isDirty('suspended_until')) {
if ($user->isDirty(['suspended_until', 'suspend_reason', 'suspend_message'])) {
$this->events->dispatch(
$user->suspended_until === null ?
new Unsuspended($user, $actor) :

View File

@ -10,9 +10,11 @@
namespace Flarum\Suspend\Notification;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\MailableInterface;
use Flarum\User\User;
use Symfony\Contracts\Translation\TranslatorInterface;
class UserSuspendedBlueprint implements BlueprintInterface
class UserSuspendedBlueprint implements BlueprintInterface, MailableInterface
{
/**
* @var User
@ -66,4 +68,20 @@ class UserSuspendedBlueprint implements BlueprintInterface
{
return User::class;
}
/**
* {@inheritdoc}
*/
public function getEmailView()
{
return ['text' => 'flarum-suspend::emails.suspended'];
}
/**
* {@inheritdoc}
*/
public function getEmailSubject(TranslatorInterface $translator)
{
return $translator->trans('flarum-suspend.email.suspended.subject');
}
}

View File

@ -10,9 +10,12 @@
namespace Flarum\Suspend\Notification;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\MailableInterface;
use Flarum\User\User;
use Illuminate\Support\Carbon;
use Symfony\Contracts\Translation\TranslatorInterface;
class UserUnsuspendedBlueprint implements BlueprintInterface
class UserUnsuspendedBlueprint implements BlueprintInterface, MailableInterface
{
/**
* @var User
@ -48,7 +51,7 @@ class UserUnsuspendedBlueprint implements BlueprintInterface
*/
public function getData()
{
return null;
return Carbon::now();
}
/**
@ -66,4 +69,20 @@ class UserUnsuspendedBlueprint implements BlueprintInterface
{
return User::class;
}
/**
* {@inheritdoc}
*/
public function getEmailView()
{
return ['text' => 'flarum-suspend::emails.unsuspended'];
}
/**
* {@inheritdoc}
*/
public function getEmailSubject(TranslatorInterface $translator)
{
return $translator->trans('flarum-suspend.email.unsuspended.subject');
}
}

View File

@ -0,0 +1,4 @@
{!! $translator->trans('flarum-suspend.email.suspended.body', [
'{recipient_display_name}' => $user->display_name,
'{suspension_message}' => $blueprint->user->suspend_message,
]) !!}

View File

@ -0,0 +1,4 @@
{!! $translator->trans('flarum-suspend.email.unsuspended.body', [
'{recipient_display_name}' => $user->display_name,
'{forum_url}' => $url->to('forum')->base(),
]) !!}