diff --git a/extensions/mentions/extend.php b/extensions/mentions/extend.php index 178330e33..794503693 100644 --- a/extensions/mentions/extend.php +++ b/extensions/mentions/extend.php @@ -12,10 +12,11 @@ namespace Flarum\Mentions; use Flarum\Api\Controller; use Flarum\Api\Serializer\BasicPostSerializer; use Flarum\Api\Serializer\BasicUserSerializer; +use Flarum\Api\Serializer\CurrentUserSerializer; +use Flarum\Api\Serializer\GroupSerializer; use Flarum\Api\Serializer\PostSerializer; use Flarum\Extend; -use Flarum\Mentions\Notification\PostMentionedBlueprint; -use Flarum\Mentions\Notification\UserMentionedBlueprint; +use Flarum\Group\Group; use Flarum\Post\Event\Deleted; use Flarum\Post\Event\Hidden; use Flarum\Post\Event\Posted; @@ -37,13 +38,16 @@ return [ ->configure(ConfigureMentions::class) ->render(Formatter\FormatPostMentions::class) ->render(Formatter\FormatUserMentions::class) + ->render(Formatter\FormatGroupMentions::class) ->unparse(Formatter\UnparsePostMentions::class) - ->unparse(Formatter\UnparseUserMentions::class), + ->unparse(Formatter\UnparseUserMentions::class) + ->parse(Formatter\CheckPermissions::class), (new Extend\Model(Post::class)) ->belongsToMany('mentionedBy', Post::class, 'post_mentions_post', 'mentions_post_id', 'post_id') ->belongsToMany('mentionsPosts', Post::class, 'post_mentions_post', 'post_id', 'mentions_post_id') - ->belongsToMany('mentionsUsers', User::class, 'post_mentions_user', 'post_id', 'mentions_user_id'), + ->belongsToMany('mentionsUsers', User::class, 'post_mentions_user', 'post_id', 'mentions_user_id') + ->belongsToMany('mentionsGroups', Group::class, 'post_mentions_group', 'post_id', 'mentions_group_id'), new Extend\Locales(__DIR__.'/locale'), @@ -51,25 +55,28 @@ return [ ->namespace('flarum-mentions', __DIR__.'/views'), (new Extend\Notification()) - ->type(PostMentionedBlueprint::class, PostSerializer::class, ['alert']) - ->type(UserMentionedBlueprint::class, PostSerializer::class, ['alert']), + ->type(Notification\PostMentionedBlueprint::class, PostSerializer::class, ['alert']) + ->type(Notification\UserMentionedBlueprint::class, PostSerializer::class, ['alert']) + ->type(Notification\GroupMentionedBlueprint::class, PostSerializer::class, ['alert']), (new Extend\ApiSerializer(BasicPostSerializer::class)) ->hasMany('mentionedBy', BasicPostSerializer::class) ->hasMany('mentionsPosts', BasicPostSerializer::class) - ->hasMany('mentionsUsers', BasicUserSerializer::class), + ->hasMany('mentionsUsers', BasicUserSerializer::class) + ->hasMany('mentionsGroups', GroupSerializer::class), (new Extend\ApiController(Controller\ShowDiscussionController::class)) ->addInclude(['posts.mentionedBy', 'posts.mentionedBy.user', 'posts.mentionedBy.discussion']) ->load([ 'posts.mentionsUsers', 'posts.mentionsPosts', 'posts.mentionsPosts.user', 'posts.mentionedBy', 'posts.mentionedBy.mentionsPosts', 'posts.mentionedBy.mentionsPosts.user', 'posts.mentionedBy.mentionsUsers', + 'posts.mentionsGroups' ]), (new Extend\ApiController(Controller\ListDiscussionsController::class)) ->load([ - 'firstPost.mentionsUsers', 'firstPost.mentionsPosts', 'firstPost.mentionsPosts.user', - 'lastPost.mentionsUsers', 'lastPost.mentionsPosts', 'lastPost.mentionsPosts.user' + 'firstPost.mentionsUsers', 'firstPost.mentionsPosts', 'firstPost.mentionsPosts.user', 'firstPost.mentionsGroups', + 'lastPost.mentionsUsers', 'lastPost.mentionsPosts', 'lastPost.mentionsPosts.user', 'lastPost.mentionsGroups' ]), (new Extend\ApiController(Controller\ShowPostController::class)) @@ -80,13 +87,16 @@ return [ ->load([ 'mentionsUsers', 'mentionsPosts', 'mentionsPosts.user', 'mentionedBy', 'mentionedBy.mentionsPosts', 'mentionedBy.mentionsPosts.user', 'mentionedBy.mentionsUsers', + 'mentionsGroups' ]), (new Extend\ApiController(Controller\CreatePostController::class)) - ->addInclude(['mentionsPosts', 'mentionsPosts.mentionedBy']), + ->addInclude(['mentionsPosts', 'mentionsPosts.mentionedBy']) + ->addOptionalInclude('mentionsGroups'), (new Extend\ApiController(Controller\UpdatePostController::class)) - ->addInclude(['mentionsPosts', 'mentionsPosts.mentionedBy']), + ->addInclude(['mentionsPosts', 'mentionsPosts.mentionedBy']) + ->addOptionalInclude('mentionsGroups'), (new Extend\ApiController(Controller\AbstractSerializeController::class)) ->prepareDataForSerialization(FilterVisiblePosts::class), @@ -103,4 +113,9 @@ return [ (new Extend\Filter(PostFilterer::class)) ->addFilter(Filter\MentionedFilter::class), + + (new Extend\ApiSerializer(CurrentUserSerializer::class)) + ->attribute('canMentionGroups', function (CurrentUserSerializer $serializer, User $user, array $attributes): bool { + return $user->can('mentionGroups'); + }) ]; diff --git a/extensions/mentions/js/src/admin/index.js b/extensions/mentions/js/src/admin/index.js index e34d92cbc..f8bc9c9fc 100644 --- a/extensions/mentions/js/src/admin/index.js +++ b/extensions/mentions/js/src/admin/index.js @@ -1,10 +1,20 @@ import app from 'flarum/admin/app'; app.initializers.add('flarum-mentions', function () { - app.extensionData.for('flarum-mentions').registerSetting({ - setting: 'flarum-mentions.allow_username_format', - type: 'boolean', - label: app.translator.trans('flarum-mentions.admin.settings.allow_username_format_label'), - help: app.translator.trans('flarum-mentions.admin.settings.allow_username_format_text'), - }); + app.extensionData + .for('flarum-mentions') + .registerSetting({ + setting: 'flarum-mentions.allow_username_format', + type: 'boolean', + label: app.translator.trans('flarum-mentions.admin.settings.allow_username_format_label'), + help: app.translator.trans('flarum-mentions.admin.settings.allow_username_format_text'), + }) + .registerPermission( + { + permission: 'mentionGroups', + label: app.translator.trans('flarum-mentions.admin.permissions.mention_groups_label'), + icon: 'fas fa-at', + }, + 'start' + ); }); diff --git a/extensions/mentions/js/src/forum/addComposerAutocomplete.js b/extensions/mentions/js/src/forum/addComposerAutocomplete.js index f1eec3122..78588a4da 100644 --- a/extensions/mentions/js/src/forum/addComposerAutocomplete.js +++ b/extensions/mentions/js/src/forum/addComposerAutocomplete.js @@ -10,6 +10,8 @@ import highlight from 'flarum/common/helpers/highlight'; import KeyboardNavigatable from 'flarum/forum/utils/KeyboardNavigatable'; import { truncate } from 'flarum/common/utils/string'; import { throttle } from 'flarum/common/utils/throttleDebounce'; +import Badge from 'flarum/common/components/Badge'; +import Group from 'flarum/common/models/Group'; import AutocompleteDropdown from './fragments/AutocompleteDropdown'; import getMentionText from './utils/getMentionText'; @@ -29,6 +31,7 @@ const throttledSearch = throttle( buildSuggestions(); }); + searched.push(typedLower); } } @@ -66,6 +69,13 @@ export default function addComposerAutocomplete() { const returnedUsers = Array.from(app.store.all('users')); const returnedUserIds = new Set(returnedUsers.map((u) => u.id())); + // Store groups, but exclude the two virtual groups - 'Guest' and 'Member'. + const returnedGroups = Array.from( + app.store.all('groups').filter((group) => { + return group.id() != Group.GUEST_ID && group.id() != Group.MEMBER_ID; + }) + ); + const applySuggestion = (replacement) => { this.attrs.composer.editor.replaceBeforeCursor(absMentionStart - 1, replacement + ' '); @@ -124,12 +134,41 @@ export default function addComposerAutocomplete() { ); }; + const makeGroupSuggestion = function (group, replacement, content, className = '') { + let groupName = group.namePlural().toLowerCase(); + + if (typed) { + groupName = highlight(groupName, typed); + } + + return ( + + ); + }; + const userMatches = function (user) { const names = [user.username(), user.displayName()]; return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed); }; + const groupMatches = function (group) { + const names = [group.nameSingular(), group.namePlural()]; + + return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed); + }; + const buildSuggestions = () => { const suggestions = []; @@ -141,6 +180,15 @@ export default function addComposerAutocomplete() { suggestions.push(makeSuggestion(user, getMentionText(user), '', 'MentionsDropdown-user')); }); + + // ... or groups. + if (app.session?.user?.canMentionGroups()) { + returnedGroups.forEach((group) => { + if (!groupMatches(group)) return; + + suggestions.push(makeGroupSuggestion(group, getMentionText(undefined, undefined, group), '', 'MentionsDropdown-group')); + }); + } } // If the user is replying to a discussion, or if they are editing a diff --git a/extensions/mentions/js/src/forum/compat.js b/extensions/mentions/js/src/forum/compat.js index 7ada97757..ee22c6773 100644 --- a/extensions/mentions/js/src/forum/compat.js +++ b/extensions/mentions/js/src/forum/compat.js @@ -1,3 +1,4 @@ +import GroupMentionedNotification from './components/GroupMentionedNotification'; import MentionsUserPage from './components/MentionsUserPage'; import PostMentionedNotification from './components/PostMentionedNotification'; import UserMentionedNotification from './components/UserMentionedNotification'; @@ -13,6 +14,7 @@ export default { 'mentions/components/MentionsUserPage': MentionsUserPage, 'mentions/components/PostMentionedNotification': PostMentionedNotification, 'mentions/components/UserMentionedNotification': UserMentionedNotification, + 'mentions/components/GroupMentionedNotification': GroupMentionedNotification, 'mentions/fragments/AutocompleteDropdown': AutocompleteDropdown, 'mentions/fragments/PostQuoteButton': PostQuoteButton, 'mentions/utils/getCleanDisplayName': getCleanDisplayName, diff --git a/extensions/mentions/js/src/forum/components/GroupMentionedNotification.js b/extensions/mentions/js/src/forum/components/GroupMentionedNotification.js new file mode 100644 index 000000000..838038ab9 --- /dev/null +++ b/extensions/mentions/js/src/forum/components/GroupMentionedNotification.js @@ -0,0 +1,25 @@ +import app from 'flarum/forum/app'; +import Notification from 'flarum/forum/components/Notification'; +import { truncate } from 'flarum/common/utils/string'; + +export default class GroupMentionedNotification extends Notification { + icon() { + return 'fas fa-at'; + } + + href() { + const post = this.attrs.notification.subject(); + + return app.route.discussion(post.discussion(), post.number()); + } + + content() { + const user = this.attrs.notification.fromUser(); + + return app.translator.trans('flarum-mentions.forum.notifications.group_mentioned_text', { user }); + } + + excerpt() { + return truncate(this.attrs.notification.subject().contentPlain(), 200); + } +} diff --git a/extensions/mentions/js/src/forum/index.js b/extensions/mentions/js/src/forum/index.js index ab5392f46..467e98e19 100644 --- a/extensions/mentions/js/src/forum/index.js +++ b/extensions/mentions/js/src/forum/index.js @@ -10,11 +10,16 @@ import addPostQuoteButton from './addPostQuoteButton'; import addComposerAutocomplete from './addComposerAutocomplete'; import PostMentionedNotification from './components/PostMentionedNotification'; import UserMentionedNotification from './components/UserMentionedNotification'; +import GroupMentionedNotification from './components/GroupMentionedNotification'; import UserPage from 'flarum/forum/components/UserPage'; import LinkButton from 'flarum/common/components/LinkButton'; import MentionsUserPage from './components/MentionsUserPage'; +import User from 'flarum/common/models/User'; +import Model from 'flarum/common/Model'; app.initializers.add('flarum-mentions', function () { + User.prototype.canMentionGroups = Model.attribute('canMentionGroups'); + // For every mention of a post inside a post's content, set up a hover handler // that shows a preview of the mentioned post. addPostMentionPreviews(); @@ -36,6 +41,7 @@ app.initializers.add('flarum-mentions', function () { app.notificationComponents.postMentioned = PostMentionedNotification; app.notificationComponents.userMentioned = UserMentionedNotification; + app.notificationComponents.groupMentioned = GroupMentionedNotification; // Add notification preferences. extend(NotificationGrid.prototype, 'notificationTypes', function (items) { @@ -50,6 +56,12 @@ app.initializers.add('flarum-mentions', function () { icon: 'fas fa-at', label: app.translator.trans('flarum-mentions.forum.settings.notify_user_mentioned_label'), }); + + items.add('groupMentioned', { + name: 'groupMentioned', + icon: 'fas fa-at', + label: app.translator.trans('flarum-mentions.forum.settings.notify_group_mentioned_label'), + }); }); // Add mentions tab in user profile diff --git a/extensions/mentions/js/src/forum/utils/getMentionText.js b/extensions/mentions/js/src/forum/utils/getMentionText.js index 99e92af30..6a99ee38e 100644 --- a/extensions/mentions/js/src/forum/utils/getMentionText.js +++ b/extensions/mentions/js/src/forum/utils/getMentionText.js @@ -1,7 +1,7 @@ import getCleanDisplayName, { shouldUseOldFormat } from './getCleanDisplayName'; /** - * Fetches the mention text for a specified user (and optionally a post ID for replies). + * Fetches the mention text for a specified user (and optionally a post ID for replies, or group). * * Automatically determines which mention syntax to be used based on the option in the * admin dashboard. Also performs display name clean-up automatically. @@ -17,9 +17,13 @@ import getCleanDisplayName, { shouldUseOldFormat } from './getCleanDisplayName'; * @example
One of the
One of the @Mods will look at this
', $response['data']['attributes']['contentHtml']); + $this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsGroups->find(4)); + } + + /** + * @test + */ + public function mentioning_an_invalid_group_doesnt_work() + { + $response = $this->send( + $this->request('POST', '/api/posts', [ + 'authenticatedAs' => 1, + 'json' => [ + 'data' => [ + 'attributes' => [ + 'content' => '@"InvalidGroup"#g99', + ], + 'relationships' => [ + 'discussion' => ['data' => ['id' => 2]], + ] + ], + ], + ]) + ); + + $this->assertEquals(201, $response->getStatusCode()); + + $response = json_decode($response->getBody(), true); + + $this->assertStringContainsString('@"InvalidGroup"#g99', $response['data']['attributes']['content']); + $this->assertStringNotContainsString('GroupMention', $response['data']['attributes']['contentHtml']); + $this->assertCount(0, CommentPost::find($response['data']['id'])->mentionsGroups); + } + + /** + * @test + */ + public function deleted_group_mentions_render_with_deleted_label() + { + $deleted_text = $this->app()->getContainer()->make('translator')->trans('flarum-mentions.forum.group_mention.deleted_text'); + + $response = $this->send( + $this->request('GET', '/api/posts/6', [ + 'authenticatedAs' => 1, + ]) + ); + + $this->assertEquals(200, $response->getStatusCode()); + + $response = json_decode($response->getBody(), true); + + $this->assertStringContainsString("@$deleted_text", $response['data']['attributes']['contentHtml']); + $this->assertStringContainsString('GroupMention', $response['data']['attributes']['contentHtml']); + $this->assertStringContainsString('GroupMention--deleted', $response['data']['attributes']['contentHtml']); + $this->assertStringNotContainsString('@OldGroupName', $response['data']['attributes']['contentHtml']); + $this->assertCount(0, CommentPost::find($response['data']['id'])->mentionsGroups); + } + + /** + * @test + */ + public function group_mentions_render_with_fresh_data() + { + $response = $this->send( + $this->request('GET', '/api/posts/7', [ + 'authenticatedAs' => 1, + ]) + ); + + $this->assertEquals(200, $response->getStatusCode()); + + $response = json_decode($response->getBody(), true); + + $this->assertStringContainsString('@Fresh Name', $response['data']['attributes']['contentHtml']); + $this->assertStringContainsString('GroupMention', $response['data']['attributes']['contentHtml']); + $this->assertStringNotContainsString('@OldGroupName', $response['data']['attributes']['contentHtml']); + $this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsGroups->find(11)); + } + + /** + * @test + */ + public function mentioning_a_group_as_an_admin_user_works() + { + $response = $this->send( + $this->request('POST', '/api/posts', [ + 'authenticatedAs' => 1, + 'json' => [ + 'data' => [ + 'attributes' => [ + 'content' => '@"Mods"#g4', + ], + 'relationships' => [ + 'discussion' => ['data' => ['id' => 2]], + ] + ] + ] + ]) + ); + + $this->assertEquals(201, $response->getStatusCode()); + + $response = json_decode($response->getBody(), true); + + $this->assertStringContainsString('@Mods', $response['data']['attributes']['contentHtml']); + $this->assertStringContainsString('fas fa-bolt', $response['data']['attributes']['contentHtml']); + $this->assertEquals('@"Mods"#g4', $response['data']['attributes']['content']); + $this->assertStringContainsString('GroupMention', $response['data']['attributes']['contentHtml']); + $this->assertCount(1, CommentPost::find($response['data']['id'])->mentionsGroups); + } + + /** + * @test + */ + public function mentioning_multiple_groups_as_an_admin_user_works() + { + $response = $this->send( + $this->request('POST', '/api/posts', [ + 'authenticatedAs' => 1, + 'json' => [ + 'data' => [ + 'attributes' => [ + 'content' => '@"Admins"#g1 @"Mods"#g4', + ], + 'relationships' => [ + 'discussion' => ['data' => ['id' => 2]], + ] + ] + ] + ]) + ); + + $this->assertEquals(201, $response->getStatusCode()); + + $response = json_decode($response->getBody(), true); + + $this->assertStringContainsString('@Admins', $response['data']['attributes']['contentHtml']); + $this->assertStringContainsString('@Mods', $response['data']['attributes']['contentHtml']); + $this->assertStringContainsString('fas fa-wrench', $response['data']['attributes']['contentHtml']); + $this->assertStringContainsString('fas fa-bolt', $response['data']['attributes']['contentHtml']); + $this->assertEquals('@"Admins"#g1 @"Mods"#g4', $response['data']['attributes']['content']); + $this->assertStringContainsString('GroupMention', $response['data']['attributes']['contentHtml']); + $this->assertCount(2, CommentPost::find($response['data']['id'])->mentionsGroups); + } + + /** + * @test + */ + public function mentioning_a_virtual_group_as_an_admin_user_does_not_work() + { + $response = $this->send( + $this->request('POST', '/api/posts', [ + 'authenticatedAs' => 1, + 'json' => [ + 'data' => [ + 'attributes' => [ + 'content' => '@"Members"#g3 @"Guests"#g2', + ], + 'relationships' => [ + 'discussion' => ['data' => ['id' => 2]], + ] + ] + ] + ]) + ); + + $this->assertEquals(201, $response->getStatusCode()); + + $response = json_decode($response->getBody(), true); + + $this->assertStringNotContainsString('@Members', $response['data']['attributes']['contentHtml']); + $this->assertStringNotContainsString('@Guests', $response['data']['attributes']['contentHtml']); + $this->assertEquals('@"Members"#g3 @"Guests"#g2', $response['data']['attributes']['content']); + $this->assertStringNotContainsString('GroupMention', $response['data']['attributes']['contentHtml']); + $this->assertCount(0, CommentPost::find($response['data']['id'])->mentionsGroups); + } + + /** + * @test + */ + public function regular_user_does_not_have_group_mention_permission_by_default() + { + $this->database(); + $this->assertFalse(User::find(3)->can('mentionGroups')); + } + + /** + * @test + */ + public function regular_user_does_have_group_mention_permission_when_added() + { + $this->prepareDatabase([ + 'group_permission' => [ + ['group_id' => Group::MEMBER_ID, 'permission' => 'mentionGroups'], + ] + ]); + + $this->database(); + $this->assertTrue(User::find(3)->can('mentionGroups')); + } + + /** + * @test + */ + public function user_without_permission_cannot_mention_groups() + { + $response = $this->send( + $this->request('POST', '/api/posts', [ + 'authenticatedAs' => 3, + 'json' => [ + 'data' => [ + 'attributes' => [ + 'content' => '@"Mods"#g4', + ], + 'relationships' => [ + 'discussion' => ['data' => ['id' => 2]], + ], + ], + ], + ]) + ); + + $this->assertEquals(201, $response->getStatusCode()); + + $response = json_decode($response->getBody(), true); + + $this->assertStringNotContainsString('@Mods', $response['data']['attributes']['contentHtml']); + $this->assertStringContainsString('@"Mods"#g4', $response['data']['attributes']['content']); + $this->assertStringNotContainsString('GroupMention', $response['data']['attributes']['contentHtml']); + $this->assertCount(0, CommentPost::find($response['data']['id'])->mentionsGroups); + } + + /** + * @test + */ + public function user_with_permission_can_mention_groups() + { + $this->prepareDatabase([ + 'group_permission' => [ + ['group_id' => Group::MEMBER_ID, 'permission' => 'mentionGroups'], + ] + ]); + + $response = $this->send( + $this->request('POST', '/api/posts', [ + 'authenticatedAs' => 3, + 'json' => [ + 'data' => [ + 'attributes' => [ + 'content' => '@"Mods"#g4', + ], + 'relationships' => [ + 'discussion' => ['data' => ['id' => 2]], + ], + ], + ], + ]) + ); + + $this->assertEquals(201, $response->getStatusCode()); + + $response = json_decode($response->getBody(), true); + + $this->assertStringContainsString('@Mods', $response['data']['attributes']['contentHtml']); + $this->assertStringContainsString('@"Mods"#g4', $response['data']['attributes']['content']); + $this->assertStringContainsString('GroupMention', $response['data']['attributes']['contentHtml']); + $this->assertCount(1, CommentPost::find($response['data']['id'])->mentionsGroups); + } + + /** + * @test + */ + public function user_with_permission_cannot_mention_hidden_groups() + { + $this->prepareDatabase([ + 'group_permission' => [ + ['group_id' => Group::MEMBER_ID, 'permission' => 'mentionGroups'], + ] + ]); + + $response = $this->send( + $this->request('POST', '/api/posts', [ + 'authenticatedAs' => 3, + 'json' => [ + 'data' => [ + 'attributes' => [ + 'content' => '@"Ninjas"#g10', + ], + 'relationships' => [ + 'discussion' => ['data' => ['id' => 2]], + ], + ], + ], + ]) + ); + + $this->assertEquals(201, $response->getStatusCode()); + + $response = json_decode($response->getBody(), true); + + $this->assertStringNotContainsString('@Ninjas', $response['data']['attributes']['contentHtml']); + $this->assertStringContainsString('@"Ninjas"#g10', $response['data']['attributes']['content']); + $this->assertStringNotContainsString('GroupMention', $response['data']['attributes']['contentHtml']); + $this->assertCount(0, CommentPost::find($response['data']['id'])->mentionsGroups); + } + + /** + * @test + */ + public function editing_a_post_that_has_a_mention_works() + { + $response = $this->send( + $this->request('PATCH', '/api/posts/4', [ + 'authenticatedAs' => 1, + 'json' => [ + 'data' => [ + 'attributes' => [ + 'content' => 'New content with @"Mods"#g4 mention', + ], + ], + ], + ]) + ); + + $this->assertEquals(200, $response->getStatusCode()); + + $response = json_decode($response->getBody(), true); + + $this->assertStringContainsString('@Mods', $response['data']['attributes']['contentHtml']); + $this->assertEquals('New content with @"Mods"#g4 mention', $response['data']['attributes']['content']); + $this->assertStringContainsString('GroupMention', $response['data']['attributes']['contentHtml']); + $this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsGroups->find(4)); + } +} diff --git a/extensions/mentions/views/emails/groupMentioned.blade.php b/extensions/mentions/views/emails/groupMentioned.blade.php new file mode 100644 index 000000000..52b6f0458 --- /dev/null +++ b/extensions/mentions/views/emails/groupMentioned.blade.php @@ -0,0 +1,7 @@ +{!! $translator->trans('flarum-mentions.email.group_mentioned.body', [ +'{recipient_display_name}' => $user->display_name, +'{mentioner_display_name}' => $blueprint->post->user->display_name, +'{title}' => $blueprint->post->discussion->title, +'{url}' => $url->to('forum')->route('discussion', ['id' => $blueprint->post->discussion_id, 'near' => $blueprint->post->number]), +'{content}' => $blueprint->post->content +]) !!} diff --git a/framework/core/src/Extend/Formatter.php b/framework/core/src/Extend/Formatter.php index 0eb79a9ff..75f7f5418 100644 --- a/framework/core/src/Extend/Formatter.php +++ b/framework/core/src/Extend/Formatter.php @@ -52,6 +52,7 @@ class Formatter implements ExtenderInterface, LifecycleInterface * - \s9e\TextFormatter\Parser $parser * - mixed $context * - string $text: The text to be parsed. + * - \Flarum\User\User|null $actor. This argument MUST either be nullable, or omitted entirely. * * The callback should return: * - string $text: The text to be parsed. diff --git a/framework/core/src/Formatter/Formatter.php b/framework/core/src/Formatter/Formatter.php index 46958d0e9..2db336cd2 100644 --- a/framework/core/src/Formatter/Formatter.php +++ b/framework/core/src/Formatter/Formatter.php @@ -9,6 +9,7 @@ namespace Flarum\Formatter; +use Flarum\User\User; use Illuminate\Contracts\Cache\Repository; use Psr\Http\Message\ServerRequestInterface; use s9e\TextFormatter\Configurator; @@ -83,14 +84,15 @@ class Formatter * * @param string $text * @param mixed $context + * @param User|null $user * @return string */ - public function parse($text, $context = null) + public function parse($text, $context = null, User $user = null) { $parser = $this->getParser($context); foreach ($this->parsingCallbacks as $callback) { - $text = $callback($parser, $context, $text); + $text = $callback($parser, $context, $text, $user); } return $parser->parse($text); diff --git a/framework/core/src/Post/Command/PostReplyHandler.php b/framework/core/src/Post/Command/PostReplyHandler.php index 96de698d0..b0bfb8877 100644 --- a/framework/core/src/Post/Command/PostReplyHandler.php +++ b/framework/core/src/Post/Command/PostReplyHandler.php @@ -85,7 +85,8 @@ class PostReplyHandler $discussion->id, Arr::get($command->data, 'attributes.content'), $actor->id, - $command->ipAddress + $command->ipAddress, + $command->actor, ); if ($actor->isAdmin() && ($time = Arr::get($command->data, 'attributes.createdAt'))) { diff --git a/framework/core/src/Post/CommentPost.php b/framework/core/src/Post/CommentPost.php index 6e89e79ea..7455abc12 100644 --- a/framework/core/src/Post/CommentPost.php +++ b/framework/core/src/Post/CommentPost.php @@ -44,9 +44,10 @@ class CommentPost extends Post * @param string $content * @param int $userId * @param string $ipAddress + * @param User|null $actor * @return static */ - public static function reply($discussionId, $content, $userId, $ipAddress) + public static function reply($discussionId, $content, $userId, $ipAddress, User $actor = null) { $post = new static; @@ -57,7 +58,7 @@ class CommentPost extends Post $post->ip_address = $ipAddress; // Set content last, as the parsing may rely on other post attributes. - $post->content = $content; + $post->setContentAttribute($content, $actor); $post->raise(new Posted($post)); @@ -74,7 +75,7 @@ class CommentPost extends Post public function revise($content, User $actor) { if ($this->content !== $content) { - $this->content = $content; + $this->setContentAttribute($content, $actor); $this->edited_at = Carbon::now(); $this->edited_user_id = $actor->id; @@ -145,10 +146,11 @@ class CommentPost extends Post * Parse the content before it is saved to the database. * * @param string $value + * @param User $actor */ - public function setContentAttribute($value) + public function setContentAttribute($value, User $actor = null) { - $this->attributes['content'] = $value ? static::$formatter->parse($value, $this) : null; + $this->attributes['content'] = $value ? static::$formatter->parse($value, $this, $actor ?? $this->user) : null; } /**