@@ -95,6 +56,96 @@ export default class SuspendUserModal extends Modal {
);
}
+ radioItems() {
+ const items = new ItemList();
+
+ items.add(
+ 'not-suspended',
+
,
+ 100
+ );
+
+ items.add(
+ 'indefinitely',
+
,
+ 90
+ );
+
+ items.add(
+ 'time-suspension',
+
,
+ 80
+ );
+
+ return items;
+ }
+
+ formItems() {
+ const items = new ItemList();
+
+ items.add('radioItems',
{this.radioItems().toArray()}
, 100);
+
+ items.add(
+ 'reason',
+
+
+
,
+ 90
+ );
+
+ items.add(
+ 'message',
+
+
+
,
+ 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));
}
}
diff --git a/extensions/suspend/js/src/forum/components/SuspensionInfoModal.js b/extensions/suspend/js/src/forum/components/SuspensionInfoModal.js
new file mode 100644
index 000000000..557851bae
--- /dev/null
+++ b/extensions/suspend/js/src/forum/components/SuspensionInfoModal.js
@@ -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 (
+
+
+
{this.message}
+
{timespan}
+
+
+
+
+
+
+ );
+ }
+
+ hide() {
+ localStorage.setItem(localStorageKey(), this.attrs.until.getTime());
+ this.attrs.state.close();
+ }
+}
diff --git a/extensions/suspend/js/src/forum/components/UserSuspendedNotification.js b/extensions/suspend/js/src/forum/components/UserSuspendedNotification.js
index a7bc60aa1..d2c9ffb32 100644
--- a/extensions/suspend/js/src/forum/components/UserSuspendedNotification.js
+++ b/extensions/suspend/js/src/forum/components/UserSuspendedNotification.js
@@ -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,
+ });
}
}
diff --git a/extensions/suspend/js/src/forum/components/UserUnsuspendedNotification.js b/extensions/suspend/js/src/forum/components/UserUnsuspendedNotification.js
index e8bde8590..640492d32 100644
--- a/extensions/suspend/js/src/forum/components/UserUnsuspendedNotification.js
+++ b/extensions/suspend/js/src/forum/components/UserUnsuspendedNotification.js
@@ -1,3 +1,4 @@
+import app from 'flarum/forum/app';
import Notification from 'flarum/components/Notification';
export default class UserUnsuspendedNotification extends Notification {
diff --git a/extensions/suspend/js/src/forum/helpers/suspensionHelper.ts b/extensions/suspend/js/src/forum/helpers/suspensionHelper.ts
new file mode 100644
index 000000000..40ffacd60
--- /dev/null
+++ b/extensions/suspend/js/src/forum/helpers/suspensionHelper.ts
@@ -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';
+}
diff --git a/extensions/suspend/js/src/forum/index.js b/extensions/suspend/js/src/forum/index.js
index 7e81ce1c8..93bc82856 100644
--- a/extensions/suspend/js/src/forum/index.js
+++ b/extensions/suspend/js/src/forum/index.js
@@ -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);
diff --git a/extensions/suspend/locale/en.yml b/extensions/suspend/locale/en.yml
index c76d904bc..5a143e818 100644
--- a/extensions/suspend/locale/en.yml
+++ b/extensions/suspend/locale/en.yml
@@ -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}
diff --git a/extensions/suspend/migrations/2021_10_27_000000_add_suspend_reason_and_message.php b/extensions/suspend/migrations/2021_10_27_000000_add_suspend_reason_and_message.php
new file mode 100644
index 000000000..906dd2de9
--- /dev/null
+++ b/extensions/suspend/migrations/2021_10_27_000000_add_suspend_reason_and_message.php
@@ -0,0 +1,15 @@
+ ['text', 'nullable' => true],
+ 'suspend_message' => ['text', 'nullable' => true]
+]);
diff --git a/extensions/suspend/src/AddUserSuspendAttributes.php b/extensions/suspend/src/AddUserSuspendAttributes.php
index 9e694c1eb..e2411469f 100755
--- a/extensions/suspend/src/AddUserSuspendAttributes.php
+++ b/extensions/suspend/src/AddUserSuspendAttributes.php
@@ -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);
}
diff --git a/extensions/suspend/src/Listener/SaveSuspensionToDatabase.php b/extensions/suspend/src/Listener/SaveSuspensionToDatabase.php
index 031f44843..59f4d452e 100755
--- a/extensions/suspend/src/Listener/SaveSuspensionToDatabase.php
+++ b/extensions/suspend/src/Listener/SaveSuspensionToDatabase.php
@@ -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) :
diff --git a/extensions/suspend/src/Notification/UserSuspendedBlueprint.php b/extensions/suspend/src/Notification/UserSuspendedBlueprint.php
index e8f32617f..5c3e729c8 100644
--- a/extensions/suspend/src/Notification/UserSuspendedBlueprint.php
+++ b/extensions/suspend/src/Notification/UserSuspendedBlueprint.php
@@ -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');
+ }
}
diff --git a/extensions/suspend/src/Notification/UserUnsuspendedBlueprint.php b/extensions/suspend/src/Notification/UserUnsuspendedBlueprint.php
index 5221154e8..85fa01c32 100644
--- a/extensions/suspend/src/Notification/UserUnsuspendedBlueprint.php
+++ b/extensions/suspend/src/Notification/UserUnsuspendedBlueprint.php
@@ -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');
+ }
}
diff --git a/extensions/suspend/views/emails/suspended.blade.php b/extensions/suspend/views/emails/suspended.blade.php
new file mode 100644
index 000000000..b25aa525c
--- /dev/null
+++ b/extensions/suspend/views/emails/suspended.blade.php
@@ -0,0 +1,4 @@
+{!! $translator->trans('flarum-suspend.email.suspended.body', [
+'{recipient_display_name}' => $user->display_name,
+'{suspension_message}' => $blueprint->user->suspend_message,
+]) !!}
diff --git a/extensions/suspend/views/emails/unsuspended.blade.php b/extensions/suspend/views/emails/unsuspended.blade.php
new file mode 100644
index 000000000..1237e6c2c
--- /dev/null
+++ b/extensions/suspend/views/emails/unsuspended.blade.php
@@ -0,0 +1,4 @@
+{!! $translator->trans('flarum-suspend.email.unsuspended.body', [
+'{recipient_display_name}' => $user->display_name,
+'{forum_url}' => $url->to('forum')->base(),
+]) !!}