diff --git a/extensions/messages/extend.php b/extensions/messages/extend.php index 2f2394922..6f7037ced 100644 --- a/extensions/messages/extend.php +++ b/extensions/messages/extend.php @@ -52,6 +52,8 @@ return [ ->fields(fn () => [ Schema\Boolean::make('canSendAnyMessage') ->get(fn (User $user, Context $context) => $user->can('sendAnyMessage')), + Schema\Boolean::make('canDeleteOwnMessages') + ->visible(fn (User $user, Context $context) => $context->getActor()->is($user)), Schema\Integer::make('messageCount') ->get(function (object $model, Context $context) { return Dialog::whereVisibleTo($context->getActor()) diff --git a/extensions/messages/js/src/admin/extend.ts b/extensions/messages/js/src/admin/extend.ts deleted file mode 100644 index 735aaaf2b..000000000 --- a/extensions/messages/js/src/admin/extend.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Extend from 'flarum/common/extenders'; -import commonExtend from '../common/extend'; -import app from 'flarum/admin/app'; - -export default [ - ...commonExtend, - - new Extend.Admin().permission( - () => ({ - icon: 'fas fa-envelope-open-text', - label: app.translator.trans('flarum-messages.admin.permissions.send_messages'), - permission: 'dialog.sendMessage', - allowGuest: false, - }), - 'start', - 95 - ), -]; diff --git a/extensions/messages/js/src/admin/extend.tsx b/extensions/messages/js/src/admin/extend.tsx new file mode 100644 index 000000000..41a28e150 --- /dev/null +++ b/extensions/messages/js/src/admin/extend.tsx @@ -0,0 +1,45 @@ +import Extend from 'flarum/common/extenders'; +import commonExtend from '../common/extend'; +import app from 'flarum/admin/app'; +import SettingDropdown from 'flarum/admin/components/SettingDropdown'; + +export default [ + ...commonExtend, + + new Extend.Admin() + .permission( + () => ({ + icon: 'fas fa-envelope-open-text', + label: app.translator.trans('flarum-messages.admin.permissions.send_messages_label'), + permission: 'dialog.sendMessage', + allowGuest: false, + }), + 'start', + 95 + ) + .permission( + () => ({ + icon: 'far fa-trash-alt', + label: app.translator.trans('flarum-messages.admin.permissions.delete_own_messages_label'), + id: 'flarum-messages.allow_delete_own_messages', + setting: () => { + const minutes = parseInt(app.data.settings['flarum-messages.allow_delete_own_messages'], 10); + + return ( + + ); + }, + }), + 'reply', + 80 + ), +]; diff --git a/extensions/messages/js/src/common/extend.ts b/extensions/messages/js/src/common/extend.ts index 59286e0b2..e5bf90f5b 100644 --- a/extensions/messages/js/src/common/extend.ts +++ b/extensions/messages/js/src/common/extend.ts @@ -9,5 +9,6 @@ export default [ .add('dialog-messages', DialogMessage), // new Extend.Model(User) // - .attribute('canSendAnyMessage'), + .attribute('canSendAnyMessage') + .attribute('canDeleteOwnMessage'), ]; diff --git a/extensions/messages/js/src/common/models/DialogMessage.ts b/extensions/messages/js/src/common/models/DialogMessage.ts index 4e88179b0..187fc531b 100644 --- a/extensions/messages/js/src/common/models/DialogMessage.ts +++ b/extensions/messages/js/src/common/models/DialogMessage.ts @@ -36,4 +36,8 @@ export default class DialogMessage extends Model { user() { return Model.hasOne('user').call(this); } + + canDelete() { + return Model.attribute('canDelete').call(this); + } } diff --git a/extensions/messages/js/src/forum/components/Message.tsx b/extensions/messages/js/src/forum/components/Message.tsx index b62cbb523..c241c3e9e 100644 --- a/extensions/messages/js/src/forum/components/Message.tsx +++ b/extensions/messages/js/src/forum/components/Message.tsx @@ -9,9 +9,12 @@ import Comment from 'flarum/forum/components/Comment'; import PostUser from 'flarum/forum/components/PostUser'; import PostMeta from 'flarum/forum/components/PostMeta'; import classList from 'flarum/common/utils/classList'; +import MessageControls from '../utils/MessageControls'; +import type MessageStreamState from '../states/MessageStreamState'; export interface IMessageAttrs extends IAbstractPostAttrs { message: DialogMessage; + state: MessageStreamState; } /** @@ -29,7 +32,7 @@ export default abstract class Message {this.timeGap(message)} - + ); } diff --git a/extensions/messages/js/src/forum/utils/MessageControls.tsx b/extensions/messages/js/src/forum/utils/MessageControls.tsx new file mode 100644 index 000000000..2ccf3b6f8 --- /dev/null +++ b/extensions/messages/js/src/forum/utils/MessageControls.tsx @@ -0,0 +1,67 @@ +import ItemList from 'flarum/common/utils/ItemList'; +import Separator from 'flarum/common/components/Separator'; +import type Mithril from 'mithril'; +import type DialogMessage from '../../common/models/DialogMessage'; +import type Message from '../components/Message'; +import Button from 'flarum/common/components/Button'; +import app from 'flarum/forum/app'; +import extractText from 'flarum/common/utils/extractText'; + +const MessageControls = { + controls(message: DialogMessage, context: Message) { + const items = new ItemList(); + + Object.entries(this.sections()).forEach(([section, method]) => { + const controls = method.call(this, message, context).toArray(); + + if (controls.length) { + controls.forEach((item) => items.add(item.itemName, item)); + items.add(section + 'Separator', ); + } + }); + + return items; + }, + + sections() { + return { + user: this.userControls, + moderation: this.moderationControls, + destructive: this.destructiveControls, + }; + }, + + userControls(message: DialogMessage, context: Message) { + return new ItemList(); + }, + + moderationControls(message: DialogMessage, context: Message) { + return new ItemList(); + }, + + destructiveControls(message: DialogMessage, context: Message) { + const items = new ItemList(); + + if (message.canDelete()) { + items.add( + 'delete', + + ); + } + + return items; + }, + + deleteAction(message: DialogMessage, context: Message) { + if (!confirm(extractText(app.translator.trans('flarum-messages.forum.message_controls.delete_confirmation')))) return; + + return message.delete().then(() => { + context.attrs.state.remove(message); + m.redraw(); + }); + }, +}; + +export default MessageControls; diff --git a/extensions/messages/locale/en.yml b/extensions/messages/locale/en.yml index 18ecc76d2..ea8769e22 100644 --- a/extensions/messages/locale/en.yml +++ b/extensions/messages/locale/en.yml @@ -3,7 +3,8 @@ flarum-messages: # Translations in this namespace are used by the admin interface. admin: permissions: - send_messages: Send private messages + send_messages_label: Send private messages + delete_own_messages_label: Delete own messages # Translations in this namespace are used by the forum user interface. forum: @@ -42,6 +43,10 @@ flarum-messages: newest_button: Newest oldest_button: Oldest + message_controls: + delete_button: Delete + delete_confirmation: Are you sure you want to delete this message? This action cannot be undone. + messages_page: cannot_send_message_button: Can't Send a Message empty_text: No new messages diff --git a/extensions/messages/src/Access/DialogMessagePolicy.php b/extensions/messages/src/Access/DialogMessagePolicy.php index f49c8b0a5..5e14dfb55 100644 --- a/extensions/messages/src/Access/DialogMessagePolicy.php +++ b/extensions/messages/src/Access/DialogMessagePolicy.php @@ -9,14 +9,36 @@ namespace Flarum\Messages\Access; +use Carbon\Carbon; use Flarum\Messages\DialogMessage; +use Flarum\Settings\SettingsRepositoryInterface; use Flarum\User\Access\AbstractPolicy; use Flarum\User\User; class DialogMessagePolicy extends AbstractPolicy { - public function update(User $actor, DialogMessage $dialogMessage): bool + public function __construct( + protected SettingsRepositoryInterface $settings + ) { + } + + public function update(User $actor, DialogMessage $message): ?bool { + return null; + } + + public function delete(User $actor, DialogMessage $message): bool|null|string + { + if ($message->user_id === $actor->id) { + $allowHiding = $this->settings->get('flarum-messages.allow_delete_own_messages'); + + if ($allowHiding === '-1' + || ($allowHiding === 'reply' && $message->number >= $message->dialog->lastMessage->number) + || (is_numeric($allowHiding) && $message->created_at->diffInMinutes(new Carbon, true) < $allowHiding)) { + return $this->allow(); + } + } + return false; } } diff --git a/extensions/messages/src/Api/Resource/DialogMessageResource.php b/extensions/messages/src/Api/Resource/DialogMessageResource.php index 356c0d65e..b5dbd54f4 100644 --- a/extensions/messages/src/Api/Resource/DialogMessageResource.php +++ b/extensions/messages/src/Api/Resource/DialogMessageResource.php @@ -78,6 +78,11 @@ class DialogMessageResource extends Resource\AbstractDatabaseResource return $actor->can('sendAnyMessage'); } }), + Endpoint\Delete::make() + ->authenticated() + ->visible(function (DialogMessage $message, Context $context): bool { + return $context->getActor()->can('delete', $message); + }), Endpoint\Index::make() ->authenticated() ->defaultInclude([ @@ -166,6 +171,12 @@ class DialogMessageResource extends Resource\AbstractDatabaseResource ->items(1) ->set(fn () => null), + // Read-only. + Schema\Boolean::make('canDelete') + ->get(function (DialogMessage $message, Context $context) { + return $context->getActor()->can('delete', $message); + }), + Schema\Relationship\ToOne::make('user') ->type('users') ->includable(), diff --git a/framework/core/js/src/admin/components/SettingDropdown.tsx b/framework/core/js/src/admin/components/SettingDropdown.tsx index 0205ff532..76f5a0a4a 100644 --- a/framework/core/js/src/admin/components/SettingDropdown.tsx +++ b/framework/core/js/src/admin/components/SettingDropdown.tsx @@ -12,6 +12,7 @@ export type SettingDropdownOption = { export interface ISettingDropdownAttrs extends ISelectDropdownAttrs { setting?: string; options: Array; + default: any; } export default class SettingDropdown extends SelectDropdown { @@ -33,7 +34,7 @@ export default class SettingDropdown { - const active = app.data.settings[this.attrs.setting!] === value; + const active = (app.data.settings[this.attrs.setting!] ?? this.attrs.default) === value; return (