1
0
mirror of https://github.com/flarum/core.git synced 2025-08-11 10:55:47 +02:00

feat(mentions,tags): tag mentions (#3769)

* feat: add tag search

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* feat(mentions): tag mentions backend

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* feat: tag mention design

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* refactor: revamp mentions autocomplete

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* fix: unauthorized mention of hidden groups

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* feat(mentions,tags): use hash format for tag mentions

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* refactor: frontend mention format API with mentionable models

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* feat: implement tag search on the frontend

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* fix: tag color contrast

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* fix: tag suggestions styling

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* test: works with disabled tags extension

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* chore: move `MentionFormats` to `formats`

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* fix: mentions preview bad styling

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* docs: further migration location clarification

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* Apply fixes from StyleCI

* fix: bad test namespace

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* fix: phpstan

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* fix: conditionally add tag related extenders

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* Apply fixes from StyleCI

* feat(phpstan): evaluate conditional extenders

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* feat: use mithril routing for tag mentions

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

---------

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>
Co-authored-by: StyleCI Bot <bot@styleci.io>
This commit is contained in:
Sami Mazouz
2023-04-19 12:58:11 +01:00
committed by GitHub
parent b868c3d763
commit 5e281136f6
46 changed files with 1788 additions and 366 deletions

View File

@@ -33,40 +33,30 @@ class GroupMentionsTest extends TestCase
'users' => [
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
['id' => 5, 'username' => 'bad_user', 'email' => 'bad_user@machine.local', 'is_email_confirmed' => 1],
],
'discussions' => [
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
],
'posts' => [
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p>One of the <GROUPMENTION color="#80349E" groupname="Mods" icon="fas fa-bolt" id="4">@"Mods"#g4</GROUPMENTION> will look at this</p></r>'],
['id' => 6, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p><GROUPMENTION color="#80349E" groupname="OldGroupName" icon="fas fa-circle" id="100">@"OldGroupName"#g100</GROUPMENTION></p></r>'],
['id' => 7, 'number' => 4, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p><GROUPMENTION color="#000" groupname="OldGroupName" icon="fas fa-circle" id="11">@"OldGroupName"#g11</GROUPMENTION></p></r>'],
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p>One of the <GROUPMENTION groupname="Mods" id="4">@"Mods"#g4</GROUPMENTION> will look at this</p></r>'],
['id' => 6, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p><GROUPMENTION groupname="OldGroupName" id="100">@"OldGroupName"#g100</GROUPMENTION></p></r>'],
['id' => 7, 'number' => 4, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p><GROUPMENTION groupname="OldGroupName" id="11">@"OldGroupName"#g11</GROUPMENTION></p></r>'],
],
'post_mentions_group' => [
['post_id' => 4, 'mentions_group_id' => 4],
['post_id' => 7, 'mentions_group_id' => 11],
],
'group_user' => [
['group_id' => 9, 'user_id' => 4],
],
'group_permission' => [
['group_id' => Group::MEMBER_ID, 'permission' => 'postWithoutThrottle'],
['group_id' => 9, 'permission' => 'mentionGroups'],
],
'groups' => [
[
'id' => 10,
'name_singular' => 'Hidden',
'name_plural' => 'Ninjas',
'color' => null,
'icon' => 'fas fa-wrench',
'is_hidden' => 1
],
[
'id' => 11,
'name_singular' => 'Fresh Name',
'name_plural' => 'Fresh Name',
'color' => '#ccc',
'icon' => 'fas fa-users',
'is_hidden' => 0
]
['id' => 9, 'name_singular' => 'HasPermissionToMentionGroups', 'name_plural' => 'test'],
['id' => 10, 'name_singular' => 'Hidden', 'name_plural' => 'Ninjas', 'icon' => 'fas fa-wrench', 'color' => '#000', 'is_hidden' => 1],
['id' => 11, 'name_singular' => 'Fresh Name', 'name_plural' => 'Fresh Name', 'color' => '#ccc', 'icon' => 'fas fa-users', 'is_hidden' => 0]
]
]);
}
@@ -324,15 +314,9 @@ class GroupMentionsTest extends TestCase
*/
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,
'authenticatedAs' => 4,
'json' => [
'data' => [
'attributes' => [
@@ -361,15 +345,9 @@ class GroupMentionsTest extends TestCase
*/
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,
'authenticatedAs' => 4,
'json' => [
'data' => [
'attributes' => [

View File

@@ -0,0 +1,385 @@
<?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.
*/
namespace Flarum\Mentions\Tests\integration\api;
use Carbon\Carbon;
use Flarum\Group\Group;
use Flarum\Post\CommentPost;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
class TagMentionsTest extends TestCase
{
use RetrievesAuthorizedUsers;
protected function setUp(): void
{
parent::setUp();
$this->extension('flarum-tags', 'flarum-mentions');
$this->prepareDatabase([
'users' => [
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
],
'discussions' => [
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
],
'posts' => [
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><TAGMENTION id="1" slug="test_old_slug" tagname="TestOldName">#test_old_slug</TAGMENTION></r>'],
['id' => 7, 'number' => 5, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 2021, 'type' => 'comment', 'content' => '<r><TAGMENTION id="3" slug="support" tagname="Support">#deleted_relation</TAGMENTION></r>'],
['id' => 8, 'number' => 6, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><TAGMENTION id="2020" slug="i_am_a_deleted_tag" tagname="i_am_a_deleted_tag">#i_am_a_deleted_tag</TAGMENTION></r>'],
['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><TAGMENTION id="5" slug="laravel">#laravel</TAGMENTION></r>'],
],
'tags' => [
['id' => 1, 'name' => 'Test', 'slug' => 'test', 'is_restricted' => 0],
['id' => 2, 'name' => 'Flarum', 'slug' => 'flarum', 'is_restricted' => 0],
['id' => 3, 'name' => 'Support', 'slug' => 'support', 'is_restricted' => 0],
['id' => 4, 'name' => 'Dev', 'slug' => 'dev', 'is_restricted' => 1],
['id' => 5, 'name' => 'Laravel "#t6 Tag', 'slug' => 'laravel', 'is_restricted' => 0],
['id' => 6, 'name' => 'Tatakai', 'slug' => '戦い', 'is_restricted' => 0],
],
'post_mentions_tag' => [
['post_id' => 4, 'mentions_tag_id' => 1],
['post_id' => 5, 'mentions_tag_id' => 2],
['post_id' => 6, 'mentions_tag_id' => 3],
['post_id' => 10, 'mentions_tag_id' => 4],
['post_id' => 10, 'mentions_tag_id' => 5],
],
'group_permission' => [
['group_id' => Group::MEMBER_ID, 'permission' => 'postWithoutThrottle'],
],
]);
}
/** @test */
public function mentioning_a_valid_tag_with_valid_format_works()
{
$response = $this->send(
$this->request('POST', '/api/posts', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'content' => '#flarum',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
],
],
],
])
);
$this->assertEquals(201, $response->getStatusCode());
$response = json_decode($response->getBody(), true);
$this->assertStringContainsString('TagMention', $response['data']['attributes']['contentHtml']);
$this->assertStringNotContainsString('TagMention--deleted', $response['data']['attributes']['contentHtml']);
$this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsTags->find(2));
}
/** @test */
public function mentioning_a_valid_tag_using_cjk_slug_with_valid_format_works()
{
$response = $this->send(
$this->request('POST', '/api/posts', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'content' => '#戦い',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
],
],
],
])
);
$this->assertEquals(201, $response->getStatusCode());
$response = json_decode($response->getBody(), true);
$this->assertStringContainsString('Tatakai', $response['data']['attributes']['contentHtml']);
$this->assertStringContainsString('TagMention', $response['data']['attributes']['contentHtml']);
$this->assertStringNotContainsString('TagMention--deleted', $response['data']['attributes']['contentHtml']);
$this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsTags->find(6));
}
/** @test */
public function mentioning_an_invalid_tag_doesnt_work()
{
$response = $this->send(
$this->request('POST', '/api/posts', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'content' => '#franzofflarum',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
],
],
],
])
);
$this->assertEquals(201, $response->getStatusCode());
$response = json_decode($response->getBody(), true);
$this->assertEquals('#franzofflarum', $response['data']['attributes']['content']);
$this->assertStringNotContainsString('TagMention', $response['data']['attributes']['contentHtml']);
$this->assertCount(0, CommentPost::find($response['data']['id'])->mentionsTags);
}
/** @test */
public function mentioning_a_tag_when_tags_disabled_does_not_cause_errors()
{
$this->extensions = ['flarum-mentions'];
$response = $this->send(
$this->request('POST', '/api/posts', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'content' => '#test',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
],
],
],
])
);
$this->assertEquals(201, $response->getStatusCode());
$response = json_decode($response->getBody(), true);
$this->assertEquals('#test', $response['data']['attributes']['content']);
$this->assertStringNotContainsString('TagMention', $response['data']['attributes']['contentHtml']);
$this->assertNull(CommentPost::find($response['data']['id'])->mentionsTags);
}
/** @test */
public function mentioning_a_restricted_tag_doesnt_work_without_privileges()
{
$response = $this->send(
$this->request('POST', '/api/posts', [
'authenticatedAs' => 3,
'json' => [
'data' => [
'attributes' => [
'content' => '#dev',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
],
],
],
])
);
$this->assertEquals(201, $response->getStatusCode());
$response = json_decode($response->getBody(), true);
$this->assertEquals('#dev', $response['data']['attributes']['content']);
$this->assertStringNotContainsString('TagMention', $response['data']['attributes']['contentHtml']);
$this->assertCount(0, CommentPost::find($response['data']['id'])->mentionsTags);
}
/** @test */
public function mentioning_a_restricted_tag_works_with_privileges()
{
$response = $this->send(
$this->request('POST', '/api/posts', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'content' => '#dev',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
],
],
],
])
);
$this->assertEquals(201, $response->getStatusCode());
$response = json_decode($response->getBody(), true);
$this->assertEquals('#dev', $response['data']['attributes']['content']);
$this->assertStringContainsString('TagMention', $response['data']['attributes']['contentHtml']);
$this->assertCount(1, CommentPost::find($response['data']['id'])->mentionsTags);
}
/** @test */
public function mentioning_multiple_tags_works()
{
$response = $this->send(
$this->request('POST', '/api/posts', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'content' => '#test #flarum #support #laravel #franzofflarum',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
],
],
],
])
);
$this->assertEquals(201, $response->getStatusCode());
$response = json_decode($response->getBody(), true);
$this->assertStringContainsString('Test', $response['data']['attributes']['contentHtml']);
$this->assertStringContainsString('Flarum', $response['data']['attributes']['contentHtml']);
$this->assertEquals('#test #flarum #support #laravel #franzofflarum', $response['data']['attributes']['content']);
$this->assertStringContainsString('TagMention', $response['data']['attributes']['contentHtml']);
$this->assertStringNotContainsString('TagMention--deleted', $response['data']['attributes']['contentHtml']);
$this->assertCount(4, CommentPost::find($response['data']['id'])->mentionsTags);
}
/** @test */
public function tag_mentions_render_with_fresh_data()
{
$response = $this->send(
$this->request('GET', '/api/posts/4', [
'authenticatedAs' => 1,
])
);
$this->assertEquals(200, $response->getStatusCode());
$response = json_decode($response->getBody(), true);
$this->assertStringContainsString('Test', $response['data']['attributes']['contentHtml']);
$this->assertStringContainsString('TagMention', $response['data']['attributes']['contentHtml']);
$this->assertCount(1, CommentPost::find($response['data']['id'])->mentionsTags);
}
/** @test */
public function tag_mentions_dont_cause_errors_when_tags_disabled()
{
$this->extensions = ['flarum-mentions'];
$response = $this->send(
$this->request('GET', '/api/posts/4', [
'authenticatedAs' => 1,
])
);
$this->assertEquals(200, $response->getStatusCode());
}
/** @test */
public function tag_mentions_unparse_with_fresh_data()
{
$response = $this->send(
$this->request('GET', '/api/posts/4', [
'authenticatedAs' => 1,
])
);
$this->assertEquals(200, $response->getStatusCode());
$response = json_decode($response->getBody(), true);
$this->assertStringContainsString('#test', $response['data']['attributes']['content']);
$this->assertCount(1, CommentPost::find($response['data']['id'])->mentionsTags);
}
/** @test */
public function deleted_tag_mentions_unparse_and_render_as_expected()
{
// No reason to hide a deleted tag's name.
$deleted_text = 'i_am_a_deleted_tag';
$response = $this->send(
$this->request('GET', '/api/posts/8', [
'authenticatedAs' => 1,
])
);
$this->assertEquals(200, $response->getStatusCode());
$response = json_decode($response->getBody(), true);
$this->assertStringContainsString($deleted_text, $response['data']['attributes']['contentHtml']);
$this->assertStringContainsString("#$deleted_text", $response['data']['attributes']['content']);
$this->assertStringContainsString('TagMention', $response['data']['attributes']['contentHtml']);
$this->assertStringContainsString('TagMention--deleted', $response['data']['attributes']['contentHtml']);
$this->assertCount(0, CommentPost::find($response['data']['id'])->mentionsTags);
}
/** @test */
public function deleted_tag_mentions_relation_unparse_and_render_as_expected()
{
// No reason to hide a deleted tag's name.
$deleted_text = 'deleted_relation';
$response = $this->send(
$this->request('GET', '/api/posts/7', [
'authenticatedAs' => 1,
])
);
$this->assertEquals(200, $response->getStatusCode());
$response = json_decode($response->getBody(), true);
$this->assertStringContainsString('Support', $response['data']['attributes']['contentHtml']);
$this->assertStringContainsString("#$deleted_text", $response['data']['attributes']['content']);
$this->assertStringContainsString('TagMention', $response['data']['attributes']['contentHtml']);
$this->assertStringContainsString('TagMention--deleted', $response['data']['attributes']['contentHtml']);
$this->assertCount(0, CommentPost::find($response['data']['id'])->mentionsTags);
}
/** @test */
public function editing_a_post_that_has_a_tag_mention_works()
{
$response = $this->send(
$this->request('PATCH', '/api/posts/10', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'content' => '#laravel',
],
],
],
])
);
$this->assertEquals(200, $response->getStatusCode());
$response = json_decode($response->getBody(), true);
$this->assertStringContainsString('Laravel "#t6 Tag', $response['data']['attributes']['contentHtml']);
$this->assertEquals('#laravel', $response['data']['attributes']['content']);
$this->assertStringContainsString('TagMention', $response['data']['attributes']['contentHtml']);
$this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsTags->find(5));
}
}