mirror of
https://github.com/flarum/core.git
synced 2025-08-06 08:27:42 +02:00
feat(pm): delete own messages (#4180)
This commit is contained in:
@@ -52,6 +52,8 @@ return [
|
|||||||
->fields(fn () => [
|
->fields(fn () => [
|
||||||
Schema\Boolean::make('canSendAnyMessage')
|
Schema\Boolean::make('canSendAnyMessage')
|
||||||
->get(fn (User $user, Context $context) => $user->can('sendAnyMessage')),
|
->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')
|
Schema\Integer::make('messageCount')
|
||||||
->get(function (object $model, Context $context) {
|
->get(function (object $model, Context $context) {
|
||||||
return Dialog::whereVisibleTo($context->getActor())
|
return Dialog::whereVisibleTo($context->getActor())
|
||||||
|
@@ -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
|
|
||||||
),
|
|
||||||
];
|
|
45
extensions/messages/js/src/admin/extend.tsx
Normal file
45
extensions/messages/js/src/admin/extend.tsx
Normal file
@@ -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 (
|
||||||
|
<SettingDropdown
|
||||||
|
default={'0'}
|
||||||
|
key="flarum-messages.allow_delete_own_messages"
|
||||||
|
options={[
|
||||||
|
{ value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') },
|
||||||
|
{ value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') },
|
||||||
|
{ value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') },
|
||||||
|
{ value: '0', label: app.translator.trans('core.admin.permissions_controls.allow_never_button') },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'reply',
|
||||||
|
80
|
||||||
|
),
|
||||||
|
];
|
@@ -9,5 +9,6 @@ export default [
|
|||||||
.add('dialog-messages', DialogMessage), //
|
.add('dialog-messages', DialogMessage), //
|
||||||
|
|
||||||
new Extend.Model(User) //
|
new Extend.Model(User) //
|
||||||
.attribute<boolean>('canSendAnyMessage'),
|
.attribute<boolean>('canSendAnyMessage')
|
||||||
|
.attribute<boolean>('canDeleteOwnMessage'),
|
||||||
];
|
];
|
||||||
|
@@ -36,4 +36,8 @@ export default class DialogMessage extends Model {
|
|||||||
user() {
|
user() {
|
||||||
return Model.hasOne<User>('user').call(this);
|
return Model.hasOne<User>('user').call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canDelete() {
|
||||||
|
return Model.attribute<boolean>('canDelete').call(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,9 +9,12 @@ import Comment from 'flarum/forum/components/Comment';
|
|||||||
import PostUser from 'flarum/forum/components/PostUser';
|
import PostUser from 'flarum/forum/components/PostUser';
|
||||||
import PostMeta from 'flarum/forum/components/PostMeta';
|
import PostMeta from 'flarum/forum/components/PostMeta';
|
||||||
import classList from 'flarum/common/utils/classList';
|
import classList from 'flarum/common/utils/classList';
|
||||||
|
import MessageControls from '../utils/MessageControls';
|
||||||
|
import type MessageStreamState from '../states/MessageStreamState';
|
||||||
|
|
||||||
export interface IMessageAttrs extends IAbstractPostAttrs {
|
export interface IMessageAttrs extends IAbstractPostAttrs {
|
||||||
message: DialogMessage;
|
message: DialogMessage;
|
||||||
|
state: MessageStreamState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,7 +32,7 @@ export default abstract class Message<CustomAttrs extends IMessageAttrs = IMessa
|
|||||||
}
|
}
|
||||||
|
|
||||||
controls(): Mithril.Children[] {
|
controls(): Mithril.Children[] {
|
||||||
return [];
|
return MessageControls.controls(this.attrs.message, this).toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
freshness(): Date {
|
freshness(): Date {
|
||||||
|
@@ -161,7 +161,7 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
|
|||||||
return (
|
return (
|
||||||
<div className="MessageStream-item" key={index} data-id={message.id()} data-number={message.number()}>
|
<div className="MessageStream-item" key={index} data-id={message.id()} data-number={message.number()}>
|
||||||
{this.timeGap(message)}
|
{this.timeGap(message)}
|
||||||
<Message message={message} />
|
<Message message={message} state={this.attrs.state} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
67
extensions/messages/js/src/forum/utils/MessageControls.tsx
Normal file
67
extensions/messages/js/src/forum/utils/MessageControls.tsx
Normal file
@@ -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<any>) {
|
||||||
|
const items = new ItemList<Mithril.Children>();
|
||||||
|
|
||||||
|
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', <Separator />);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
|
||||||
|
sections() {
|
||||||
|
return {
|
||||||
|
user: this.userControls,
|
||||||
|
moderation: this.moderationControls,
|
||||||
|
destructive: this.destructiveControls,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
userControls(message: DialogMessage, context: Message) {
|
||||||
|
return new ItemList<Mithril.Children>();
|
||||||
|
},
|
||||||
|
|
||||||
|
moderationControls(message: DialogMessage, context: Message) {
|
||||||
|
return new ItemList<Mithril.Children>();
|
||||||
|
},
|
||||||
|
|
||||||
|
destructiveControls(message: DialogMessage, context: Message) {
|
||||||
|
const items = new ItemList<Mithril.Children>();
|
||||||
|
|
||||||
|
if (message.canDelete()) {
|
||||||
|
items.add(
|
||||||
|
'delete',
|
||||||
|
<Button icon="far fa-trash-alt" onclick={() => this.deleteAction(message, context)}>
|
||||||
|
{app.translator.trans('flarum-messages.forum.message_controls.delete_button')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
@@ -3,7 +3,8 @@ flarum-messages:
|
|||||||
# Translations in this namespace are used by the admin interface.
|
# Translations in this namespace are used by the admin interface.
|
||||||
admin:
|
admin:
|
||||||
permissions:
|
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.
|
# Translations in this namespace are used by the forum user interface.
|
||||||
forum:
|
forum:
|
||||||
@@ -42,6 +43,10 @@ flarum-messages:
|
|||||||
newest_button: Newest
|
newest_button: Newest
|
||||||
oldest_button: Oldest
|
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:
|
messages_page:
|
||||||
cannot_send_message_button: Can't Send a Message
|
cannot_send_message_button: Can't Send a Message
|
||||||
empty_text: No new messages
|
empty_text: No new messages
|
||||||
|
@@ -9,14 +9,36 @@
|
|||||||
|
|
||||||
namespace Flarum\Messages\Access;
|
namespace Flarum\Messages\Access;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
use Flarum\Messages\DialogMessage;
|
use Flarum\Messages\DialogMessage;
|
||||||
|
use Flarum\Settings\SettingsRepositoryInterface;
|
||||||
use Flarum\User\Access\AbstractPolicy;
|
use Flarum\User\Access\AbstractPolicy;
|
||||||
use Flarum\User\User;
|
use Flarum\User\User;
|
||||||
|
|
||||||
class DialogMessagePolicy extends AbstractPolicy
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -78,6 +78,11 @@ class DialogMessageResource extends Resource\AbstractDatabaseResource
|
|||||||
return $actor->can('sendAnyMessage');
|
return $actor->can('sendAnyMessage');
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
Endpoint\Delete::make()
|
||||||
|
->authenticated()
|
||||||
|
->visible(function (DialogMessage $message, Context $context): bool {
|
||||||
|
return $context->getActor()->can('delete', $message);
|
||||||
|
}),
|
||||||
Endpoint\Index::make()
|
Endpoint\Index::make()
|
||||||
->authenticated()
|
->authenticated()
|
||||||
->defaultInclude([
|
->defaultInclude([
|
||||||
@@ -166,6 +171,12 @@ class DialogMessageResource extends Resource\AbstractDatabaseResource
|
|||||||
->items(1)
|
->items(1)
|
||||||
->set(fn () => null),
|
->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')
|
Schema\Relationship\ToOne::make('user')
|
||||||
->type('users')
|
->type('users')
|
||||||
->includable(),
|
->includable(),
|
||||||
|
@@ -12,6 +12,7 @@ export type SettingDropdownOption = {
|
|||||||
export interface ISettingDropdownAttrs extends ISelectDropdownAttrs {
|
export interface ISettingDropdownAttrs extends ISelectDropdownAttrs {
|
||||||
setting?: string;
|
setting?: string;
|
||||||
options: Array<SettingDropdownOption>;
|
options: Array<SettingDropdownOption>;
|
||||||
|
default: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SettingDropdown<CustomAttrs extends ISettingDropdownAttrs = ISettingDropdownAttrs> extends SelectDropdown<CustomAttrs> {
|
export default class SettingDropdown<CustomAttrs extends ISettingDropdownAttrs = ISettingDropdownAttrs> extends SelectDropdown<CustomAttrs> {
|
||||||
@@ -33,7 +34,7 @@ export default class SettingDropdown<CustomAttrs extends ISettingDropdownAttrs =
|
|||||||
return super.view({
|
return super.view({
|
||||||
...vnode,
|
...vnode,
|
||||||
children: this.attrs.options.map(({ value, label }) => {
|
children: this.attrs.options.map(({ value, label }) => {
|
||||||
const active = app.data.settings[this.attrs.setting!] === value;
|
const active = (app.data.settings[this.attrs.setting!] ?? this.attrs.default) === value;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button icon={active ? 'fas fa-check' : true} onclick={saveSettings.bind(this, { [this.attrs.setting!]: value })} active={active}>
|
<Button icon={active ? 'fas fa-check' : true} onclick={saveSettings.bind(this, { [this.attrs.setting!]: value })} active={active}>
|
||||||
|
@@ -391,4 +391,12 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
|
|||||||
1
|
1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove(model: T): void {
|
||||||
|
const page = this.pages.find((pg) => pg.items.includes(model));
|
||||||
|
|
||||||
|
if (page) {
|
||||||
|
page.items = page.items.filter((item) => item !== model);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,8 +20,9 @@ const PostControls = {
|
|||||||
controls(post, context) {
|
controls(post, context) {
|
||||||
const items = new ItemList();
|
const items = new ItemList();
|
||||||
|
|
||||||
['user', 'moderation', 'destructive'].forEach((section) => {
|
Object.entries(this.sections()).forEach(([section, method]) => {
|
||||||
const controls = this[section + 'Controls'](post, context).toArray();
|
const controls = method.call(this, post, context).toArray();
|
||||||
|
|
||||||
if (controls.length) {
|
if (controls.length) {
|
||||||
controls.forEach((item) => items.add(item.itemName, item));
|
controls.forEach((item) => items.add(item.itemName, item));
|
||||||
items.add(section + 'Separator', <Separator />);
|
items.add(section + 'Separator', <Separator />);
|
||||||
@@ -31,6 +32,14 @@ const PostControls = {
|
|||||||
return items;
|
return items;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sections() {
|
||||||
|
return {
|
||||||
|
user: this.userControls,
|
||||||
|
moderation: this.moderationControls,
|
||||||
|
destructive: this.destructiveControls,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get controls for a post pertaining to the current user (e.g. report).
|
* Get controls for a post pertaining to the current user (e.g. report).
|
||||||
*
|
*
|
||||||
|
Reference in New Issue
Block a user