mirror of
https://github.com/flarum/framework.git
synced 2025-01-31 18:34:01 +08:00
feat: Display suspension to user (#41)
This commit is contained in:
parent
984b553dc8
commit
fc06dba1b3
|
@ -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'),
|
||||
];
|
||||
|
|
19
extensions/suspend/js/src/forum/checkForSuspension.ts
Normal file
19
extensions/suspend/js/src/forum/checkForSuspension.ts
Normal 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);
|
||||
}
|
13
extensions/suspend/js/src/forum/compat.js
Normal file
13
extensions/suspend/js/src/forum/compat.js
Normal 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,
|
||||
};
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import app from 'flarum/forum/app';
|
||||
import Notification from 'flarum/components/Notification';
|
||||
|
||||
export default class UserUnsuspendedNotification extends Notification {
|
||||
|
|
16
extensions/suspend/js/src/forum/helpers/suspensionHelper.ts
Normal file
16
extensions/suspend/js/src/forum/helpers/suspensionHelper.ts
Normal 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';
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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]
|
||||
]);
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) :
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
4
extensions/suspend/views/emails/suspended.blade.php
Normal file
4
extensions/suspend/views/emails/suspended.blade.php
Normal file
|
@ -0,0 +1,4 @@
|
|||
{!! $translator->trans('flarum-suspend.email.suspended.body', [
|
||||
'{recipient_display_name}' => $user->display_name,
|
||||
'{suspension_message}' => $blueprint->user->suspend_message,
|
||||
]) !!}
|
4
extensions/suspend/views/emails/unsuspended.blade.php
Normal file
4
extensions/suspend/views/emails/unsuspended.blade.php
Normal file
|
@ -0,0 +1,4 @@
|
|||
{!! $translator->trans('flarum-suspend.email.unsuspended.body', [
|
||||
'{recipient_display_name}' => $user->display_name,
|
||||
'{forum_url}' => $url->to('forum')->base(),
|
||||
]) !!}
|
Loading…
Reference in New Issue
Block a user