mirror of
https://github.com/flarum/core.git
synced 2025-08-16 05:14:20 +02:00
Compare commits
1 Commits
sm/jas-no-
...
sm/itemlis
Author | SHA1 | Date | |
---|---|---|---|
|
13f997d784 |
@@ -23,6 +23,3 @@ indent_size = 2
|
||||
|
||||
[*.neon]
|
||||
indent_style = tab
|
||||
|
||||
[{install,update}.php]
|
||||
indent_size = 2
|
||||
|
54
.github/workflows/REUSABLE_backend.yml
vendored
54
.github/workflows/REUSABLE_backend.yml
vendored
@@ -31,8 +31,7 @@ on:
|
||||
description: Versions of PHP to test with. Should be array of strings encoded as JSON array
|
||||
type: string
|
||||
required: false
|
||||
# Keep PHP versions synced with build-install-packages.yml
|
||||
default: '["8.1", "8.2", "8.3"]'
|
||||
default: '["8.1", "8.2"]'
|
||||
|
||||
php_extensions:
|
||||
description: PHP extensions to install.
|
||||
@@ -44,7 +43,7 @@ on:
|
||||
description: Versions of databases to test with. Should be array of strings encoded as JSON array
|
||||
type: string
|
||||
required: false
|
||||
default: '["mysql:5.7", "mysql:8.0.30", "mysql:8.1.0", "mariadb", "sqlite:3"]'
|
||||
default: '["mysql:5.7", "mysql:8.0.30", "mysql:8.1.0", "mariadb"]'
|
||||
|
||||
php_ini_values:
|
||||
description: PHP ini values
|
||||
@@ -52,26 +51,14 @@ on:
|
||||
required: false
|
||||
default: error_reporting=E_ALL
|
||||
|
||||
runner_type:
|
||||
description: The type of runner to use for the jobs. This should be one of the types supported by the `runs-on` keyword.
|
||||
type: string
|
||||
required: false
|
||||
default: 'ubuntu-latest'
|
||||
|
||||
secrets:
|
||||
composer_auth:
|
||||
description: The Composer auth tokens to use for private packages.
|
||||
required: false
|
||||
|
||||
env:
|
||||
COMPOSER_ROOT_VERSION: dev-main
|
||||
# `inputs.composer_directory` defaults to `inputs.backend_directory`
|
||||
FLARUM_TEST_TMP_DIR_LOCAL: tests/integration/tmp
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -85,49 +72,32 @@ jobs:
|
||||
# Expands the matrix by naming DBs.
|
||||
- service: 'mysql:5.7'
|
||||
db: MySQL 5.7
|
||||
driver: mysql
|
||||
- service: 'mysql:8.0.30'
|
||||
db: MySQL 8.0
|
||||
driver: mysql
|
||||
- service: mariadb
|
||||
db: MariaDB
|
||||
driver: mysql
|
||||
- service: 'mysql:8.1.0'
|
||||
db: MySQL 8.1
|
||||
driver: mysql
|
||||
- service: 'sqlite:3'
|
||||
db: SQLite
|
||||
driver: sqlite
|
||||
|
||||
# Include Database prefix tests with only one PHP version.
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:5.7'
|
||||
db: MySQL 5.7
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:8.0.30'
|
||||
db: MySQL 8.0
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: mariadb
|
||||
db: MariaDB
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:8.1.0'
|
||||
db: MySQL 8.1
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'sqlite:3'
|
||||
db: SQLite
|
||||
driver: sqlite
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
|
||||
@@ -135,22 +105,10 @@ jobs:
|
||||
exclude:
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: 'mysql:8.0.30'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: mariadb
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: mariadb
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:8.1.0'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: 'mysql:8.1.0'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'sqlite:3'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: 'sqlite:3'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: ${{ matrix.service != 'sqlite:3' && matrix.service || '' }}
|
||||
image: ${{ matrix.service }}
|
||||
ports:
|
||||
- 13306:3306
|
||||
|
||||
@@ -173,7 +131,6 @@ jobs:
|
||||
ini-values: ${{ matrix.php_ini_values }}
|
||||
|
||||
- name: Create MySQL Database
|
||||
if: ${{ matrix.service != 'sqlite:3' }}
|
||||
run: |
|
||||
sudo systemctl start mysql
|
||||
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
|
||||
@@ -203,11 +160,10 @@ jobs:
|
||||
DB_PORT: 13306
|
||||
DB_PASSWORD: root
|
||||
DB_PREFIX: ${{ matrix.prefix }}
|
||||
DB_DRIVER: ${{ matrix.driver }}
|
||||
COMPOSER_PROCESS_TIMEOUT: 600
|
||||
|
||||
phpstan:
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
12
.github/workflows/REUSABLE_frontend.yml
vendored
12
.github/workflows/REUSABLE_frontend.yml
vendored
@@ -86,30 +86,20 @@ on:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
runner_type:
|
||||
description: The type of runner to use for the jobs. This should be one of the types supported by the `runs-on` keyword.
|
||||
type: string
|
||||
required: false
|
||||
default: 'ubuntu-latest'
|
||||
|
||||
secrets:
|
||||
bundlewatch_github_token:
|
||||
description: The GitHub token to use for Bundlewatch.
|
||||
required: false
|
||||
composer_auth:
|
||||
description: The Composer auth tokens to use for private packages.
|
||||
required: false
|
||||
|
||||
env:
|
||||
COMPOSER_ROOT_VERSION: dev-main
|
||||
ci_script: ${{ inputs.js_package_manager == 'yarn' && 'yarn install --immutable' || 'npm ci' }}
|
||||
cache_dependency_path: ${{ inputs.cache_dependency_path || format(inputs.js_package_manager == 'yarn' && '{0}/yarn.lock' || '{0}/package-lock.json', inputs.frontend_directory) }}
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Checks & Build
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: >-
|
||||
((github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) || github.event_name != 'pull_request')
|
||||
|
29
.github/workflows/build-install-packages.yml
vendored
29
.github/workflows/build-install-packages.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: Build Install Packages
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
env:
|
||||
VERSION: ${{ github.event.release.tag_name }}
|
||||
PHP_VERSIONS: '8.1 8.2 8.3'
|
||||
INSTALL_PACKAGES_INPUTS: '{ "flarum_version": "{0}", "php_versions": "{1}" }'
|
||||
|
||||
jobs:
|
||||
delay:
|
||||
name: Wait for packagist to publish new packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: sleep 30m
|
||||
|
||||
build:
|
||||
name: Build Installation Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger build in flarum/installation-packages
|
||||
uses: benc-uk/workflow-dispatch@v1
|
||||
with:
|
||||
workflow: Build Flarum Install Packages
|
||||
repo: flarum/installation-packages
|
||||
token: ${{ secrets.PACKAGES_BUILD_TOKEN }}
|
||||
inputs: ${{ format(env.INSTALL_PACKAGES_INPUTS, env.VERSION, env.PHP_VERSIONS) }}
|
@@ -38,4 +38,3 @@ If you discover a security vulnerability within Flarum, please send an e-mail to
|
||||
## License
|
||||
|
||||
Flarum is open-source software licensed under the [MIT License](https://github.com/flarum/flarum/blob/master/LICENSE).
|
||||
|
||||
|
@@ -46,7 +46,7 @@
|
||||
"Flarum\\Lock\\": "extensions/lock/src",
|
||||
"Flarum\\Mentions\\": "extensions/mentions/src",
|
||||
"Flarum\\Nicknames\\": "extensions/nicknames/src",
|
||||
"Flarum\\ExtensionManager\\": "extensions/package-manager/src",
|
||||
"Flarum\\PackageManager\\": "extensions/package-manager/src",
|
||||
"Flarum\\Pusher\\": "extensions/pusher/src",
|
||||
"Flarum\\Statistics\\": "extensions/statistics/src",
|
||||
"Flarum\\Sticky\\": "extensions/sticky/src",
|
||||
@@ -70,7 +70,7 @@
|
||||
"Flarum\\Lock\\Tests\\": "extensions/lock/tests",
|
||||
"Flarum\\Mentions\\Tests\\": "extensions/mentions/tests",
|
||||
"Flarum\\Nicknames\\Tests\\": "extensions/nicknames/tests",
|
||||
"Flarum\\ExtensionManager\\Tests\\": "extensions/package-manager/tests",
|
||||
"Flarum\\PackageManager\\Tests\\": "extensions/package-manager/tests",
|
||||
"Flarum\\Pusher\\Tests\\": "extensions/pusher/tests",
|
||||
"Flarum\\Statistics\\Tests\\": "extensions/statistics/tests",
|
||||
"Flarum\\Sticky\\Tests\\": "extensions/sticky/tests",
|
||||
@@ -94,7 +94,7 @@
|
||||
"flarum/markdown": "self.version",
|
||||
"flarum/mentions": "self.version",
|
||||
"flarum/nicknames": "self.version",
|
||||
"flarum/extension-manager": "self.version",
|
||||
"flarum/package-manager": "self.version",
|
||||
"flarum/pusher": "self.version",
|
||||
"flarum/statistics": "self.version",
|
||||
"flarum/sticky": "self.version",
|
||||
@@ -108,11 +108,10 @@
|
||||
"php": "^8.1",
|
||||
"ext-json": "*",
|
||||
"components/font-awesome": "^5.15.0",
|
||||
"composer/composer": "^2.7",
|
||||
"composer/composer": "^2.0",
|
||||
"dflydev/fig-cookies": "^3.0",
|
||||
"doctrine/dbal": "^3.6.2",
|
||||
"dragonmantank/cron-expression": "^3.3",
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"franzl/whoops-middleware": "2.0",
|
||||
"guzzlehttp/guzzle": "*",
|
||||
"illuminate/bus": "^10.0",
|
||||
@@ -131,7 +130,7 @@
|
||||
"illuminate/support": "^10.0",
|
||||
"illuminate/validation": "^10.0",
|
||||
"illuminate/view": "^10.0",
|
||||
"intervention/image": "^3.2",
|
||||
"intervention/image": "^2.7.2",
|
||||
"jenssegers/agent": "^2.6",
|
||||
"laminas/laminas-diactoros": "^3.0",
|
||||
"laminas/laminas-httphandlerrunner": "^2.6",
|
||||
@@ -151,6 +150,7 @@
|
||||
"pusher/pusher-php-server": "^7.2",
|
||||
"s9e/text-formatter": "^2.13",
|
||||
"staudenmeir/eloquent-eager-limit": "^1.8.2",
|
||||
"sycho/json-api": "^0.5.0",
|
||||
"sycho/sourcemap": "^2.0.0",
|
||||
"symfony/config": "^6.3",
|
||||
"symfony/console": "^6.3",
|
||||
@@ -162,14 +162,13 @@
|
||||
"symfony/postmark-mailer": "^6.3",
|
||||
"symfony/translation": "^6.3",
|
||||
"symfony/yaml": "^6.3",
|
||||
"flarum/json-api-server": "^0.1.0",
|
||||
"wikimedia/less.php": "^4.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.5",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"phpstan/phpstan": "^1.10.0",
|
||||
"larastan/larastan": "^2.7",
|
||||
"nunomaduro/larastan": "^2.6",
|
||||
"symfony/var-dumper": "^6.3"
|
||||
},
|
||||
"config": {
|
||||
|
@@ -47,7 +47,7 @@ class Akismet
|
||||
$client = new Client();
|
||||
|
||||
return $client->request('POST', "$this->apiUrl/$type", [
|
||||
'headers' => [
|
||||
'headers' => [
|
||||
'User-Agent' => "Flarum/$this->flarumVersion | Akismet/$this->extensionVersion",
|
||||
],
|
||||
'form_params' => $this->params,
|
||||
|
@@ -7,10 +7,9 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Api\Serializer\BasicDiscussionSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Approval\Access;
|
||||
use Flarum\Approval\Api\PostResourceFields;
|
||||
use Flarum\Approval\Event\PostWasApproved;
|
||||
use Flarum\Approval\Listener;
|
||||
use Flarum\Discussion\Discussion;
|
||||
@@ -37,13 +36,17 @@ return [
|
||||
->default('is_approved', true)
|
||||
->cast('is_approved', 'bool'),
|
||||
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Boolean::make('isApproved'),
|
||||
]),
|
||||
(new Extend\ApiSerializer(BasicDiscussionSerializer::class))
|
||||
->attribute('isApproved', function (BasicDiscussionSerializer $serializer, Discussion $discussion): bool {
|
||||
return $discussion->is_approved;
|
||||
}),
|
||||
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class),
|
||||
(new Extend\ApiSerializer(PostSerializer::class))
|
||||
->attribute('isApproved', function ($serializer, Post $post) {
|
||||
return (bool) $post->is_approved;
|
||||
})->attribute('canApprove', function (PostSerializer $serializer, Post $post) {
|
||||
return (bool) $serializer->getActor()->can('approvePosts', $post->discussion);
|
||||
}),
|
||||
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
|
@@ -1,29 +0,0 @@
|
||||
<?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\Approval\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Post\Post;
|
||||
|
||||
class PostResourceFields
|
||||
{
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Boolean::make('isApproved')
|
||||
->writable(fn (Post $post, Context $context) => $context->getActor()->can('approve', $post))
|
||||
// set by the ApproveContent listener.
|
||||
->set(fn () => null),
|
||||
Schema\Boolean::make('canApprove')
|
||||
->get(fn (Post $post, Context $context) => $context->getActor()->can('approvePosts', $post->discussion)),
|
||||
];
|
||||
}
|
||||
}
|
@@ -11,7 +11,6 @@ namespace Flarum\Approval\Listener;
|
||||
|
||||
use Flarum\Approval\Event\PostWasApproved;
|
||||
use Flarum\Post\Event\Saving;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class ApproveContent
|
||||
@@ -21,42 +20,23 @@ class ApproveContent
|
||||
$events->listen(Saving::class, $this->approvePost(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PermissionDeniedException
|
||||
*/
|
||||
public function approvePost(Saving $event): void
|
||||
{
|
||||
$attributes = $event->data['attributes'];
|
||||
$post = $event->post;
|
||||
|
||||
// Nothing to do if it is already approved.
|
||||
if ($post->is_approved) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* We approve a post in one of two cases:
|
||||
* - The post was unapproved and the allowed action is approving it. We trigger an event.
|
||||
* - The post was unapproved and the allowed actor is hiding or un-hiding it.
|
||||
* We approve it silently if the action is unhiding.
|
||||
*/
|
||||
$approvingSilently = false;
|
||||
|
||||
if (isset($attributes['isApproved'])) {
|
||||
$event->actor->assertCan('approve', $post);
|
||||
|
||||
$isApproved = (bool) $attributes['isApproved'];
|
||||
} elseif (isset($attributes['isHidden']) && $event->actor->can('approve', $post)) {
|
||||
} elseif (! empty($attributes['isHidden']) && $event->actor->can('approve', $post)) {
|
||||
$isApproved = true;
|
||||
$approvingSilently = $attributes['isHidden'];
|
||||
}
|
||||
|
||||
if (! empty($isApproved)) {
|
||||
$post->is_approved = true;
|
||||
|
||||
if (! $approvingSilently) {
|
||||
$post->raise(new PostWasApproved($post, $event->actor));
|
||||
}
|
||||
$post->raise(new PostWasApproved($post, $event->actor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,23 +10,19 @@
|
||||
namespace Flarum\Approval\Tests\integration;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
|
||||
trait InteractsWithUnapprovedContent
|
||||
{
|
||||
protected function prepareUnapprovedDatabaseContent()
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
User::class => [
|
||||
'users' => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'luceos', 'email' => 'luceos@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
Discussion::class => [
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1, 'is_private' => 0],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 2, 'comment_count' => 1, 'is_approved' => 0, 'is_private' => 1],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 3, 'comment_count' => 1, 'is_approved' => 0, 'is_private' => 1],
|
||||
@@ -35,7 +31,7 @@ trait InteractsWithUnapprovedContent
|
||||
['id' => 6, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 6, 'comment_count' => 1, 'is_approved' => 0, 'is_private' => 1],
|
||||
['id' => 7, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 7, 'comment_count' => 1, 'is_approved' => 1, 'is_private' => 0],
|
||||
],
|
||||
Post::class => [
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 3, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
@@ -49,7 +45,7 @@ trait InteractsWithUnapprovedContent
|
||||
['id' => 10, 'discussion_id' => 7, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 4],
|
||||
['id' => 11, 'discussion_id' => 7, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 1, 'is_approved' => 0, 'number' => 5],
|
||||
],
|
||||
Group::class => [
|
||||
'groups' => [
|
||||
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0]
|
||||
],
|
||||
'group_user' => [
|
||||
|
@@ -1,123 +0,0 @@
|
||||
<?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\Approval\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Approval\Tests\integration\InteractsWithUnapprovedContent;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
|
||||
class ApprovePostsTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
use InteractsWithUnapprovedContent;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-approval');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'luceos', 'email' => 'luceos@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 0, 'number' => 3],
|
||||
['id' => 4, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => Carbon::now(), 'is_approved' => 1, 'number' => 4],
|
||||
['id' => 5, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 0, 'number' => 5],
|
||||
],
|
||||
'groups' => [
|
||||
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
],
|
||||
'group_user' => [
|
||||
['user_id' => 3, 'group_id' => 4],
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => 4, 'permission' => 'discussion.approvePosts'],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function can_approve_unapproved_post()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/posts/3', [
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'isApproved' => true
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
$this->assertEquals(1, $this->database()->table('posts')->where('id', 3)->where('is_approved', 1)->count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function cannot_approve_post_without_permission()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/posts/3', [
|
||||
'authenticatedAs' => 4,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'isApproved' => true
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
$this->assertEquals(0, $this->database()->table('posts')->where('id', 3)->where('is_approved', 1)->count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function hiding_post_silently_approves_it()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/posts/5', [
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'isHidden' => true
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
$this->assertEquals(1, $this->database()->table('posts')->where('id', 5)->where('is_approved', 1)->count());
|
||||
}
|
||||
}
|
@@ -1,153 +0,0 @@
|
||||
<?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\Approval\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Approval\Tests\integration\InteractsWithUnapprovedContent;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
|
||||
class CreatePostsTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
use InteractsWithUnapprovedContent;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-flags', 'flarum-approval');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'luceos', 'email' => 'luceos@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 2, 'comment_count' => 1, 'is_approved' => 0],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 3, 'comment_count' => 1, 'is_approved' => 0],
|
||||
],
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 3],
|
||||
['id' => 4, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 5, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 6, 'discussion_id' => 2, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 3],
|
||||
['id' => 7, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 8, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 9, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 0, 'number' => 3],
|
||||
],
|
||||
'groups' => [
|
||||
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
],
|
||||
'group_user' => [
|
||||
['user_id' => 3, 'group_id' => 4],
|
||||
['user_id' => 2, 'group_id' => 5],
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => 4, 'permission' => 'discussion.startWithoutApproval'],
|
||||
['group_id' => 5, 'permission' => 'discussion.replyWithoutApproval'],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider startDiscussionDataProvider
|
||||
* @test
|
||||
*/
|
||||
public function can_start_discussion_without_approval_when_allowed(int $authenticatedAs, bool $allowed)
|
||||
{
|
||||
$this->database()->table('group_permission')->where('group_id', Group::MEMBER_ID)->where('permission', 'discussion.startWithoutApproval')->delete();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/discussions', [
|
||||
'authenticatedAs' => $authenticatedAs,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'discussions',
|
||||
'attributes' => [
|
||||
'title' => 'This is a new discussion',
|
||||
'content' => 'This is a new discussion',
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$body = $response->getBody()->getContents();
|
||||
$json = json_decode($body, true);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode(), $body);
|
||||
$this->assertEquals($allowed ? 1 : 0, $this->database()->table('discussions')->where('id', $json['data']['id'])->value('is_approved'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider replyToDiscussionDataProvider
|
||||
* @test
|
||||
*/
|
||||
public function can_reply_without_approval_when_allowed(?int $authenticatedAs, bool $allowed)
|
||||
{
|
||||
$this->database()->table('group_permission')->where('group_id', Group::MEMBER_ID)->where('permission', 'discussion.replyWithoutApproval')->delete();
|
||||
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/posts', [
|
||||
'authenticatedAs' => $authenticatedAs,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => 'This is a new reply',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => [
|
||||
'data' => [
|
||||
'type' => 'discussions',
|
||||
'id' => 1
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$body = $response->getBody()->getContents();
|
||||
$json = json_decode($body, true);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode(), $body);
|
||||
$this->assertEquals($allowed ? 1 : 0, $this->database()->table('posts')->where('id', $json['data']['id'])->value('is_approved'));
|
||||
}
|
||||
|
||||
public static function startDiscussionDataProvider(): array
|
||||
{
|
||||
return [
|
||||
'Admin' => [1, true],
|
||||
'User without permission' => [2, false],
|
||||
'Permission Given' => [3, true],
|
||||
'Another user without permission' => [4, false],
|
||||
];
|
||||
}
|
||||
|
||||
public static function replyToDiscussionDataProvider(): array
|
||||
{
|
||||
return [
|
||||
'Admin' => [1, true],
|
||||
'User without permission' => [3, false],
|
||||
'Permission Given' => [2, true],
|
||||
'Another user without permission' => [4, false],
|
||||
];
|
||||
}
|
||||
}
|
@@ -50,7 +50,7 @@
|
||||
padding: 15px 15px;
|
||||
|
||||
.scrolled & {
|
||||
box-shadow: 0 2px 6px var(--shadow-color);
|
||||
.box-shadow(0 2px 6px var(--shadow-color));
|
||||
}
|
||||
}
|
||||
|
||||
|
6
extensions/emoji/js/.prettierrc.json
Normal file
6
extensions/emoji/js/.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"printWidth": 150,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
2
extensions/emoji/js/dist/forum.js
generated
vendored
2
extensions/emoji/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/emoji/js/dist/forum.js.map
generated
vendored
2
extensions/emoji/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -1,47 +1,47 @@
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import TextEditorButton from 'flarum/common/components/TextEditorButton';
|
||||
import KeyboardNavigatable from 'flarum/common/utils/KeyboardNavigatable';
|
||||
import Tooltip from 'flarum/common/components/Tooltip';
|
||||
import AutocompleteReader from 'flarum/common/utils/AutocompleteReader';
|
||||
|
||||
import AutocompleteDropdown from './fragments/AutocompleteDropdown';
|
||||
import getEmojiIconCode from './helpers/getEmojiIconCode';
|
||||
import cdn from './cdn';
|
||||
|
||||
export default function addComposerAutocomplete() {
|
||||
const $container = $('<div class="ComposerBody-emojiDropdownContainer"></div>');
|
||||
const dropdown = new AutocompleteDropdown();
|
||||
let emojiMap = null;
|
||||
|
||||
extend('flarum/common/components/TextEditor', 'oninit', function () {
|
||||
this._loaders.push(async () => await import('./emojiMap').then((m) => (emojiMap = m.default)));
|
||||
// prettier-ignore
|
||||
this.commonEmoji = [
|
||||
'😀', '😁', '😂', '😃', '😄', '😅', '😆', '😇', '😈', '😉', '😊', '😋', '😌', '😍', '😎', '😏', '😐️', '😑', '😒',
|
||||
'😓', '😔', '😕', '😖', '😗', '😘', '😙', '😚', '😛', '😜', '😝', '😞', '😟', '😠', '😡', '😢', '😣', '😤', '😥',
|
||||
'😦', '😧', '😨', '😩', '😪', '😫', '😬', '😭', '😮', '😮💨', '😯', '😰', '😱', '😲', '😳', '😴', '😵', '😵💫',
|
||||
'😶', '😶🌫️', '😷', '😸', '😹', '😺', '😻', '😼', '😽', '😾', '😿', '🙀', '🙁', '🙂', '🙃', '🙄',
|
||||
];
|
||||
});
|
||||
|
||||
extend('flarum/common/components/TextEditor', 'onbuild', function () {
|
||||
this.emojiDropdown = new AutocompleteDropdown();
|
||||
const $editor = this.$('.TextEditor-editor').wrap('<div class="ComposerBody-emojiWrapper"></div>');
|
||||
|
||||
this.navigator = new KeyboardNavigatable();
|
||||
this.navigator
|
||||
.when(() => this.emojiDropdown.active)
|
||||
.onUp(() => this.emojiDropdown.navigate(-1))
|
||||
.onDown(() => this.emojiDropdown.navigate(1))
|
||||
.onSelect(this.emojiDropdown.complete.bind(this.emojiDropdown))
|
||||
.onCancel(this.emojiDropdown.hide.bind(this.emojiDropdown))
|
||||
.when(() => dropdown.active)
|
||||
.onUp(() => dropdown.navigate(-1))
|
||||
.onDown(() => dropdown.navigate(1))
|
||||
.onSelect(dropdown.complete.bind(dropdown))
|
||||
.onCancel(dropdown.hide.bind(dropdown))
|
||||
.bindTo($editor);
|
||||
|
||||
$editor.after($('<div class="ComposerBody-emojiDropdownContainer"></div>'));
|
||||
$editor.after($container);
|
||||
});
|
||||
|
||||
extend('flarum/common/components/TextEditor', 'buildEditorParams', function (params) {
|
||||
const emojiKeys = Object.keys(emojiMap);
|
||||
|
||||
const autocompleteReader = new AutocompleteReader(':');
|
||||
let relEmojiStart;
|
||||
let absEmojiStart;
|
||||
let typed;
|
||||
|
||||
const applySuggestion = (replacement) => {
|
||||
this.attrs.composer.editor.replaceBeforeCursor(absEmojiStart - 1, replacement + ' ');
|
||||
|
||||
dropdown.hide();
|
||||
};
|
||||
|
||||
params.inputListeners.push(() => {
|
||||
const selection = this.attrs.composer.editor.getSelectionRange();
|
||||
@@ -50,34 +50,42 @@ export default function addComposerAutocomplete() {
|
||||
|
||||
if (selection[1] - cursor > 0) return;
|
||||
|
||||
// Search backwards from the cursor for an ':' symbol. If we find
|
||||
// one and followed by a whitespace, we will want to show the
|
||||
// autocomplete dropdown!
|
||||
const lastChunk = this.attrs.composer.editor.getLastNChars(15);
|
||||
const autocompleting = autocompleteReader.check(lastChunk, cursor, /[a-z0-9]|\+|\-|_|\:/);
|
||||
absEmojiStart = 0;
|
||||
for (let i = lastChunk.length - 1; i >= 0; i--) {
|
||||
const character = lastChunk.substr(i, 1);
|
||||
// check what user typed, emoji names only contains alphanumeric,
|
||||
// underline, '+' and '-'
|
||||
if (!/[a-z0-9]|\+|\-|_|\:/.test(character)) break;
|
||||
// make sure ':' preceded by a whitespace or newline
|
||||
if (character === ':' && (i == 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) {
|
||||
relEmojiStart = i + 1;
|
||||
absEmojiStart = cursor - lastChunk.length + i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.emojiDropdown.hide();
|
||||
this.emojiDropdown.active = false;
|
||||
dropdown.hide();
|
||||
dropdown.active = false;
|
||||
|
||||
if (autocompleting) {
|
||||
const typed = autocompleting.typed;
|
||||
const emojiDropdown = this.emojiDropdown;
|
||||
|
||||
const applySuggestion = (replacement) => {
|
||||
this.attrs.composer.editor.replaceBeforeCursor(autocompleting.absoluteStart - 1, replacement + ' ');
|
||||
this.emojiDropdown.hide();
|
||||
};
|
||||
if (absEmojiStart) {
|
||||
typed = lastChunk.substring(relEmojiStart).toLowerCase();
|
||||
|
||||
const makeSuggestion = function ({ emoji, name, code }) {
|
||||
return (
|
||||
<Tooltip text={name}>
|
||||
<button
|
||||
key={emoji}
|
||||
onclick={() => applySuggestion(emoji)}
|
||||
onmouseenter={function () {
|
||||
emojiDropdown.setIndex($(this).parent().index() - 1);
|
||||
}}
|
||||
>
|
||||
<img alt={emoji} className="emoji" draggable="false" loading="lazy" src={`${cdn}72x72/${code}.png`} title={name} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<button
|
||||
key={emoji}
|
||||
onclick={() => applySuggestion(emoji)}
|
||||
onmouseenter={function () {
|
||||
dropdown.setIndex($(this).parent().index() - 1);
|
||||
}}
|
||||
>
|
||||
<img alt={emoji} className="emoji" draggable="false" loading="lazy" src={`${cdn}72x72/${code}.png`} />
|
||||
{name}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -91,7 +99,7 @@ export default function addComposerAutocomplete() {
|
||||
};
|
||||
const regTyped = fuzzyRegexp(typed);
|
||||
|
||||
let maxSuggestions = 40;
|
||||
let maxSuggestions = 7;
|
||||
|
||||
const findMatchingEmojis = (matcher) => {
|
||||
for (let i = 0; i < emojiKeys.length && maxSuggestions > 0; i++) {
|
||||
@@ -100,7 +108,7 @@ export default function addComposerAutocomplete() {
|
||||
if (similarEmoji.indexOf(curEmoji) === -1) {
|
||||
const names = emojiMap[curEmoji];
|
||||
for (let name of names) {
|
||||
if (matcher(name, curEmoji)) {
|
||||
if (matcher(name)) {
|
||||
--maxSuggestions;
|
||||
similarEmoji.push(curEmoji);
|
||||
break;
|
||||
@@ -111,17 +119,10 @@ export default function addComposerAutocomplete() {
|
||||
};
|
||||
|
||||
// First, try to find all emojis starting with the given string
|
||||
findMatchingEmojis((emojiName, emoji) => {
|
||||
// If no input is provided yet, match the most common emojis.
|
||||
if (!typed) {
|
||||
return this.commonEmoji?.includes(emoji);
|
||||
}
|
||||
|
||||
return emojiName.indexOf(typed) === 0;
|
||||
});
|
||||
findMatchingEmojis((emoji) => emoji.indexOf(typed) === 0);
|
||||
|
||||
// If there are still suggestions left, try for some fuzzy matches
|
||||
findMatchingEmojis((emojiName) => regTyped.test(emojiName));
|
||||
findMatchingEmojis((emoji) => regTyped.test(emoji));
|
||||
|
||||
const suggestions = similarEmoji
|
||||
.map((emoji) => ({
|
||||
@@ -132,14 +133,14 @@ export default function addComposerAutocomplete() {
|
||||
.map(makeSuggestion);
|
||||
|
||||
if (suggestions.length) {
|
||||
this.emojiDropdown.items = suggestions;
|
||||
m.render(this.$('.ComposerBody-emojiDropdownContainer')[0], this.emojiDropdown.render());
|
||||
dropdown.items = suggestions;
|
||||
m.render($container[0], dropdown.render());
|
||||
|
||||
this.emojiDropdown.show();
|
||||
const coordinates = this.attrs.composer.editor.getCaretCoordinates(autocompleting.absoluteStart);
|
||||
const width = this.emojiDropdown.$().outerWidth();
|
||||
const height = this.emojiDropdown.$().outerHeight();
|
||||
const parent = this.emojiDropdown.$().offsetParent();
|
||||
dropdown.show();
|
||||
const coordinates = this.attrs.composer.editor.getCaretCoordinates(absEmojiStart);
|
||||
const width = dropdown.$().outerWidth();
|
||||
const height = dropdown.$().outerHeight();
|
||||
const parent = dropdown.$().offsetParent();
|
||||
let left = coordinates.left;
|
||||
let top = coordinates.top + 15;
|
||||
|
||||
@@ -155,15 +156,15 @@ export default function addComposerAutocomplete() {
|
||||
top = Math.max(-(parent.offset().top - $(document).scrollTop()), top);
|
||||
left = Math.max(-parent.offset().left, left);
|
||||
|
||||
this.emojiDropdown.show(left, top);
|
||||
dropdown.show(left, top);
|
||||
}
|
||||
};
|
||||
|
||||
buildSuggestions();
|
||||
|
||||
this.emojiDropdown.setIndex(0);
|
||||
this.emojiDropdown.$().scrollTop(0);
|
||||
this.emojiDropdown.active = true;
|
||||
dropdown.setIndex(0);
|
||||
dropdown.$().scrollTop(0);
|
||||
dropdown.active = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -7,28 +7,29 @@ img.emoji {
|
||||
.EmojiDropdown {
|
||||
max-width: 500px;
|
||||
max-height: 200px;
|
||||
overflow: visible;
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
padding: 8px;
|
||||
margin: 5px 0 !important;
|
||||
|
||||
> li {
|
||||
display: inline-block;
|
||||
> li > button {
|
||||
color: var(--text-color);
|
||||
font-weight: bold;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
padding-left: 45px;
|
||||
|
||||
> button {
|
||||
color: var(--text-color);
|
||||
font-weight: bold;
|
||||
padding: 8px;
|
||||
border-radius: var(--border-radius);
|
||||
.emoji {
|
||||
float: left;
|
||||
margin-left: -30px;
|
||||
}
|
||||
}
|
||||
|
||||
> .Dropdown-header {
|
||||
display: block;
|
||||
.Dropdown-header {
|
||||
color: var(--muted-more-color);
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
padding: 4px 8px;
|
||||
margin: 0 0 4px 0;
|
||||
padding-bottom: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -7,17 +7,25 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Controller\AbstractSerializeController;
|
||||
use Flarum\Api\Controller\ListPostsController;
|
||||
use Flarum\Api\Controller\ShowDiscussionController;
|
||||
use Flarum\Api\Controller\ShowPostController;
|
||||
use Flarum\Api\Serializer\CurrentUserSerializer;
|
||||
use Flarum\Api\Serializer\ForumSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Flags\Access\ScopeFlagVisibility;
|
||||
use Flarum\Flags\AddCanFlagAttribute;
|
||||
use Flarum\Flags\AddFlagsApiAttributes;
|
||||
use Flarum\Flags\AddNewFlagCountAttribute;
|
||||
use Flarum\Flags\Api\Controller\CreateFlagController;
|
||||
use Flarum\Flags\Api\Controller\DeleteFlagsController;
|
||||
use Flarum\Flags\Api\ForumResourceFields;
|
||||
use Flarum\Flags\Api\PostResourceFields;
|
||||
use Flarum\Flags\Api\Resource\FlagResource;
|
||||
use Flarum\Flags\Api\UserResourceFields;
|
||||
use Flarum\Flags\Api\Controller\ListFlagsController;
|
||||
use Flarum\Flags\Api\Serializer\FlagSerializer;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Flags\Listener;
|
||||
use Flarum\Flags\PrepareFlagsApiData;
|
||||
use Flarum\Forum\Content\AssertRegistered;
|
||||
use Flarum\Post\Event\Deleted;
|
||||
use Flarum\Post\Post;
|
||||
@@ -33,6 +41,8 @@ return [
|
||||
->js(__DIR__.'/js/dist/admin.js'),
|
||||
|
||||
(new Extend\Routes('api'))
|
||||
->get('/flags', 'flags.index', ListFlagsController::class)
|
||||
->post('/flags', 'flags.create', CreateFlagController::class)
|
||||
->delete('/posts/{id}/flags', 'flags.delete', DeleteFlagsController::class),
|
||||
|
||||
(new Extend\Model(User::class))
|
||||
@@ -41,26 +51,27 @@ return [
|
||||
(new Extend\Model(Post::class))
|
||||
->hasMany('flags', Flag::class, 'post_id'),
|
||||
|
||||
new Extend\ApiResource(FlagResource::class),
|
||||
(new Extend\ApiSerializer(PostSerializer::class))
|
||||
->hasMany('flags', FlagSerializer::class)
|
||||
->attribute('canFlag', AddCanFlagAttribute::class),
|
||||
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class),
|
||||
(new Extend\ApiSerializer(CurrentUserSerializer::class))
|
||||
->attribute('newFlagCount', AddNewFlagCountAttribute::class),
|
||||
|
||||
(new Extend\ApiResource(Resource\UserResource::class))
|
||||
->fields(UserResourceFields::class),
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->attributes(AddFlagsApiAttributes::class),
|
||||
|
||||
(new Extend\ApiResource(Resource\ForumResource::class))
|
||||
->fields(ForumResourceFields::class),
|
||||
(new Extend\ApiController(ShowDiscussionController::class))
|
||||
->addInclude(['posts.flags', 'posts.flags.user']),
|
||||
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint) {
|
||||
return $endpoint->addDefaultInclude(['posts.flags', 'posts.flags.user']);
|
||||
}),
|
||||
(new Extend\ApiController(ListPostsController::class))
|
||||
->addInclude(['flags', 'flags.user']),
|
||||
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint) {
|
||||
return $endpoint->addDefaultInclude(['flags', 'flags.user']);
|
||||
}),
|
||||
(new Extend\ApiController(ShowPostController::class))
|
||||
->addInclude(['flags', 'flags.user']),
|
||||
|
||||
(new Extend\ApiController(AbstractSerializeController::class))
|
||||
->prepareDataForSerialization(PrepareFlagsApiData::class),
|
||||
|
||||
(new Extend\Settings())
|
||||
->serializeToForum('guidelinesUrl', 'flarum-flags.guidelines_url'),
|
||||
|
1
extensions/flags/js/dist-typings/forum/components/FlagList.d.ts
generated
vendored
1
extensions/flags/js/dist-typings/forum/components/FlagList.d.ts
generated
vendored
@@ -8,5 +8,4 @@ export interface IFlagListAttrs extends ComponentAttrs {
|
||||
export default class FlagList<CustomAttrs extends IFlagListAttrs = IFlagListAttrs> extends Component<CustomAttrs, FlagListState> {
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
|
||||
view(): JSX.Element;
|
||||
content(state: FlagListState): JSX.Element[][] | null;
|
||||
}
|
||||
|
4
extensions/flags/js/dist-typings/forum/components/FlagPostModal.d.ts
generated
vendored
4
extensions/flags/js/dist-typings/forum/components/FlagPostModal.d.ts
generated
vendored
@@ -1,5 +1,5 @@
|
||||
/// <reference types="flarum/@types/translator-icu-rich" />
|
||||
export default class FlagPostModal extends FormModal<import("flarum/common/components/FormModal").IFormModalAttrs, undefined> {
|
||||
export default class FlagPostModal extends Modal<import("flarum/common/components/Modal").IInternalModalAttrs, undefined> {
|
||||
constructor();
|
||||
oninit(vnode: any): void;
|
||||
success: boolean | undefined;
|
||||
@@ -10,6 +10,6 @@ export default class FlagPostModal extends FormModal<import("flarum/common/compo
|
||||
flagReasons(): ItemList<any>;
|
||||
onsubmit(e: any): void;
|
||||
}
|
||||
import FormModal from "flarum/common/components/FormModal";
|
||||
import Modal from "flarum/common/components/Modal";
|
||||
import Stream from "flarum/common/utils/Stream";
|
||||
import ItemList from "flarum/common/utils/ItemList";
|
||||
|
10
extensions/flags/js/dist-typings/forum/states/FlagListState.d.ts
generated
vendored
10
extensions/flags/js/dist-typings/forum/states/FlagListState.d.ts
generated
vendored
@@ -1,13 +1,15 @@
|
||||
import type ForumApplication from 'flarum/forum/ForumApplication';
|
||||
import type Flag from '../models/Flag';
|
||||
import PaginatedListState from 'flarum/common/states/PaginatedListState';
|
||||
export default class FlagListState extends PaginatedListState<Flag> {
|
||||
import type Post from 'flarum/common/models/Post';
|
||||
export default class FlagListState {
|
||||
app: ForumApplication;
|
||||
loading: boolean;
|
||||
cache: Flag[] | null;
|
||||
index: Post | false | null;
|
||||
constructor(app: ForumApplication);
|
||||
get type(): string;
|
||||
/**
|
||||
* Load flags into the application's cache if they haven't already
|
||||
* been loaded.
|
||||
*/
|
||||
load(): Promise<void>;
|
||||
load(): void;
|
||||
}
|
||||
|
2
extensions/flags/js/dist/forum.js
generated
vendored
2
extensions/flags/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/flags/js/dist/forum.js.map
generated
vendored
2
extensions/flags/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -54,7 +54,7 @@ export default function () {
|
||||
|
||||
const controls = PostControls.destructiveControls(this.attrs.post);
|
||||
|
||||
Object.keys(controls.toObject()).forEach((k) => {
|
||||
Object.keys(controls.items).forEach((k) => {
|
||||
const attrs = controls.get(k).attrs;
|
||||
|
||||
attrs.className = 'Button';
|
||||
|
@@ -8,8 +8,6 @@ import HeaderListItem from 'flarum/forum/components/HeaderListItem';
|
||||
import type Mithril from 'mithril';
|
||||
import type Post from 'flarum/common/models/Post';
|
||||
import type FlagListState from '../states/FlagListState';
|
||||
import type Flag from '../models/Flag';
|
||||
import { Page } from 'flarum/common/states/PaginatedListState';
|
||||
|
||||
export interface IFlagListAttrs extends ComponentAttrs {
|
||||
state: FlagListState;
|
||||
@@ -18,55 +16,49 @@ export interface IFlagListAttrs extends ComponentAttrs {
|
||||
export default class FlagList<CustomAttrs extends IFlagListAttrs = IFlagListAttrs> extends Component<CustomAttrs, FlagListState> {
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
this.state = this.attrs.state;
|
||||
}
|
||||
|
||||
view() {
|
||||
const state = this.attrs.state;
|
||||
const flags = this.state.cache || [];
|
||||
|
||||
return (
|
||||
<HeaderList
|
||||
className="FlagList"
|
||||
title={app.translator.trans('flarum-flags.forum.flagged_posts.title')}
|
||||
hasItems={state.hasItems()}
|
||||
loading={state.isLoading()}
|
||||
hasItems={flags.length}
|
||||
loading={this.state.loading}
|
||||
emptyText={app.translator.trans('flarum-flags.forum.flagged_posts.empty_text')}
|
||||
loadMore={() => state.hasNext() && !state.isLoadingNext() && state.loadNext()}
|
||||
>
|
||||
<ul className="HeaderListGroup-content">{this.content(state)}</ul>
|
||||
<ul className="HeaderListGroup-content">
|
||||
{!this.state.loading &&
|
||||
flags.map((flag) => {
|
||||
const post = flag.post() as Post;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<HeaderListItem
|
||||
className="Flag"
|
||||
avatar={<Avatar user={post.user() || null} />}
|
||||
icon="fas fa-flag"
|
||||
content={app.translator.trans('flarum-flags.forum.flagged_posts.item_text', {
|
||||
username: username(post.user()),
|
||||
em: <em />,
|
||||
discussion: post.discussion().title(),
|
||||
})}
|
||||
excerpt={post.contentPlain()}
|
||||
datetime={flag.createdAt()}
|
||||
href={app.route.post(post)}
|
||||
onclick={(e: MouseEvent) => {
|
||||
app.flags.index = post;
|
||||
e.redraw = false;
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</HeaderList>
|
||||
);
|
||||
}
|
||||
|
||||
content(state: FlagListState) {
|
||||
if (!state.isLoading() && state.hasItems()) {
|
||||
return state.getPages().map((page: Page<Flag>) => {
|
||||
return page.items.map((flag: Flag) => {
|
||||
const post = flag.post() as Post;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<HeaderListItem
|
||||
className="Flag"
|
||||
avatar={<Avatar user={post.user() || null} />}
|
||||
icon="fas fa-flag"
|
||||
content={app.translator.trans('flarum-flags.forum.flagged_posts.item_text', {
|
||||
username: username(post.user()),
|
||||
em: <em />,
|
||||
discussion: post.discussion().title(),
|
||||
})}
|
||||
excerpt={post.contentPlain()}
|
||||
datetime={flag.createdAt()}
|
||||
href={app.route.post(post)}
|
||||
onclick={(e: MouseEvent) => {
|
||||
e.redraw = false;
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import FormModal from 'flarum/common/components/FormModal';
|
||||
import Modal from 'flarum/common/components/Modal';
|
||||
import Form from 'flarum/common/components/Form';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
|
||||
@@ -7,7 +7,7 @@ import Stream from 'flarum/common/utils/Stream';
|
||||
import withAttr from 'flarum/common/utils/withAttr';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
|
||||
export default class FlagPostModal extends FormModal {
|
||||
export default class FlagPostModal extends Modal {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
@@ -151,6 +151,7 @@ export default class FlagPostModal extends FormModal {
|
||||
reason: this.reason() === 'other' ? null : this.reason(),
|
||||
reasonDetail: this.reasonDetail(),
|
||||
relationships: {
|
||||
user: app.session.user,
|
||||
post: this.attrs.post,
|
||||
},
|
||||
},
|
||||
|
@@ -25,7 +25,7 @@ export default class FlagsDropdown<CustomAttrs extends IFlagsDropdownAttrs = IFl
|
||||
}
|
||||
|
||||
getUnreadCount() {
|
||||
return app.forum.attribute<number>('flagCount');
|
||||
return app.flags.cache ? app.flags.cache.length : app.forum.attribute<number>('flagCount');
|
||||
}
|
||||
|
||||
getNewCount() {
|
||||
|
@@ -1,33 +1,39 @@
|
||||
import type ForumApplication from 'flarum/forum/ForumApplication';
|
||||
import type Flag from '../models/Flag';
|
||||
import PaginatedListState from 'flarum/common/states/PaginatedListState';
|
||||
import type Post from 'flarum/common/models/Post';
|
||||
|
||||
export default class FlagListState extends PaginatedListState<Flag> {
|
||||
export default class FlagListState {
|
||||
public app: ForumApplication;
|
||||
public loading = false;
|
||||
public cache: Flag[] | null = null;
|
||||
public index: Post | false | null = null;
|
||||
|
||||
constructor(app: ForumApplication) {
|
||||
super({}, 1, null);
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return 'flags';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load flags into the application's cache if they haven't already
|
||||
* been loaded.
|
||||
*/
|
||||
load(): Promise<void> {
|
||||
if (this.app.session.user?.attribute<number>('newFlagCount')) {
|
||||
this.pages = [];
|
||||
this.location = { page: 1 };
|
||||
load() {
|
||||
if (this.cache && !this.app.session.user!.attribute<number>('newFlagCount')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.pages.length > 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
return super.loadNext();
|
||||
this.app.store
|
||||
.find<Flag[]>('flags')
|
||||
.then((flags) => {
|
||||
this.app.session.user!.pushAttributes({ newFlagCount: 0 });
|
||||
this.cache = flags.sort((a, b) => b.createdAt()!.getTime() - a.createdAt()!.getTime());
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@
|
||||
namespace Flarum\Flags\Access;
|
||||
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
@@ -22,26 +23,31 @@ class ScopeFlagVisibility
|
||||
|
||||
public function __invoke(User $actor, Builder $query): void
|
||||
{
|
||||
$query
|
||||
->whereHas('post', function (Builder $query) use ($actor) {
|
||||
$query->whereVisibleTo($actor);
|
||||
})
|
||||
->where(function (Builder $query) use ($actor) {
|
||||
if ($this->extensions->isEnabled('flarum-tags')) {
|
||||
$query
|
||||
->select('flags.*')
|
||||
->whereHas('post.discussion.tags', function ($query) use ($actor) {
|
||||
$query->whereHasPermission($actor, 'discussion.viewFlags');
|
||||
});
|
||||
if ($this->extensions->isEnabled('flarum-tags')) {
|
||||
$query
|
||||
->select('flags.*')
|
||||
->leftJoin('posts', 'posts.id', '=', 'flags.post_id')
|
||||
->leftJoin('discussions', 'discussions.id', '=', 'posts.discussion_id')
|
||||
->whereNotExists(function ($query) use ($actor) {
|
||||
return $query->selectRaw('1')
|
||||
->from('discussion_tag')
|
||||
->whereNotIn('tag_id', function ($query) use ($actor) {
|
||||
Tag::query()->setQuery($query->from('tags'))->whereHasPermission($actor, 'discussion.viewFlags')->select('tags.id');
|
||||
})
|
||||
->whereColumn('discussions.id', 'discussion_id');
|
||||
});
|
||||
|
||||
if ($actor->hasPermission('discussion.viewFlags')) {
|
||||
$query->orWhereDoesntHave('post.discussion.tags');
|
||||
}
|
||||
}
|
||||
if (! $actor->hasPermission('discussion.viewFlags')) {
|
||||
$query->whereExists(function ($query) {
|
||||
return $query->selectRaw('1')
|
||||
->from('discussion_tag')
|
||||
->whereColumn('discussions.id', 'discussion_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (! $actor->hasPermission('discussion.viewFlags')) {
|
||||
$query->orWhere('flags.user_id', $actor->id);
|
||||
}
|
||||
});
|
||||
if (! $actor->hasPermission('discussion.viewFlags')) {
|
||||
$query->orWhere('flags.user_id', $actor->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
38
extensions/flags/src/AddCanFlagAttribute.php
Normal file
38
extensions/flags/src/AddCanFlagAttribute.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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\Flags;
|
||||
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\User;
|
||||
|
||||
class AddCanFlagAttribute
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(PostSerializer $serializer, Post $post): bool
|
||||
{
|
||||
return $serializer->getActor()->can('flag', $post) && $this->checkFlagOwnPostSetting($serializer->getActor(), $post);
|
||||
}
|
||||
|
||||
protected function checkFlagOwnPostSetting(User $actor, Post $post): bool
|
||||
{
|
||||
if ($actor->id === $post->user_id) {
|
||||
// If $actor is the post author, check to see if the setting is enabled
|
||||
return (bool) $this->settings->get('flarum-flags.can_flag_own');
|
||||
}
|
||||
// $actor is not the post author
|
||||
return true;
|
||||
}
|
||||
}
|
40
extensions/flags/src/AddFlagsApiAttributes.php
Executable file
40
extensions/flags/src/AddFlagsApiAttributes.php
Executable file
@@ -0,0 +1,40 @@
|
||||
<?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\Flags;
|
||||
|
||||
use Flarum\Api\Serializer\ForumSerializer;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\User;
|
||||
|
||||
class AddFlagsApiAttributes
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ForumSerializer $serializer): array
|
||||
{
|
||||
$attributes = [
|
||||
'canViewFlags' => $serializer->getActor()->hasPermissionLike('discussion.viewFlags')
|
||||
];
|
||||
|
||||
if ($attributes['canViewFlags']) {
|
||||
$attributes['flagCount'] = (int) $this->getFlagCount($serializer->getActor());
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
protected function getFlagCount(User $actor): int
|
||||
{
|
||||
return Flag::whereVisibleTo($actor)->distinct()->count('flags.post_id');
|
||||
}
|
||||
}
|
32
extensions/flags/src/AddNewFlagCountAttribute.php
Normal file
32
extensions/flags/src/AddNewFlagCountAttribute.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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\Flags;
|
||||
|
||||
use Flarum\Api\Serializer\CurrentUserSerializer;
|
||||
use Flarum\User\User;
|
||||
|
||||
class AddNewFlagCountAttribute
|
||||
{
|
||||
public function __invoke(CurrentUserSerializer $serializer, User $user): int
|
||||
{
|
||||
return $this->getNewFlagCount($user);
|
||||
}
|
||||
|
||||
protected function getNewFlagCount(User $actor): int
|
||||
{
|
||||
$query = Flag::whereVisibleTo($actor);
|
||||
|
||||
if ($time = $actor->read_flags_at) {
|
||||
$query->where('flags.created_at', '>', $time);
|
||||
}
|
||||
|
||||
return $query->distinct()->count('flags.post_id');
|
||||
}
|
||||
}
|
43
extensions/flags/src/Api/Controller/CreateFlagController.php
Normal file
43
extensions/flags/src/Api/Controller/CreateFlagController.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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\Flags\Api\Controller;
|
||||
|
||||
use Flarum\Api\Controller\AbstractCreateController;
|
||||
use Flarum\Flags\Api\Serializer\FlagSerializer;
|
||||
use Flarum\Flags\Command\CreateFlag;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class CreateFlagController extends AbstractCreateController
|
||||
{
|
||||
public ?string $serializer = FlagSerializer::class;
|
||||
|
||||
public array $include = [
|
||||
'post',
|
||||
'post.flags',
|
||||
'user'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $bus
|
||||
) {
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): Flag
|
||||
{
|
||||
return $this->bus->dispatch(
|
||||
new CreateFlag(RequestUtil::getActor($request), Arr::get($request->getParsedBody(), 'data', []))
|
||||
);
|
||||
}
|
||||
}
|
54
extensions/flags/src/Api/Controller/ListFlagsController.php
Normal file
54
extensions/flags/src/Api/Controller/ListFlagsController.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?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\Flags\Api\Controller;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Api\Controller\AbstractListController;
|
||||
use Flarum\Flags\Api\Serializer\FlagSerializer;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class ListFlagsController extends AbstractListController
|
||||
{
|
||||
public ?string $serializer = FlagSerializer::class;
|
||||
|
||||
public array $include = [
|
||||
'user',
|
||||
'post',
|
||||
'post.user',
|
||||
'post.discussion'
|
||||
];
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document): iterable
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$include = $this->extractInclude($request);
|
||||
|
||||
$actor->assertRegistered();
|
||||
|
||||
$actor->read_flags_at = Carbon::now();
|
||||
$actor->save();
|
||||
|
||||
$flags = Flag::whereVisibleTo($actor)
|
||||
->latest('flags.created_at')
|
||||
->groupBy('post_id')
|
||||
->get();
|
||||
|
||||
if (in_array('post.user', $include)) {
|
||||
$include[] = 'post.user.groups';
|
||||
}
|
||||
|
||||
$this->loadRelations($flags, $include);
|
||||
|
||||
return $flags;
|
||||
}
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
<?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\Flags\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Flags\Flag;
|
||||
|
||||
class ForumResourceFields
|
||||
{
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Boolean::make('canViewFlags')
|
||||
->get(function (object $model, Context $context) {
|
||||
return $context->getActor()->hasPermissionLike('discussion.viewFlags');
|
||||
}),
|
||||
Schema\Integer::make('flagCount')
|
||||
->visible(fn (object $model, Context $context) => $context->getActor()->hasPermissionLike('discussion.viewFlags'))
|
||||
->get(function (object $model, Context $context) {
|
||||
return Flag::whereVisibleTo($context->getActor())->distinct()->count('flags.post_id');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,42 +0,0 @@
|
||||
<?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\Flags\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
|
||||
class PostResourceFields
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Boolean::make('canFlag')
|
||||
->get(function (Post $post, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
return $actor->can('flag', $post) && (
|
||||
// $actor is not the post author
|
||||
$actor->id !== $post->user_id
|
||||
// If $actor is the post author, check to see if the setting is enabled
|
||||
|| ((bool) $this->settings->get('flarum-flags.can_flag_own'))
|
||||
);
|
||||
}),
|
||||
Schema\Relationship\ToMany::make('flags')
|
||||
->includable(),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,165 +0,0 @@
|
||||
<?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\Flags\Api\Resource;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Api\Context as FlarumContext;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource\AbstractDatabaseResource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Api\Sort\SortColumn;
|
||||
use Flarum\Flags\Event\Created;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Http\Exception\InvalidParameterException;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
|
||||
/**
|
||||
* @extends AbstractDatabaseResource<Flag>
|
||||
*/
|
||||
class FlagResource extends AbstractDatabaseResource
|
||||
{
|
||||
public function __construct(
|
||||
protected PostRepository $posts,
|
||||
protected TranslatorInterface $translator,
|
||||
protected SettingsRepositoryInterface $settings,
|
||||
) {
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'flags';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return Flag::class;
|
||||
}
|
||||
|
||||
public function query(Context $context): object
|
||||
{
|
||||
if ($context->listing(self::class)) {
|
||||
$query = Flag::query()->groupBy('post_id');
|
||||
|
||||
$this->scope($query, $context);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
return parent::query($context);
|
||||
}
|
||||
|
||||
public function scope(Builder $query, Context $context): void
|
||||
{
|
||||
$query->whereVisibleTo($context->getActor());
|
||||
}
|
||||
|
||||
public function newModel(Context $context): object
|
||||
{
|
||||
if ($context->creating(self::class)) {
|
||||
Flag::unguard();
|
||||
|
||||
return Flag::query()->firstOrNew([
|
||||
'post_id' => (int) Arr::get($context->body(), 'data.relationships.post.data.id'),
|
||||
'user_id' => $context->getActor()->id
|
||||
], [
|
||||
'type' => 'user',
|
||||
]);
|
||||
}
|
||||
|
||||
return parent::newModel($context);
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Create::make()
|
||||
->authenticated()
|
||||
->defaultInclude(['post', 'post.flags', 'user']),
|
||||
Endpoint\Index::make()
|
||||
->authenticated()
|
||||
->defaultInclude(['user', 'post', 'post.user', 'post.discussion'])
|
||||
->defaultSort('-createdAt')
|
||||
->paginate()
|
||||
->after(function (FlarumContext $context, $data) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
$actor->read_flags_at = Carbon::now();
|
||||
$actor->save();
|
||||
|
||||
return $data;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
Schema\Str::make('type'),
|
||||
Schema\Str::make('reason')
|
||||
->writableOnCreate()
|
||||
->nullable()
|
||||
->requiredOnCreateWithout(['reasonDetail'])
|
||||
->validationMessages([
|
||||
'reason.required_without' => $this->translator->trans('flarum-flags.forum.flag_post.reason_missing_message'),
|
||||
]),
|
||||
Schema\Str::make('reasonDetail')
|
||||
->writableOnCreate()
|
||||
->nullable()
|
||||
->requiredOnCreateWithout(['reason'])
|
||||
->validationMessages([
|
||||
'reasonDetail.required_without' => $this->translator->trans('flarum-flags.forum.flag_post.reason_missing_message'),
|
||||
]),
|
||||
Schema\DateTime::make('createdAt'),
|
||||
|
||||
Schema\Relationship\ToOne::make('post')
|
||||
->includable()
|
||||
->writable(fn (Flag $flag, FlarumContext $context) => $context->creating())
|
||||
->set(function (Flag $flag, Post $post, FlarumContext $context) {
|
||||
if (! ($post instanceof CommentPost)) {
|
||||
throw new InvalidParameterException;
|
||||
}
|
||||
|
||||
$actor = $context->getActor();
|
||||
|
||||
$actor->assertCan('flag', $post);
|
||||
|
||||
if ($actor->id === $post->user_id && ! $this->settings->get('flarum-flags.can_flag_own')) {
|
||||
throw new PermissionDeniedException;
|
||||
}
|
||||
|
||||
$flag->post_id = $post->id;
|
||||
}),
|
||||
Schema\Relationship\ToOne::make('user')
|
||||
->includable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function sorts(): array
|
||||
{
|
||||
return [
|
||||
SortColumn::make('createdAt'),
|
||||
];
|
||||
}
|
||||
|
||||
public function created(object $model, Context $context): ?object
|
||||
{
|
||||
$this->events->dispatch(new Created($model, $context->getActor(), $context->body()));
|
||||
|
||||
return parent::created($model, $context);
|
||||
}
|
||||
}
|
48
extensions/flags/src/Api/Serializer/FlagSerializer.php
Normal file
48
extensions/flags/src/Api/Serializer/FlagSerializer.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Flags\Api\Serializer;
|
||||
|
||||
use Flarum\Api\Serializer\AbstractSerializer;
|
||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Flags\Flag;
|
||||
use InvalidArgumentException;
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
|
||||
class FlagSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'flags';
|
||||
|
||||
protected function getDefaultAttributes(object|array $model): array
|
||||
{
|
||||
if (! ($model instanceof Flag)) {
|
||||
throw new InvalidArgumentException(
|
||||
$this::class.' can only serialize instances of '.Flag::class
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $model->type,
|
||||
'reason' => $model->reason,
|
||||
'reasonDetail' => $model->reason_detail,
|
||||
'createdAt' => $this->formatDate($model->created_at),
|
||||
];
|
||||
}
|
||||
|
||||
protected function post(Flag $flag): ?Relationship
|
||||
{
|
||||
return $this->hasOne($flag, PostSerializer::class);
|
||||
}
|
||||
|
||||
protected function user(Flag $flag): ?Relationship
|
||||
{
|
||||
return $this->hasOne($flag, BasicUserSerializer::class);
|
||||
}
|
||||
}
|
@@ -1,36 +0,0 @@
|
||||
<?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\Flags\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\User\User;
|
||||
|
||||
class UserResourceFields
|
||||
{
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Integer::make('newFlagCount')
|
||||
->visible(fn (User $user, Context $context) => $context->getActor()->id === $user->id)
|
||||
->get(function (User $user, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
$query = Flag::whereVisibleTo($actor);
|
||||
|
||||
if ($time = $actor->read_flags_at) {
|
||||
$query->where('flags.created_at', '>', $time);
|
||||
}
|
||||
|
||||
return $query->distinct()->count('flags.post_id');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@@ -7,11 +7,15 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Api\Resource\Contracts;
|
||||
namespace Flarum\Flags\Command;
|
||||
|
||||
use Tobyz\JsonApiServer\Resource\Findable as FindableContract;
|
||||
use Flarum\User\User;
|
||||
|
||||
interface Findable extends FindableContract
|
||||
class CreateFlag
|
||||
{
|
||||
//
|
||||
public function __construct(
|
||||
public User $actor,
|
||||
public array $data
|
||||
) {
|
||||
}
|
||||
}
|
79
extensions/flags/src/Command/CreateFlagHandler.php
Normal file
79
extensions/flags/src/Command/CreateFlagHandler.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?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\Flags\Command;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Flags\Event\Created;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Foundation\ValidationException;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Illuminate\Events\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Tobscure\JsonApi\Exception\InvalidParameterException;
|
||||
|
||||
class CreateFlagHandler
|
||||
{
|
||||
public function __construct(
|
||||
protected PostRepository $posts,
|
||||
protected TranslatorInterface $translator,
|
||||
protected SettingsRepositoryInterface $settings,
|
||||
protected Dispatcher $events
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(CreateFlag $command): Flag
|
||||
{
|
||||
$actor = $command->actor;
|
||||
$data = $command->data;
|
||||
|
||||
$postId = Arr::get($data, 'relationships.post.data.id');
|
||||
$post = $this->posts->findOrFail($postId, $actor);
|
||||
|
||||
if (! ($post instanceof CommentPost)) {
|
||||
throw new InvalidParameterException;
|
||||
}
|
||||
|
||||
$actor->assertCan('flag', $post);
|
||||
|
||||
if ($actor->id === $post->user_id && ! $this->settings->get('flarum-flags.can_flag_own')) {
|
||||
throw new PermissionDeniedException();
|
||||
}
|
||||
|
||||
if (Arr::get($data, 'attributes.reason') === null && Arr::get($data, 'attributes.reasonDetail') === '') {
|
||||
throw new ValidationException([
|
||||
'message' => $this->translator->trans('flarum-flags.forum.flag_post.reason_missing_message')
|
||||
]);
|
||||
}
|
||||
|
||||
Flag::unguard();
|
||||
|
||||
$flag = Flag::firstOrNew([
|
||||
'post_id' => $post->id,
|
||||
'user_id' => $actor->id
|
||||
]);
|
||||
|
||||
$flag->post_id = $post->id;
|
||||
$flag->user_id = $actor->id;
|
||||
$flag->type = 'user';
|
||||
$flag->reason = Arr::get($data, 'attributes.reason');
|
||||
$flag->reason_detail = Arr::get($data, 'attributes.reasonDetail');
|
||||
$flag->created_at = Carbon::now();
|
||||
|
||||
$flag->save();
|
||||
|
||||
$this->events->dispatch(new Created($flag, $actor, $data));
|
||||
|
||||
return $flag;
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@
|
||||
namespace Flarum\Flags\Command;
|
||||
|
||||
use Flarum\Flags\Event\Deleting;
|
||||
use Flarum\Flags\Event\FlagsWillBeDeleted;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Illuminate\Events\Dispatcher;
|
||||
@@ -30,6 +31,9 @@ class DeleteFlagsHandler
|
||||
|
||||
$actor->assertCan('viewFlags', $post->discussion);
|
||||
|
||||
// Deprecated, removed v2.0
|
||||
$this->events->dispatch(new FlagsWillBeDeleted($post, $actor, $command->data));
|
||||
|
||||
foreach ($post->flags as $flag) {
|
||||
$this->events->dispatch(new Deleting($flag, $actor, $command->data));
|
||||
}
|
||||
|
27
extensions/flags/src/Event/FlagsWillBeDeleted.php
Normal file
27
extensions/flags/src/Event/FlagsWillBeDeleted.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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\Flags\Event;
|
||||
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
|
||||
/**
|
||||
* @deprecated v2.0
|
||||
* Listen for Flarum\Flags\Event\Deleting instead
|
||||
*/
|
||||
class FlagsWillBeDeleted
|
||||
{
|
||||
public function __construct(
|
||||
public Post $post,
|
||||
public User $actor,
|
||||
public array $data = []
|
||||
) {
|
||||
}
|
||||
}
|
@@ -14,7 +14,6 @@ use Flarum\Database\AbstractModel;
|
||||
use Flarum\Database\ScopeVisibilityTrait;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
@@ -31,11 +30,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
class Flag extends AbstractModel
|
||||
{
|
||||
use ScopeVisibilityTrait;
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
protected $casts = ['created_at' => 'datetime'];
|
||||
|
||||
|
@@ -1,30 +0,0 @@
|
||||
<?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\Flags;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class FlagFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'user',
|
||||
'post_id' => Post::factory(),
|
||||
'user_id' => User::factory(),
|
||||
'reason' => $this->faker->sentence,
|
||||
'reason_detail' => $this->faker->sentence,
|
||||
'created_at' => Carbon::now(),
|
||||
];
|
||||
}
|
||||
}
|
64
extensions/flags/src/PrepareFlagsApiData.php
Executable file
64
extensions/flags/src/PrepareFlagsApiData.php
Executable file
@@ -0,0 +1,64 @@
|
||||
<?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\Flags;
|
||||
|
||||
use Flarum\Api\Controller;
|
||||
use Flarum\Flags\Api\Controller\CreateFlagController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class PrepareFlagsApiData
|
||||
{
|
||||
public function __invoke(Controller\AbstractSerializeController $controller, mixed $data, ServerRequestInterface $request): void
|
||||
{
|
||||
// For any API action that allows the 'flags' relationship to be
|
||||
// included, we need to preload this relationship onto the data (Post
|
||||
// models) so that we can selectively expose only the flags that the
|
||||
// user has permission to view.
|
||||
if ($controller instanceof Controller\ShowDiscussionController) {
|
||||
if ($data->relationLoaded('posts')) {
|
||||
$posts = $data->getRelation('posts');
|
||||
}
|
||||
}
|
||||
|
||||
if ($controller instanceof Controller\ListPostsController) {
|
||||
$posts = $data->all();
|
||||
}
|
||||
|
||||
if ($controller instanceof Controller\ShowPostController) {
|
||||
$posts = [$data];
|
||||
}
|
||||
|
||||
if ($controller instanceof CreateFlagController) {
|
||||
$posts = [$data->post];
|
||||
}
|
||||
|
||||
if (isset($posts)) {
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$postsWithPermission = [];
|
||||
|
||||
foreach ($posts as $post) {
|
||||
if (is_object($post)) {
|
||||
$post->setRelation('flags', null);
|
||||
|
||||
if ($actor->can('viewFlags', $post->discussion)) {
|
||||
$postsWithPermission[] = $post;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($postsWithPermission)) {
|
||||
(new Collection($postsWithPermission))
|
||||
->load('flags', 'flags.user');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,13 +9,9 @@
|
||||
|
||||
namespace Flarum\Flags\Tests\integration\api\flags;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListTest extends TestCase
|
||||
@@ -32,7 +28,7 @@ class ListTest extends TestCase
|
||||
$this->extension('flarum-flags');
|
||||
|
||||
$this->prepareDatabase([
|
||||
User::class => [
|
||||
'users' => [
|
||||
$this->normalUser(),
|
||||
[
|
||||
'id' => 3,
|
||||
@@ -48,22 +44,20 @@ class ListTest extends TestCase
|
||||
'group_permission' => [
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'],
|
||||
],
|
||||
Discussion::class => [
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => '', 'user_id' => 1, 'comment_count' => 1],
|
||||
],
|
||||
Post::class => [
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 4, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>', 'is_private' => true],
|
||||
],
|
||||
Flag::class => [
|
||||
'flags' => [
|
||||
['id' => 1, 'post_id' => 1, 'user_id' => 1],
|
||||
['id' => 2, 'post_id' => 1, 'user_id' => 2],
|
||||
['id' => 3, 'post_id' => 1, 'user_id' => 3],
|
||||
['id' => 4, 'post_id' => 2, 'user_id' => 2],
|
||||
['id' => 5, 'post_id' => 3, 'user_id' => 1],
|
||||
['id' => 6, 'post_id' => 4, 'user_id' => 1],
|
||||
]
|
||||
]);
|
||||
}
|
||||
@@ -71,7 +65,7 @@ class ListTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function admin_can_see_one_flag_per_visible_post()
|
||||
public function admin_can_see_one_flag_per_post()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/flags', [
|
||||
@@ -79,9 +73,9 @@ class ListTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body = $response->getBody()->getContents());
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$data = json_decode($body, true)['data'];
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
|
||||
$ids = Arr::pluck($data, 'id');
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5'], $ids);
|
||||
@@ -90,7 +84,7 @@ class ListTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function regular_user_sees_own_flags_of_visible_posts()
|
||||
public function regular_user_sees_own_flags()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/flags', [
|
||||
@@ -109,7 +103,7 @@ class ListTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function mod_can_see_one_flag_per_visible_post()
|
||||
public function mod_can_see_one_flag_per_post()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/flags', [
|
||||
|
@@ -9,14 +9,9 @@
|
||||
|
||||
namespace Flarum\Flags\Tests\integration\api\flags;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListWithTagsTest extends TestCase
|
||||
@@ -34,13 +29,13 @@ class ListWithTagsTest extends TestCase
|
||||
$this->extension('flarum-tags');
|
||||
|
||||
$this->prepareDatabase([
|
||||
Tag::class => [
|
||||
'tags' => [
|
||||
['id' => 1, 'name' => 'Unrestricted', 'slug' => '1', 'position' => 0, 'parent_id' => null],
|
||||
['id' => 2, 'name' => 'Mods can view discussions', 'slug' => '2', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
|
||||
['id' => 3, 'name' => 'Mods can view flags', 'slug' => '3', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
|
||||
['id' => 4, 'name' => 'Mods can view discussions and flags', 'slug' => '4', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
|
||||
],
|
||||
User::class => [
|
||||
'users' => [
|
||||
$this->normalUser(),
|
||||
[
|
||||
'id' => 3,
|
||||
@@ -55,12 +50,12 @@ class ListWithTagsTest extends TestCase
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag2.viewForum'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag2.viewDiscussions'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag3.discussion.viewFlags'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewForum'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewDiscussions'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.discussion.viewFlags'],
|
||||
],
|
||||
Discussion::class => [
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => 'no tags', 'user_id' => 1, 'comment_count' => 1],
|
||||
['id' => 2, 'title' => 'has tags where mods can view discussions but not flags', 'user_id' => 1, 'comment_count' => 1],
|
||||
['id' => 3, 'title' => 'has tags where mods can view flags but not discussions', 'user_id' => 1, 'comment_count' => 1],
|
||||
@@ -73,7 +68,7 @@ class ListWithTagsTest extends TestCase
|
||||
['discussion_id' => 4, 'tag_id' => 4],
|
||||
['discussion_id' => 5, 'tag_id' => 1],
|
||||
],
|
||||
Post::class => [
|
||||
'posts' => [
|
||||
// From regular ListTest
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
@@ -84,7 +79,7 @@ class ListWithTagsTest extends TestCase
|
||||
['id' => 6, 'discussion_id' => 4, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 7, 'discussion_id' => 5, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
],
|
||||
Flag::class => [
|
||||
'flags' => [
|
||||
// From regular ListTest
|
||||
['id' => 1, 'post_id' => 1, 'user_id' => 1],
|
||||
['id' => 2, 'post_id' => 1, 'user_id' => 2],
|
||||
@@ -154,7 +149,9 @@ class ListWithTagsTest extends TestCase
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
|
||||
$ids = Arr::pluck($data, 'id');
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5', '8', '9'], $ids);
|
||||
// 7 is included, even though mods can't view discussions.
|
||||
// This is because the UI doesnt allow discussions.viewFlags without viewDiscussions.
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5', '7', '8', '9'], $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,145 +0,0 @@
|
||||
<?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\Flags\Tests\integration\api\posts;
|
||||
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class IncludeFlagsVisibilityTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function setup(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-tags', 'flarum-flags');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
$this->normalUser(),
|
||||
[
|
||||
'id' => 3,
|
||||
'username' => 'mod',
|
||||
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
|
||||
'email' => 'normal2@machine.local',
|
||||
'is_email_confirmed' => 1,
|
||||
],
|
||||
[
|
||||
'id' => 4,
|
||||
'username' => 'tod',
|
||||
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
|
||||
'email' => 'tod@machine.local',
|
||||
'is_email_confirmed' => 1,
|
||||
],
|
||||
[
|
||||
'id' => 5,
|
||||
'username' => 'ted',
|
||||
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
|
||||
'email' => 'ted@machine.local',
|
||||
'is_email_confirmed' => 1,
|
||||
],
|
||||
],
|
||||
'group_user' => [
|
||||
['group_id' => 5, 'user_id' => 2],
|
||||
['group_id' => 6, 'user_id' => 3],
|
||||
],
|
||||
'groups' => [
|
||||
['id' => 5, 'name_singular' => 'group5', 'name_plural' => 'group5', 'color' => null, 'icon' => 'fas fa-crown', 'is_hidden' => false],
|
||||
['id' => 6, 'name_singular' => 'group1', 'name_plural' => 'group1', 'color' => null, 'icon' => 'fas fa-cog', 'is_hidden' => false],
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => Group::MEMBER_ID, 'permission' => 'tag1.viewForum'],
|
||||
['group_id' => 5, 'permission' => 'tag1.viewForum'],
|
||||
['group_id' => 5, 'permission' => 'discussion.viewFlags'],
|
||||
['group_id' => 6, 'permission' => 'tag1.discussion.viewFlags'],
|
||||
['group_id' => 6, 'permission' => 'tag1.viewForum'],
|
||||
],
|
||||
'tags' => [
|
||||
['id' => 1, 'name' => 'Tag 1', 'slug' => 'tag-1', 'is_primary' => false, 'position' => null, 'parent_id' => null, 'is_restricted' => true],
|
||||
['id' => 2, 'name' => 'Tag 2', 'slug' => 'tag-2', 'is_primary' => true, 'position' => 2, 'parent_id' => null, 'is_restricted' => false],
|
||||
],
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => 'Test1', 'user_id' => 1, 'comment_count' => 1],
|
||||
['id' => 2, 'title' => 'Test2', 'user_id' => 1, 'comment_count' => 1],
|
||||
],
|
||||
'discussion_tag' => [
|
||||
['discussion_id' => 1, 'tag_id' => 1],
|
||||
['discussion_id' => 2, 'tag_id' => 2],
|
||||
],
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
|
||||
['id' => 4, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 5, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
],
|
||||
'flags' => [
|
||||
['id' => 1, 'post_id' => 1, 'user_id' => 1],
|
||||
['id' => 2, 'post_id' => 1, 'user_id' => 5],
|
||||
['id' => 3, 'post_id' => 1, 'user_id' => 3],
|
||||
['id' => 4, 'post_id' => 2, 'user_id' => 5],
|
||||
['id' => 5, 'post_id' => 3, 'user_id' => 1],
|
||||
|
||||
['id' => 6, 'post_id' => 4, 'user_id' => 1],
|
||||
['id' => 7, 'post_id' => 5, 'user_id' => 5],
|
||||
['id' => 8, 'post_id' => 5, 'user_id' => 5],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider listFlagsIncludesDataProvider
|
||||
* @test
|
||||
*/
|
||||
public function user_sees_where_allowed_with_included_tags(int $actorId, array $expectedIncludes)
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/posts', [
|
||||
'authenticatedAs' => $actorId,
|
||||
])->withQueryParams([
|
||||
'include' => 'flags'
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$responseBody = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
$data = $responseBody['data'];
|
||||
|
||||
$this->assertEquals(['1', '2', '3', '4', '5'], Arr::pluck($data, 'id'));
|
||||
$this->assertEqualsCanonicalizing(
|
||||
$expectedIncludes,
|
||||
collect($responseBody['included'] ?? [])
|
||||
->filter(fn ($include) => $include['type'] === 'flags')
|
||||
->pluck('id')
|
||||
->map(strval(...))
|
||||
->all()
|
||||
);
|
||||
}
|
||||
|
||||
public function listFlagsIncludesDataProvider(): array
|
||||
{
|
||||
return [
|
||||
'admin_sees_all' => [1, [1, 2, 3, 4, 5, 6, 7, 8]],
|
||||
'user_with_general_permission_sees_where_unrestricted_tag' => [2, [6, 7, 8]],
|
||||
'user_with_tag1_permission_sees_tag1_flags' => [3, [1, 2, 3, 4, 5]],
|
||||
'normal_user_sees_none' => [4, []],
|
||||
'normal_user_sees_own' => [5, [2, 7, 4, 8]],
|
||||
];
|
||||
}
|
||||
}
|
@@ -9,20 +9,19 @@
|
||||
|
||||
namespace Flarum\Likes;
|
||||
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Controller;
|
||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Likes\Api\PostResourceFields;
|
||||
use Flarum\Likes\Api\LoadLikesRelationship;
|
||||
use Flarum\Likes\Event\PostWasLiked;
|
||||
use Flarum\Likes\Event\PostWasUnliked;
|
||||
use Flarum\Likes\Notification\PostLikedBlueprint;
|
||||
use Flarum\Likes\Query\LikedByFilter;
|
||||
use Flarum\Likes\Query\LikedFilter;
|
||||
use Flarum\Post\Event\Deleted;
|
||||
use Flarum\Post\Filter\PostSearcher;
|
||||
use Flarum\Post\Filter\PostFilterer;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\User\Search\UserSearcher;
|
||||
use Flarum\User\Filter\UserFilterer;
|
||||
use Flarum\User\User;
|
||||
|
||||
return [
|
||||
@@ -39,32 +38,49 @@ return [
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(PostLikedBlueprint::class, ['alert']),
|
||||
->type(PostLikedBlueprint::class, PostSerializer::class, ['alert']),
|
||||
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class)
|
||||
->endpoint(
|
||||
[Endpoint\Index::class, Endpoint\Show::class, Endpoint\Create::class, Endpoint\Update::class],
|
||||
function (Endpoint\Index|Endpoint\Show|Endpoint\Create|Endpoint\Update $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->addDefaultInclude(['likes']);
|
||||
}
|
||||
),
|
||||
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->addDefaultInclude(['posts.likes']);
|
||||
(new Extend\ApiSerializer(PostSerializer::class))
|
||||
->hasMany('likes', BasicUserSerializer::class)
|
||||
->attribute('canLike', function (PostSerializer $serializer, $model) {
|
||||
return (bool) $serializer->getActor()->can('like', $model);
|
||||
})
|
||||
->attribute('likesCount', function (PostSerializer $serializer, $model) {
|
||||
return $model->getAttribute('likes_count') ?: 0;
|
||||
}),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowDiscussionController::class))
|
||||
->addInclude('posts.likes')
|
||||
->loadWhere('posts.likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
|
||||
(new Extend\ApiController(Controller\ListPostsController::class))
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
(new Extend\ApiController(Controller\ShowPostController::class))
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
(new Extend\ApiController(Controller\CreatePostController::class))
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
(new Extend\ApiController(Controller\UpdatePostController::class))
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
||||
|
||||
(new Extend\Event())
|
||||
->listen(PostWasLiked::class, Listener\SendNotificationWhenPostIsLiked::class)
|
||||
->listen(PostWasUnliked::class, Listener\SendNotificationWhenPostIsUnliked::class)
|
||||
->listen(Deleted::class, function (Deleted $event) {
|
||||
$event->post->likes()->detach();
|
||||
}),
|
||||
->subscribe(Listener\SaveLikesToDatabase::class),
|
||||
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(PostSearcher::class, LikedByFilter::class)
|
||||
->addFilter(UserSearcher::class, LikedFilter::class),
|
||||
(new Extend\Filter(PostFilterer::class))
|
||||
->addFilter(LikedByFilter::class),
|
||||
|
||||
(new Extend\Filter(UserFilterer::class))
|
||||
->addFilter(LikedFilter::class),
|
||||
|
||||
(new Extend\Settings())
|
||||
->default('flarum-likes.like_own_post', true),
|
||||
|
2
extensions/likes/js/dist/forum.js
generated
vendored
2
extensions/likes/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/likes/js/dist/forum.js.map
generated
vendored
2
extensions/likes/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -2,15 +2,11 @@ import Extend from 'flarum/common/extenders';
|
||||
import Post from 'flarum/common/models/Post';
|
||||
import User from 'flarum/common/models/User';
|
||||
import LikesUserPage from './components/LikesUserPage';
|
||||
import PostLikedNotification from './components/PostLikedNotification';
|
||||
|
||||
export default [
|
||||
new Extend.Routes() //
|
||||
.add('user.likes', '/u/:username/likes', LikesUserPage),
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('postLiked', PostLikedNotification),
|
||||
|
||||
new Extend.Model(Post) //
|
||||
.hasMany<User>('likes')
|
||||
.attribute<number>('likesCount')
|
||||
|
@@ -3,11 +3,14 @@ import app from 'flarum/forum/app';
|
||||
|
||||
import addLikeAction from './addLikeAction';
|
||||
import addLikesList from './addLikesList';
|
||||
import PostLikedNotification from './components/PostLikedNotification';
|
||||
import addLikesTabToUserProfile from './addLikesTabToUserProfile';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-likes', () => {
|
||||
app.notificationComponents.postLiked = PostLikedNotification;
|
||||
|
||||
addLikeAction();
|
||||
addLikesList();
|
||||
addLikesTabToUserProfile();
|
||||
|
65
extensions/likes/src/Api/LoadLikesRelationship.php
Normal file
65
extensions/likes/src/Api/LoadLikesRelationship.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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\Likes\Api;
|
||||
|
||||
use Flarum\Api\Controller\AbstractSerializeController;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class LoadLikesRelationship
|
||||
{
|
||||
public static int $maxLikes = 4;
|
||||
|
||||
public static function mutateRelation(BelongsToMany $query, ServerRequestInterface $request): void
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$grammar = $query->getQuery()->getGrammar();
|
||||
|
||||
$query
|
||||
// So that we can tell if the current user has liked the post.
|
||||
->orderBy(new Expression($grammar->wrap('user_id').' = '.$actor->id), 'desc')
|
||||
// Limiting a relationship results is only possible because
|
||||
// the Post model uses the \Staudenmeir\EloquentEagerLimit\HasEagerLimit
|
||||
// trait.
|
||||
->limit(self::$maxLikes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called using the @see ApiController::prepareDataForSerialization extender.
|
||||
*/
|
||||
public static function countRelation(AbstractSerializeController $controller, mixed $data): array
|
||||
{
|
||||
$loadable = null;
|
||||
|
||||
if ($data instanceof Discussion) {
|
||||
// We do this because the ShowDiscussionController manipulates the posts
|
||||
// in a way that some of them are just ids.
|
||||
$loadable = $data->posts->filter(function ($post) {
|
||||
return $post instanceof Post;
|
||||
});
|
||||
} elseif ($data instanceof Collection) {
|
||||
$loadable = $data;
|
||||
} elseif ($data instanceof Post) {
|
||||
$loadable = $data->newCollection([$data]);
|
||||
}
|
||||
|
||||
if ($loadable) {
|
||||
$loadable->loadCount('likes');
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
@@ -1,65 +0,0 @@
|
||||
<?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\Likes\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Likes\Event\PostWasLiked;
|
||||
use Flarum\Likes\Event\PostWasUnliked;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
|
||||
class PostResourceFields
|
||||
{
|
||||
public static int $maxLikes = 4;
|
||||
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Boolean::make('isLiked')
|
||||
->visible(false)
|
||||
->writable(fn (Post $post, Context $context) => $context->getActor()->can('like', $post))
|
||||
->set(function (Post $post, bool $liked, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
$currentlyLiked = $post->likes()->where('user_id', $actor->id)->exists();
|
||||
|
||||
if ($liked && ! $currentlyLiked) {
|
||||
$post->likes()->attach($actor->id);
|
||||
|
||||
$post->raise(new PostWasLiked($post, $actor));
|
||||
} elseif ($currentlyLiked) {
|
||||
$post->likes()->detach($actor->id);
|
||||
|
||||
$post->raise(new PostWasUnliked($post, $actor));
|
||||
}
|
||||
}),
|
||||
|
||||
Schema\Boolean::make('canLike')
|
||||
->get(fn (Post $post, Context $context) => $context->getActor()->can('like', $post)),
|
||||
Schema\Integer::make('likesCount')
|
||||
->countRelation('likes'),
|
||||
|
||||
Schema\Relationship\ToMany::make('likes')
|
||||
->type('users')
|
||||
->includable()
|
||||
->scope(function (Builder $query, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
$grammar = $query->getQuery()->getGrammar();
|
||||
|
||||
// So that we can tell if the current user has liked the post.
|
||||
$query
|
||||
->orderBy(new Expression($grammar->wrap('user_id').' = '.$actor->id), 'desc')
|
||||
->limit(static::$maxLikes);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
55
extensions/likes/src/Listener/SaveLikesToDatabase.php
Executable file
55
extensions/likes/src/Listener/SaveLikesToDatabase.php
Executable file
@@ -0,0 +1,55 @@
|
||||
<?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\Likes\Listener;
|
||||
|
||||
use Flarum\Likes\Event\PostWasLiked;
|
||||
use Flarum\Likes\Event\PostWasUnliked;
|
||||
use Flarum\Post\Event\Deleted;
|
||||
use Flarum\Post\Event\Saving;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class SaveLikesToDatabase
|
||||
{
|
||||
public function subscribe(Dispatcher $events): void
|
||||
{
|
||||
$events->listen(Saving::class, $this->whenPostIsSaving(...));
|
||||
$events->listen(Deleted::class, $this->whenPostIsDeleted(...));
|
||||
}
|
||||
|
||||
public function whenPostIsSaving(Saving $event): void
|
||||
{
|
||||
$post = $event->post;
|
||||
$data = $event->data;
|
||||
|
||||
if ($post->exists && isset($data['attributes']['isLiked'])) {
|
||||
$actor = $event->actor;
|
||||
$liked = (bool) $data['attributes']['isLiked'];
|
||||
|
||||
$actor->assertCan('like', $post);
|
||||
|
||||
$currentlyLiked = $post->likes()->where('user_id', $actor->id)->exists();
|
||||
|
||||
if ($liked && ! $currentlyLiked) {
|
||||
$post->likes()->attach($actor->id);
|
||||
|
||||
$post->raise(new PostWasLiked($post, $actor));
|
||||
} elseif ($currentlyLiked) {
|
||||
$post->likes()->detach($actor->id);
|
||||
|
||||
$post->raise(new PostWasUnliked($post, $actor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function whenPostIsDeleted(Deleted $event): void
|
||||
{
|
||||
$event->post->likes()->detach();
|
||||
}
|
||||
}
|
@@ -9,14 +9,10 @@
|
||||
|
||||
namespace Flarum\Likes\Query;
|
||||
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Filter\ValidateFilterTrait;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class LikedByFilter implements FilterInterface
|
||||
{
|
||||
use ValidateFilterTrait;
|
||||
@@ -26,11 +22,11 @@ class LikedByFilter implements FilterInterface
|
||||
return 'likedBy';
|
||||
}
|
||||
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
{
|
||||
$likedId = $this->asInt($value);
|
||||
$likedId = $this->asInt($filterValue);
|
||||
|
||||
$state
|
||||
$filterState
|
||||
->getQuery()
|
||||
->whereIn('id', function ($query) use ($likedId, $negate) {
|
||||
$query->select('post_id')
|
||||
|
@@ -9,14 +9,10 @@
|
||||
|
||||
namespace Flarum\Likes\Query;
|
||||
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Filter\ValidateFilterTrait;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class LikedFilter implements FilterInterface
|
||||
{
|
||||
use ValidateFilterTrait;
|
||||
@@ -26,11 +22,11 @@ class LikedFilter implements FilterInterface
|
||||
return 'liked';
|
||||
}
|
||||
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
{
|
||||
$likedId = $this->asString($value);
|
||||
$likedId = $this->asString($filterValue);
|
||||
|
||||
$state
|
||||
$filterState
|
||||
->getQuery()
|
||||
->whereIn('id', function ($query) use ($likedId) {
|
||||
$query->select('user_id')
|
||||
|
@@ -10,13 +10,9 @@
|
||||
namespace Flarum\Likes\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class LikePostTest extends TestCase
|
||||
@@ -30,21 +26,21 @@ class LikePostTest extends TestCase
|
||||
$this->extension('flarum-likes');
|
||||
|
||||
$this->prepareDatabase([
|
||||
User::class => [
|
||||
'users' => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'Acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
Discussion::class => [
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 2],
|
||||
],
|
||||
Post::class => [
|
||||
'posts' => [
|
||||
['id' => 1, 'number' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>something</p></t>'],
|
||||
['id' => 3, 'number' => 2, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>something</p></t>'],
|
||||
['id' => 5, 'number' => 3, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'discussionRenamed', 'content' => '<t><p>something</p></t>'],
|
||||
['id' => 6, 'number' => 4, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>something</p></t>'],
|
||||
],
|
||||
Group::class => [
|
||||
'groups' => [
|
||||
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
['id' => 6, 'name_singular' => 'Acme1', 'name_plural' => 'Acme1', 'is_hidden' => 0]
|
||||
],
|
||||
@@ -76,7 +72,7 @@ class LikePostTest extends TestCase
|
||||
|
||||
$post = CommentPost::query()->find($postId);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertNotNull($post->likes->where('id', $authenticatedAs)->first(), $message);
|
||||
}
|
||||
|
||||
@@ -96,7 +92,7 @@ class LikePostTest extends TestCase
|
||||
|
||||
$post = CommentPost::query()->find($postId);
|
||||
|
||||
$this->assertContainsEquals($response->getStatusCode(), [401, 403], $message);
|
||||
$this->assertEquals(403, $response->getStatusCode(), $message);
|
||||
$this->assertNull($post->likes->where('id', $authenticatedAs)->first());
|
||||
}
|
||||
|
||||
|
@@ -10,13 +10,10 @@
|
||||
namespace Flarum\Likes\Tests\integration\api\discussions;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Likes\Api\PostResourceFields;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Likes\Api\LoadLikesRelationship;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListPostsTest extends TestCase
|
||||
@@ -33,13 +30,13 @@ class ListPostsTest extends TestCase
|
||||
$this->extension('flarum-likes');
|
||||
|
||||
$this->prepareDatabase([
|
||||
Discussion::class => [
|
||||
'discussions' => [
|
||||
['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 1],
|
||||
],
|
||||
Post::class => [
|
||||
'posts' => [
|
||||
['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
],
|
||||
User::class => [
|
||||
'users' => [
|
||||
$this->normalUser(),
|
||||
['id' => 102, 'username' => 'user102', 'email' => '102@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 103, 'username' => 'user103', 'email' => '103@machine.local', 'is_email_confirmed' => 1],
|
||||
@@ -135,7 +132,7 @@ class ListPostsTest extends TestCase
|
||||
$likes = $data['relationships']['likes']['data'];
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertCount(PostResourceFields::$maxLikes, $likes);
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
// Displays the correct count of likes
|
||||
$this->assertEquals(11, $data['attributes']['likesCount']);
|
||||
// Of the limited amount of likes, the actor always appears
|
||||
@@ -162,7 +159,7 @@ class ListPostsTest extends TestCase
|
||||
$likes = $data[0]['relationships']['likes']['data'];
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertCount(PostResourceFields::$maxLikes, $likes);
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
// Displays the correct count of likes
|
||||
$this->assertEquals(11, $data[0]['attributes']['likesCount']);
|
||||
// Of the limited amount of likes, the actor always appears
|
||||
@@ -173,7 +170,7 @@ class ListPostsTest extends TestCase
|
||||
* @dataProvider likesIncludeProvider
|
||||
* @test
|
||||
*/
|
||||
public function likes_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(?string $include)
|
||||
public function likes_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(string $include)
|
||||
{
|
||||
// Show discussion endpoint
|
||||
$response = $this->send(
|
||||
@@ -184,27 +181,22 @@ class ListPostsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$included = json_decode($body, true)['included'] ?? [];
|
||||
$included = json_decode($response->getBody()->getContents(), true)['included'];
|
||||
|
||||
$likes = collect($included)
|
||||
->where('type', 'posts')
|
||||
->where('id', 101)
|
||||
->first()['relationships']['likes']['data'] ?? null;
|
||||
->first()['relationships']['likes']['data'];
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertNotNull($likes, $body);
|
||||
$this->assertCount(PostResourceFields::$maxLikes, $likes);
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
// Displays the correct count of likes
|
||||
$this->assertEquals(11, collect($included)
|
||||
->where('type', 'posts')
|
||||
->where('id', 101)
|
||||
->first()['attributes']['likesCount'] ?? null, $body);
|
||||
->first()['attributes']['likesCount']);
|
||||
// Of the limited amount of likes, the actor always appears
|
||||
$this->assertEquals([2, 102, 104, 105], Arr::pluck($likes, 'id'), $body);
|
||||
$this->assertEquals([2, 102, 104, 105], Arr::pluck($likes, 'id'));
|
||||
}
|
||||
|
||||
public function likesIncludeProvider(): array
|
||||
@@ -212,7 +204,7 @@ class ListPostsTest extends TestCase
|
||||
return [
|
||||
['posts,posts.likes'],
|
||||
['posts.likes'],
|
||||
[null],
|
||||
[''],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -7,20 +7,20 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Api\Serializer\BasicDiscussionSerializer;
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\Event\Saving;
|
||||
use Flarum\Discussion\Filter\DiscussionFilterer;
|
||||
use Flarum\Discussion\Search\DiscussionSearcher;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Lock\Access;
|
||||
use Flarum\Lock\Event\DiscussionWasLocked;
|
||||
use Flarum\Lock\Event\DiscussionWasUnlocked;
|
||||
use Flarum\Lock\Filter\LockedFilter;
|
||||
use Flarum\Lock\Listener;
|
||||
use Flarum\Lock\Notification\DiscussionLockedBlueprint;
|
||||
use Flarum\Lock\Post\DiscussionLockedPost;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\Lock\Query\LockedFilterGambit;
|
||||
|
||||
return [
|
||||
(new Extend\Frontend('forum'))
|
||||
@@ -33,44 +33,33 @@ return [
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(DiscussionLockedBlueprint::class, ['alert']),
|
||||
->type(DiscussionLockedBlueprint::class, BasicDiscussionSerializer::class, ['alert']),
|
||||
|
||||
(new Extend\Model(Discussion::class))
|
||||
->cast('is_locked', 'bool'),
|
||||
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Boolean::make('isLocked')
|
||||
->writable(fn (Discussion $discussion, Context $context) => $context->getActor()->can('lock', $discussion))
|
||||
->set(function (Discussion $discussion, bool $isLocked, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
|
||||
if ($discussion->is_locked === $isLocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
$discussion->is_locked = $isLocked;
|
||||
|
||||
$discussion->raise(
|
||||
$discussion->is_locked
|
||||
? new DiscussionWasLocked($discussion, $actor)
|
||||
: new DiscussionWasUnlocked($discussion, $actor)
|
||||
);
|
||||
}),
|
||||
Schema\Boolean::make('canLock')
|
||||
->get(fn (Discussion $discussion, Context $context) => $context->getActor()->can('lock', $discussion)),
|
||||
]),
|
||||
(new Extend\ApiSerializer(DiscussionSerializer::class))
|
||||
->attribute('isLocked', function (DiscussionSerializer $serializer, Discussion $discussion) {
|
||||
return $discussion->is_locked;
|
||||
})
|
||||
->attribute('canLock', function (DiscussionSerializer $serializer, Discussion $discussion) {
|
||||
return $serializer->getActor()->can('lock', $discussion);
|
||||
}),
|
||||
|
||||
(new Extend\Post())
|
||||
->type(DiscussionLockedPost::class),
|
||||
|
||||
(new Extend\Event())
|
||||
->listen(Saving::class, Listener\SaveLockedToDatabase::class)
|
||||
->listen(DiscussionWasLocked::class, Listener\CreatePostWhenDiscussionIsLocked::class)
|
||||
->listen(DiscussionWasUnlocked::class, Listener\CreatePostWhenDiscussionIsUnlocked::class),
|
||||
|
||||
(new Extend\Policy())
|
||||
->modelPolicy(Discussion::class, Access\DiscussionPolicy::class),
|
||||
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(DiscussionSearcher::class, LockedFilter::class),
|
||||
(new Extend\Filter(DiscussionFilterer::class))
|
||||
->addFilter(LockedFilterGambit::class),
|
||||
|
||||
(new Extend\SimpleFlarumSearch(DiscussionSearcher::class))
|
||||
->addGambit(LockedFilterGambit::class),
|
||||
];
|
||||
|
2
extensions/lock/js/dist/admin.js
generated
vendored
2
extensions/lock/js/dist/admin.js
generated
vendored
@@ -1,2 +1,2 @@
|
||||
(()=>{var e={n:r=>{var o=r&&r.__esModule?()=>r.default:()=>r;return e.d(o,{a:o}),o},d:(r,o)=>{for(var a in o)e.o(o,a)&&!e.o(r,a)&&Object.defineProperty(r,a,{enumerable:!0,get:o[a]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},r={};(()=>{"use strict";e.r(r),e.d(r,{extend:()=>m});const o=flarum.reg.get("core","admin/app");var a=e.n(o);const t=flarum.reg.get("core","common/extenders");var s=e.n(t);const n=flarum.reg.get("core","common/query/IGambit"),l=flarum.reg.get("core","common/app");var i=e.n(l);class c extends n.BooleanGambit{key(){return i().translator.trans("flarum-lock.lib.gambits.discussions.locked.key",{},!0)}filterKey(){return"locked"}}flarum.reg.add("flarum-lock","common/query/discussions/LockedGambit",c);const m=[(new(s().Search)).gambit("discussions",c)];a().initializers.add("lock",(()=>{a().extensionData.for("flarum-lock").registerPermission({icon:"fas fa-lock",label:a().translator.trans("flarum-lock.admin.permissions.lock_discussions_label"),permission:"discussion.lock"},"moderate",95)}))})(),module.exports=r})();
|
||||
(()=>{var e={n:o=>{var r=o&&o.__esModule?()=>o.default:()=>o;return e.d(r,{a:r}),r},d:(o,r)=>{for(var a in r)e.o(r,a)&&!e.o(o,a)&&Object.defineProperty(o,a,{enumerable:!0,get:r[a]})},o:(e,o)=>Object.prototype.hasOwnProperty.call(e,o),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},o={};(()=>{"use strict";e.r(o);const r=flarum.reg.get("core","admin/app");var a=e.n(r);a().initializers.add("lock",(()=>{a().extensionData.for("flarum-lock").registerPermission({icon:"fas fa-lock",label:a().translator.trans("flarum-lock.admin.permissions.lock_discussions_label"),permission:"discussion.lock"},"moderate",95)}))})(),module.exports=o})();
|
||||
//# sourceMappingURL=admin.js.map
|
2
extensions/lock/js/dist/admin.js.map
generated
vendored
2
extensions/lock/js/dist/admin.js.map
generated
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"admin.js","mappings":"MACA,IAAIA,EAAsB,CCA1BA,EAAyBC,IACxB,IAAIC,EAASD,GAAUA,EAAOE,WAC7B,IAAOF,EAAiB,QACxB,IAAM,EAEP,OADAD,EAAoBI,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,CAAM,ECLdF,EAAwB,CAACM,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXP,EAAoBS,EAAEF,EAAYC,KAASR,EAAoBS,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDR,EAAwB,CAACc,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFf,EAAyBM,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,G,qDCL9D,MAAM,EAA+BC,OAAOC,IAAIV,IAAI,OAAQ,a,aCA5D,MAAM,EAA+BS,OAAOC,IAAIV,IAAI,OAAQ,oB,aCA5D,MAAM,EAA+BS,OAAOC,IAAIV,IAAI,OAAQ,wBCAtD,EAA+BS,OAAOC,IAAIV,IAAI,OAAQ,c,aCE7C,MAAMW,UAAqB,EAAAC,cACxCjB,MACE,OAAO,qBAAqB,iDAAkD,CAAC,GAAG,EACpF,CACAkB,YACE,MAAO,QACT,EAEFJ,OAAOC,IAAII,IAAI,cAAe,wCAAyCH,GCRvE,UAAgB,IAAI,aACnBI,OAAO,cAAeJ,ICDvB,qBAAqB,QAAQ,KAC3B,sBAAsB,eAAeK,mBAAmB,CACtDC,KAAM,cACNC,MAAO,qBAAqB,wDAC5BC,WAAY,mBACX,WAAY,GAAG,G","sources":["webpack://@flarum/lock/webpack/bootstrap","webpack://@flarum/lock/webpack/runtime/compat get default export","webpack://@flarum/lock/webpack/runtime/define property getters","webpack://@flarum/lock/webpack/runtime/hasOwnProperty shorthand","webpack://@flarum/lock/webpack/runtime/make namespace object","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'admin/app')\"","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'common/extenders')\"","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'common/query/IGambit')\"","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'common/app')\"","webpack://@flarum/lock/./src/common/query/discussions/LockedGambit.ts","webpack://@flarum/lock/./src/common/extend.ts","webpack://@flarum/lock/./src/admin/index.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'admin/app');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/extenders');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/query/IGambit');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/app');","import { BooleanGambit } from 'flarum/common/query/IGambit';\nimport app from 'flarum/common/app';\nexport default class LockedGambit extends BooleanGambit {\n key() {\n return app.translator.trans('flarum-lock.lib.gambits.discussions.locked.key', {}, true);\n }\n filterKey() {\n return 'locked';\n }\n}\nflarum.reg.add('flarum-lock', 'common/query/discussions/LockedGambit', LockedGambit);","import Extend from 'flarum/common/extenders';\nimport LockedGambit from './query/discussions/LockedGambit';\nexport default [new Extend.Search() //\n.gambit('discussions', LockedGambit)];","import app from 'flarum/admin/app';\nexport { default as extend } from './extend';\napp.initializers.add('lock', () => {\n app.extensionData.for('flarum-lock').registerPermission({\n icon: 'fas fa-lock',\n label: app.translator.trans('flarum-lock.admin.permissions.lock_discussions_label'),\n permission: 'discussion.lock'\n }, 'moderate', 95);\n});"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","reg","LockedGambit","BooleanGambit","filterKey","add","gambit","registerPermission","icon","label","permission"],"sourceRoot":""}
|
||||
{"version":3,"file":"admin.js","mappings":"MACA,IAAIA,EAAsB,CCA1BA,EAAyBC,IACxB,IAAIC,EAASD,GAAUA,EAAOE,WAC7B,IAAOF,EAAiB,QACxB,IAAM,EAEP,OADAD,EAAoBI,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,CAAM,ECLdF,EAAwB,CAACM,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXP,EAAoBS,EAAEF,EAAYC,KAASR,EAAoBS,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDR,EAAwB,CAACc,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFf,EAAyBM,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,G,+BCL9D,MAAM,EAA+BC,OAAOC,IAAIV,IAAI,OAAQ,a,aCC5D,qBAAqB,QAAQ,KAC3B,sBAAsB,eAAeW,mBAAmB,CACtDC,KAAM,cACNC,MAAO,qBAAqB,wDAC5BC,WAAY,mBACX,WAAY,GAAG,G","sources":["webpack://@flarum/lock/webpack/bootstrap","webpack://@flarum/lock/webpack/runtime/compat get default export","webpack://@flarum/lock/webpack/runtime/define property getters","webpack://@flarum/lock/webpack/runtime/hasOwnProperty shorthand","webpack://@flarum/lock/webpack/runtime/make namespace object","webpack://@flarum/lock/external root \"flarum.reg.get('core', 'admin/app')\"","webpack://@flarum/lock/./src/admin/index.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'admin/app');","import app from 'flarum/admin/app';\napp.initializers.add('lock', () => {\n app.extensionData.for('flarum-lock').registerPermission({\n icon: 'fas fa-lock',\n label: app.translator.trans('flarum-lock.admin.permissions.lock_discussions_label'),\n permission: 'discussion.lock'\n }, 'moderate', 95);\n});"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","reg","registerPermission","icon","label","permission"],"sourceRoot":""}
|
2
extensions/lock/js/dist/forum.js
generated
vendored
2
extensions/lock/js/dist/forum.js
generated
vendored
@@ -1,2 +1,2 @@
|
||||
(()=>{var o={n:e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},d:(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o:(o,e)=>Object.prototype.hasOwnProperty.call(o,e),r:o=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})}},e={};(()=>{"use strict";o.r(e),o.d(e,{extend:()=>N});const t=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","forum/app");var r=o.n(n);const s=flarum.reg.get("core","common/models/Discussion");var c=o.n(s);const a=flarum.reg.get("core","common/components/Badge");var i=o.n(a);const l=flarum.reg.get("core","forum/utils/DiscussionControls");var u=o.n(l);const d=flarum.reg.get("core","forum/components/DiscussionPage");var f=o.n(d);const k=flarum.reg.get("core","common/components/Button");var g=o.n(k);const p=flarum.reg.get("core","common/extenders");var b=o.n(p);const y=flarum.reg.get("core","forum/components/EventPost");var _=o.n(y);class v extends(_()){icon(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"}descriptionKey(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedPost",v);const x=flarum.reg.get("core","common/query/IGambit"),L=flarum.reg.get("core","common/app");var h=o.n(L);class P extends x.BooleanGambit{key(){return h().translator.trans("flarum-lock.lib.gambits.discussions.locked.key",{},!0)}filterKey(){return"locked"}}flarum.reg.add("flarum-lock","common/query/discussions/LockedGambit",P);const w=[(new(b().Search)).gambit("discussions",P)],S=flarum.reg.get("core","forum/components/Notification");var j=o.n(S);class D extends(j()){icon(){return"fas fa-lock"}href(){const o=this.attrs.notification;return r().route.discussion(o.subject(),o.content().postNumber)}content(){return r().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})}excerpt(){return null}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedNotification",D);const N=[...w,(new(b().PostTypes)).add("discussionLocked",v),(new(b().Notification)).add("discussionLocked",D),new(b().Model)(c()).attribute("isLocked").attribute("canLock")];r().initializers.add("flarum-lock",(()=>{(0,t.extend)(c().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(i(),{type:"locked",label:r().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,t.extend)(u(),"moderationControls",(function(o,e){e.canLock()&&o.add("lock",m(g(),{icon:"fas fa-lock",onclick:this.lockAction.bind(e)},r().translator.trans("flarum-lock.forum.discussion_controls.".concat(e.isLocked()?"unlock":"lock","_button"))))})),u().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((()=>{r().current.matches(f())&&r().current.get("stream").update(),m.redraw()}))},(0,t.extend)("flarum/forum/components/NotificationGrid","notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:r().translator.trans("flarum-lock.forum.settings.notify_discussion_locked_label")})}))}))})(),module.exports=e})();
|
||||
(()=>{var o={n:t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return o.d(e,{a:e}),e},d:(t,e)=>{for(var n in e)o.o(e,n)&&!o.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},o:(o,t)=>Object.prototype.hasOwnProperty.call(o,t),r:o=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})}},t={};(()=>{"use strict";o.r(t),o.d(t,{extend:()=>P});const e=flarum.reg.get("core","common/extend"),n=flarum.reg.get("core","forum/app");var r=o.n(n);const s=flarum.reg.get("core","forum/components/Notification");var c=o.n(s);class a extends(c()){icon(){return"fas fa-lock"}href(){const o=this.attrs.notification;return r().route.discussion(o.subject(),o.content().postNumber)}content(){return r().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})}excerpt(){return null}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedNotification",a);const i=flarum.reg.get("core","common/models/Discussion");var u=o.n(i);const l=flarum.reg.get("core","common/components/Badge");var d=o.n(l);const f=flarum.reg.get("core","forum/utils/DiscussionControls");var k=o.n(f);const g=flarum.reg.get("core","forum/components/DiscussionPage");var p=o.n(g);const b=flarum.reg.get("core","common/components/Button");var _=o.n(b);const v=flarum.reg.get("core","common/extenders");var y=o.n(v);const x=flarum.reg.get("core","forum/components/EventPost");var L=o.n(x);class h extends(L()){icon(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"}descriptionKey(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedPost",h);const P=[(new(y().PostTypes)).add("discussionLocked",h),new(y().Model)(u()).attribute("isLocked").attribute("canLock")];r().initializers.add("flarum-lock",(()=>{r().notificationComponents.discussionLocked=a,(0,e.extend)(u().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(d(),{type:"locked",label:r().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,e.extend)(k(),"moderationControls",(function(o,t){t.canLock()&&o.add("lock",m(_(),{icon:"fas fa-lock",onclick:this.lockAction.bind(t)},r().translator.trans("flarum-lock.forum.discussion_controls.".concat(t.isLocked()?"unlock":"lock","_button"))))})),k().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((()=>{r().current.matches(p())&&r().current.get("stream").update(),m.redraw()}))},(0,e.extend)("flarum/forum/components/NotificationGrid","notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:r().translator.trans("flarum-lock.forum.settings.notify_discussion_locked_label")})}))}))})(),module.exports=t})();
|
||||
//# sourceMappingURL=forum.js.map
|
2
extensions/lock/js/dist/forum.js.map
generated
vendored
2
extensions/lock/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
export { default as default } from '../common/extend';
|
@@ -1,7 +1,5 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('lock', () => {
|
||||
app.extensionData.for('flarum-lock').registerPermission(
|
||||
{
|
||||
|
@@ -1,7 +0,0 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import LockedGambit from './query/discussions/LockedGambit';
|
||||
|
||||
export default [
|
||||
new Extend.Search() //
|
||||
.gambit('discussions', LockedGambit),
|
||||
];
|
@@ -1,12 +0,0 @@
|
||||
import { BooleanGambit } from 'flarum/common/query/IGambit';
|
||||
import app from 'flarum/common/app';
|
||||
|
||||
export default class LockedGambit extends BooleanGambit {
|
||||
key(): string {
|
||||
return app.translator.trans('flarum-lock.lib.gambits.discussions.locked.key', {}, true);
|
||||
}
|
||||
|
||||
filterKey(): string {
|
||||
return 'locked';
|
||||
}
|
||||
}
|
@@ -2,18 +2,10 @@ import Extend from 'flarum/common/extenders';
|
||||
import Discussion from 'flarum/common/models/Discussion';
|
||||
import DiscussionLockedPost from './components/DiscussionLockedPost';
|
||||
|
||||
import commonExtend from '../common/extend';
|
||||
import DiscussionLockedNotification from './components/DiscussionLockedNotification';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
new Extend.PostTypes() //
|
||||
.add('discussionLocked', DiscussionLockedPost),
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('discussionLocked', DiscussionLockedNotification),
|
||||
|
||||
new Extend.Model(Discussion) //
|
||||
.attribute<boolean>('isLocked')
|
||||
.attribute<boolean>('canLock'),
|
||||
|
@@ -1,12 +1,15 @@
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import app from 'flarum/forum/app';
|
||||
|
||||
import DiscussionLockedNotification from './components/DiscussionLockedNotification';
|
||||
import addLockBadge from './addLockBadge';
|
||||
import addLockControl from './addLockControl';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-lock', () => {
|
||||
app.notificationComponents.discussionLocked = DiscussionLockedNotification;
|
||||
|
||||
addLockBadge();
|
||||
addLockControl();
|
||||
|
||||
|
@@ -35,12 +35,3 @@ flarum-lock:
|
||||
# These translations are used in the Settings page.
|
||||
settings:
|
||||
notify_discussion_locked_label: Someone locks a discussion I started
|
||||
|
||||
# Translations in this namespace are used by the forum and admin interfaces.
|
||||
lib:
|
||||
|
||||
# These translations are used by gambits. Gambit keys must be in snake_case, no spaces.
|
||||
gambits:
|
||||
discussions:
|
||||
locked:
|
||||
key: locked
|
||||
|
@@ -1,36 +0,0 @@
|
||||
<?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\Lock\Filter;
|
||||
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class LockedFilter implements FilterInterface
|
||||
{
|
||||
public function getFilterKey(): string
|
||||
{
|
||||
return 'locked';
|
||||
}
|
||||
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
$this->constrain($state->getQuery(), $negate);
|
||||
}
|
||||
|
||||
protected function constrain(Builder $query, bool $negate): void
|
||||
{
|
||||
$query->where('is_locked', ! $negate);
|
||||
}
|
||||
}
|
40
extensions/lock/src/Listener/SaveLockedToDatabase.php
Executable file
40
extensions/lock/src/Listener/SaveLockedToDatabase.php
Executable file
@@ -0,0 +1,40 @@
|
||||
<?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\Lock\Listener;
|
||||
|
||||
use Flarum\Discussion\Event\Saving;
|
||||
use Flarum\Lock\Event\DiscussionWasLocked;
|
||||
use Flarum\Lock\Event\DiscussionWasUnlocked;
|
||||
|
||||
class SaveLockedToDatabase
|
||||
{
|
||||
public function handle(Saving $event): void
|
||||
{
|
||||
if (isset($event->data['attributes']['isLocked'])) {
|
||||
$isLocked = (bool) $event->data['attributes']['isLocked'];
|
||||
$discussion = $event->discussion;
|
||||
$actor = $event->actor;
|
||||
|
||||
$actor->assertCan('lock', $discussion);
|
||||
|
||||
if ((bool) $discussion->is_locked === $isLocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
$discussion->is_locked = $isLocked;
|
||||
|
||||
$discussion->raise(
|
||||
$discussion->is_locked
|
||||
? new DiscussionWasLocked($discussion, $actor)
|
||||
: new DiscussionWasUnlocked($discussion, $actor)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
44
extensions/lock/src/Query/LockedFilterGambit.php
Normal file
44
extensions/lock/src/Query/LockedFilterGambit.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?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\Lock\Query;
|
||||
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Search\AbstractRegexGambit;
|
||||
use Flarum\Search\SearchState;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
class LockedFilterGambit extends AbstractRegexGambit implements FilterInterface
|
||||
{
|
||||
protected function getGambitPattern(): string
|
||||
{
|
||||
return 'is:locked';
|
||||
}
|
||||
|
||||
protected function conditions(SearchState $search, array $matches, bool $negate): void
|
||||
{
|
||||
$this->constrain($search->getQuery(), $negate);
|
||||
}
|
||||
|
||||
public function getFilterKey(): string
|
||||
{
|
||||
return 'locked';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
{
|
||||
$this->constrain($filterState->getQuery(), $negate);
|
||||
}
|
||||
|
||||
protected function constrain(Builder $query, bool $negate): void
|
||||
{
|
||||
$query->where('is_locked', ! $negate);
|
||||
}
|
||||
}
|
@@ -9,22 +9,24 @@
|
||||
|
||||
namespace Flarum\Mentions;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
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\Approval\Event\PostWasApproved;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Mentions\Api\PostResourceFields;
|
||||
use Flarum\Mentions\Api\LoadMentionedByRelationship;
|
||||
use Flarum\Post\Event\Deleted;
|
||||
use Flarum\Post\Event\Hidden;
|
||||
use Flarum\Post\Event\Posted;
|
||||
use Flarum\Post\Event\Restored;
|
||||
use Flarum\Post\Event\Revised;
|
||||
use Flarum\Post\Filter\PostSearcher;
|
||||
use Flarum\Post\Filter\PostFilterer;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\Tags\Api\Serializer\TagSerializer;
|
||||
use Flarum\User\User;
|
||||
|
||||
return [
|
||||
@@ -57,49 +59,50 @@ return [
|
||||
->namespace('flarum-mentions', __DIR__.'/views'),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(Notification\PostMentionedBlueprint::class, ['alert'])
|
||||
->type(Notification\UserMentionedBlueprint::class, ['alert'])
|
||||
->type(Notification\GroupMentionedBlueprint::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\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class)
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->addDefaultInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion']);
|
||||
})
|
||||
->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index {
|
||||
return $endpoint->eagerLoad(['mentionsUsers', 'mentionsPosts', 'mentionsPosts.user', 'mentionsPosts.discussion', 'mentionsGroups']);
|
||||
(new Extend\ApiSerializer(BasicPostSerializer::class))
|
||||
->hasMany('mentionedBy', BasicPostSerializer::class)
|
||||
->hasMany('mentionsPosts', BasicPostSerializer::class)
|
||||
->hasMany('mentionsUsers', BasicUserSerializer::class)
|
||||
->hasMany('mentionsGroups', GroupSerializer::class)
|
||||
->attribute('mentionedByCount', function (BasicPostSerializer $serializer, Post $post) {
|
||||
// Only if it was eager loaded.
|
||||
return $post->getAttribute('mentioned_by_count') ?? 0;
|
||||
}),
|
||||
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index {
|
||||
return $endpoint->eagerLoadWhenIncluded([
|
||||
'firstPost' => [
|
||||
'firstPost.mentionsUsers', 'firstPost.mentionsPosts',
|
||||
'firstPost.mentionsPosts.user', 'firstPost.mentionsPosts.discussion', 'firstPost.mentionsGroups',
|
||||
],
|
||||
'lastPost' => [
|
||||
'lastPost.mentionsUsers', 'lastPost.mentionsPosts',
|
||||
'lastPost.mentionsPosts.user', 'lastPost.mentionsPosts.discussion', 'lastPost.mentionsGroups',
|
||||
],
|
||||
]);
|
||||
})
|
||||
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Show {
|
||||
return $endpoint->addDefaultInclude(['posts.mentionedBy', 'posts.mentionedBy.user', 'posts.mentionedBy.discussion'])
|
||||
->eagerLoadWhenIncluded([
|
||||
'posts' => [
|
||||
'posts.mentionsUsers', 'posts.mentionsPosts', 'posts.mentionsPosts.user',
|
||||
'posts.mentionsPosts.discussion', 'posts.mentionsGroups'
|
||||
],
|
||||
]);
|
||||
}),
|
||||
(new Extend\ApiController(Controller\ShowDiscussionController::class))
|
||||
->addInclude(['posts.mentionedBy', 'posts.mentionedBy.user', 'posts.mentionedBy.discussion'])
|
||||
->load([
|
||||
'posts.mentionsUsers', 'posts.mentionsPosts', 'posts.mentionsPosts.user',
|
||||
'posts.mentionsPosts.discussion', 'posts.mentionsGroups'
|
||||
])
|
||||
->loadWhere('posts.mentionedBy', LoadMentionedByRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadMentionedByRelationship::countRelation(...)),
|
||||
|
||||
(new Extend\ApiResource(Resource\UserResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Boolean::make('canMentionGroups')
|
||||
->visible(fn (User $user, Context $context) => $context->getActor()->id === $user->id)
|
||||
->get(fn (User $user) => $user->can('mentionGroups')),
|
||||
(new Extend\ApiController(Controller\ListDiscussionsController::class))
|
||||
->load([
|
||||
'firstPost.mentionsUsers', 'firstPost.mentionsPosts',
|
||||
'firstPost.mentionsPosts.user', 'firstPost.mentionsPosts.discussion', 'firstPost.mentionsGroups',
|
||||
'lastPost.mentionsUsers', 'lastPost.mentionsPosts',
|
||||
'lastPost.mentionsPosts.user', 'lastPost.mentionsPosts.discussion', 'lastPost.mentionsGroups',
|
||||
]),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowPostController::class))
|
||||
->addInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion'])
|
||||
// We wouldn't normally need to eager load on a single model,
|
||||
// but we do so here for visibility scoping.
|
||||
->loadWhere('mentionedBy', LoadMentionedByRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadMentionedByRelationship::countRelation(...)),
|
||||
|
||||
(new Extend\ApiController(Controller\ListPostsController::class))
|
||||
->addInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion'])
|
||||
->load(['mentionsUsers', 'mentionsPosts', 'mentionsPosts.user', 'mentionsPosts.discussion', 'mentionsGroups'])
|
||||
->loadWhere('mentionedBy', LoadMentionedByRelationship::mutateRelation(...))
|
||||
->prepareDataForSerialization(LoadMentionedByRelationship::countRelation(...)),
|
||||
|
||||
(new Extend\Settings)
|
||||
->serializeToForum('allowUsernameMentionFormat', 'flarum-mentions.allow_username_format', 'boolval'),
|
||||
|
||||
@@ -111,9 +114,14 @@ return [
|
||||
->listen(Hidden::class, Listener\UpdateMentionsMetadataWhenInvisible::class)
|
||||
->listen(Deleted::class, Listener\UpdateMentionsMetadataWhenInvisible::class),
|
||||
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(PostSearcher::class, Filter\MentionedFilter::class)
|
||||
->addFilter(PostSearcher::class, Filter\MentionedPostFilter::class),
|
||||
(new Extend\Filter(PostFilterer::class))
|
||||
->addFilter(Filter\MentionedFilter::class)
|
||||
->addFilter(Filter\MentionedPostFilter::class),
|
||||
|
||||
(new Extend\ApiSerializer(CurrentUserSerializer::class))
|
||||
->attribute('canMentionGroups', function (CurrentUserSerializer $serializer, User $user): bool {
|
||||
return $user->can('mentionGroups');
|
||||
}),
|
||||
|
||||
// Tag mentions
|
||||
(new Extend\Conditional())
|
||||
@@ -122,23 +130,18 @@ return [
|
||||
->render(Formatter\FormatTagMentions::class)
|
||||
->unparse(Formatter\UnparseTagMentions::class),
|
||||
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Relationship\ToMany::make('mentionsTags')
|
||||
->type('tags'),
|
||||
(new Extend\ApiSerializer(BasicPostSerializer::class))
|
||||
->hasMany('mentionsTags', TagSerializer::class),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowDiscussionController::class))
|
||||
->load(['posts.mentionsTags']),
|
||||
|
||||
(new Extend\ApiController(Controller\ListDiscussionsController::class))
|
||||
->load([
|
||||
'firstPost.mentionsTags', 'lastPost.mentionsTags',
|
||||
]),
|
||||
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Show {
|
||||
return $endpoint->eagerLoadWhenIncluded(['posts' => ['posts.mentionsTags']]);
|
||||
})
|
||||
->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint): Endpoint\Index {
|
||||
return $endpoint->eagerLoadWhenIncluded(['firstPost' => ['firstPost.mentionsTags'], 'lastPost' => ['lastPost.mentionsTags']]);
|
||||
}),
|
||||
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint): Endpoint\Endpoint {
|
||||
return $endpoint->eagerLoad(['mentionsTags']);
|
||||
}),
|
||||
(new Extend\ApiController(Controller\ListPostsController::class))
|
||||
->load(['mentionsTags']),
|
||||
]),
|
||||
];
|
||||
|
2
extensions/mentions/js/dist/forum.js
generated
vendored
2
extensions/mentions/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/mentions/js/dist/forum.js.map
generated
vendored
2
extensions/mentions/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -2,33 +2,45 @@ import app from 'flarum/forum/app';
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import TextEditorButton from 'flarum/common/components/TextEditorButton';
|
||||
import KeyboardNavigatable from 'flarum/common/utils/KeyboardNavigatable';
|
||||
import AutocompleteReader from 'flarum/common/utils/AutocompleteReader';
|
||||
import { throttle } from 'flarum/common/utils/throttleDebounce';
|
||||
|
||||
import AutocompleteDropdown from './fragments/AutocompleteDropdown';
|
||||
import MentionableModels from './mentionables/MentionableModels';
|
||||
|
||||
export default function addComposerAutocomplete() {
|
||||
const $container = $('<div class="ComposerBody-mentionsDropdownContainer"></div>');
|
||||
const dropdown = new AutocompleteDropdown();
|
||||
|
||||
extend('flarum/common/components/TextEditor', 'onbuild', function () {
|
||||
this.mentionsDropdown = new AutocompleteDropdown();
|
||||
this.searchMentions = throttle(250, (mentionables, buildSuggestions) => mentionables.search().then(buildSuggestions));
|
||||
const $editor = this.$('.TextEditor-editor').wrap('<div class="ComposerBody-mentionsWrapper"></div>');
|
||||
|
||||
this.navigator = new KeyboardNavigatable();
|
||||
this.navigator
|
||||
.when(() => this.mentionsDropdown.active)
|
||||
.onUp(() => this.mentionsDropdown.navigate(-1))
|
||||
.onDown(() => this.mentionsDropdown.navigate(1))
|
||||
.onSelect(this.mentionsDropdown.complete.bind(this.mentionsDropdown))
|
||||
.onCancel(this.mentionsDropdown.hide.bind(this.mentionsDropdown))
|
||||
.when(() => dropdown.active)
|
||||
.onUp(() => dropdown.navigate(-1))
|
||||
.onDown(() => dropdown.navigate(1))
|
||||
.onSelect(dropdown.complete.bind(dropdown))
|
||||
.onCancel(dropdown.hide.bind(dropdown))
|
||||
.bindTo($editor);
|
||||
|
||||
$editor.after($('<div class="ComposerBody-mentionsDropdownContainer"></div>'));
|
||||
$editor.after($container);
|
||||
});
|
||||
|
||||
extend('flarum/common/components/TextEditor', 'buildEditorParams', function (params) {
|
||||
let relMentionStart;
|
||||
let absMentionStart;
|
||||
let matchTyped;
|
||||
|
||||
let mentionables = new MentionableModels({
|
||||
onmouseenter: function () {
|
||||
dropdown.setIndex($(this).parent().index());
|
||||
},
|
||||
onclick: (replacement) => {
|
||||
this.attrs.composer.editor.replaceBeforeCursor(absMentionStart - 1, replacement + ' ');
|
||||
|
||||
dropdown.hide();
|
||||
},
|
||||
});
|
||||
|
||||
const suggestionsInputListener = () => {
|
||||
const selection = this.attrs.composer.editor.getSelectionRange();
|
||||
|
||||
@@ -36,27 +48,30 @@ export default function addComposerAutocomplete() {
|
||||
|
||||
if (selection[1] - cursor > 0) return;
|
||||
|
||||
// Search backwards from the cursor for a mention triggering symbol. If we find one,
|
||||
// we will want to show the correct autocomplete dropdown!
|
||||
// Check classes implementing the IMentionableModel interface to see triggering symbols.
|
||||
const lastChunk = this.attrs.composer.editor.getLastNChars(30);
|
||||
absMentionStart = 0;
|
||||
let activeFormat = null;
|
||||
const autocompleteReader = new AutocompleteReader((character) => !!(activeFormat = app.mentionFormats.get(character)));
|
||||
const autocompleting = autocompleteReader.check(this.attrs.composer.editor.getLastNChars(30), cursor, /\S+/);
|
||||
for (let i = lastChunk.length - 1; i >= 0; i--) {
|
||||
const character = lastChunk.substr(i, 1);
|
||||
activeFormat = app.mentionFormats.get(character);
|
||||
|
||||
const mentionsDropdown = this.mentionsDropdown;
|
||||
let mentionables = new MentionableModels({
|
||||
onmouseenter: function () {
|
||||
mentionsDropdown.setIndex($(this).parent().index());
|
||||
},
|
||||
onclick: (replacement) => {
|
||||
this.attrs.composer.editor.replaceBeforeCursor(autocompleting.absoluteStart - 1, replacement + ' ');
|
||||
this.mentionsDropdown.hide();
|
||||
},
|
||||
});
|
||||
if (activeFormat && (i === 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) {
|
||||
relMentionStart = i + 1;
|
||||
absMentionStart = cursor - lastChunk.length + i + 1;
|
||||
mentionables.init(activeFormat.makeMentionables());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.mentionsDropdown.hide();
|
||||
this.mentionsDropdown.active = false;
|
||||
dropdown.hide();
|
||||
dropdown.active = false;
|
||||
|
||||
if (autocompleting) {
|
||||
mentionables.init(activeFormat.makeMentionables());
|
||||
matchTyped = activeFormat.queryFromTyped(autocompleting.typed);
|
||||
if (absMentionStart) {
|
||||
const typed = lastChunk.substring(relMentionStart).toLowerCase();
|
||||
matchTyped = activeFormat.queryFromTyped(typed);
|
||||
|
||||
if (!matchTyped) return;
|
||||
|
||||
@@ -68,14 +83,14 @@ export default function addComposerAutocomplete() {
|
||||
const suggestions = mentionables.buildSuggestions();
|
||||
|
||||
if (suggestions.length) {
|
||||
this.mentionsDropdown.items = suggestions;
|
||||
m.render(this.$('.ComposerBody-mentionsDropdownContainer')[0], this.mentionsDropdown.render());
|
||||
dropdown.items = suggestions;
|
||||
m.render($container[0], dropdown.render());
|
||||
|
||||
this.mentionsDropdown.show();
|
||||
const coordinates = this.attrs.composer.editor.getCaretCoordinates(autocompleting.absoluteStart);
|
||||
const width = this.mentionsDropdown.$().outerWidth();
|
||||
const height = this.mentionsDropdown.$().outerHeight();
|
||||
const parent = this.mentionsDropdown.$().offsetParent();
|
||||
dropdown.show();
|
||||
const coordinates = this.attrs.composer.editor.getCaretCoordinates(absMentionStart);
|
||||
const width = dropdown.$().outerWidth();
|
||||
const height = dropdown.$().outerHeight();
|
||||
const parent = dropdown.$().offsetParent();
|
||||
let left = coordinates.left;
|
||||
let top = coordinates.top + 15;
|
||||
|
||||
@@ -91,21 +106,21 @@ export default function addComposerAutocomplete() {
|
||||
top = Math.max(-(parent.offset().top - $(document).scrollTop()), top);
|
||||
left = Math.max(-parent.offset().left, left);
|
||||
|
||||
this.mentionsDropdown.show(left, top);
|
||||
dropdown.show(left, top);
|
||||
} else {
|
||||
this.mentionsDropdown.active = false;
|
||||
this.mentionsDropdown.hide();
|
||||
dropdown.active = false;
|
||||
dropdown.hide();
|
||||
}
|
||||
};
|
||||
|
||||
this.mentionsDropdown.active = true;
|
||||
dropdown.active = true;
|
||||
|
||||
buildSuggestions();
|
||||
|
||||
this.mentionsDropdown.setIndex(0);
|
||||
this.mentionsDropdown.$().scrollTop(0);
|
||||
dropdown.setIndex(0);
|
||||
dropdown.$().scrollTop(0);
|
||||
|
||||
this.searchMentions(mentionables, buildSuggestions);
|
||||
mentionables.search()?.then(buildSuggestions);
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -2,9 +2,6 @@ import Extend from 'flarum/common/extenders';
|
||||
import Post from 'flarum/common/models/Post';
|
||||
import User from 'flarum/common/models/User';
|
||||
import MentionsUserPage from './components/MentionsUserPage';
|
||||
import PostMentionedNotification from './components/PostMentionedNotification';
|
||||
import UserMentionedNotification from './components/UserMentionedNotification';
|
||||
import GroupMentionedNotification from './components/GroupMentionedNotification';
|
||||
|
||||
export default [
|
||||
new Extend.Routes() //
|
||||
@@ -14,11 +11,6 @@ export default [
|
||||
.hasMany<Post>('mentionedBy')
|
||||
.attribute<number>('mentionedByCount'),
|
||||
|
||||
new Extend.Notification() //
|
||||
.add('postMentioned', PostMentionedNotification)
|
||||
.add('userMentioned', UserMentionedNotification)
|
||||
.add('groupMentioned', GroupMentionedNotification),
|
||||
|
||||
new Extend.Model(User) //
|
||||
.attribute<boolean>('canMentionGroups'),
|
||||
];
|
||||
|
@@ -5,6 +5,7 @@ import './components/UserMentionedNotification';
|
||||
import './fragments/AutocompleteDropdown';
|
||||
import './fragments/PostQuoteButton';
|
||||
import './utils/getCleanDisplayName';
|
||||
import './utils/getMentionText';
|
||||
import './utils/reply';
|
||||
import './utils/selectedText';
|
||||
import './utils/textFormatter';
|
||||
|
@@ -9,6 +9,9 @@ import addMentionedByList from './addMentionedByList';
|
||||
import addPostReplyAction from './addPostReplyAction';
|
||||
import addPostQuoteButton from './addPostQuoteButton';
|
||||
import addComposerAutocomplete from './addComposerAutocomplete';
|
||||
import PostMentionedNotification from './components/PostMentionedNotification';
|
||||
import UserMentionedNotification from './components/UserMentionedNotification';
|
||||
import GroupMentionedNotification from './components/GroupMentionedNotification';
|
||||
import MentionFormats from './mentionables/formats/MentionFormats';
|
||||
import UserPage from 'flarum/forum/components/UserPage';
|
||||
import LinkButton from 'flarum/common/components/LinkButton';
|
||||
@@ -37,6 +40,10 @@ app.initializers.add('flarum-mentions', function () {
|
||||
// posts or users that the user could mention.
|
||||
addComposerAutocomplete();
|
||||
|
||||
app.notificationComponents.postMentioned = PostMentionedNotification;
|
||||
app.notificationComponents.userMentioned = UserMentionedNotification;
|
||||
app.notificationComponents.groupMentioned = GroupMentionedNotification;
|
||||
|
||||
// Add notification preferences.
|
||||
extend('flarum/forum/components/NotificationGrid', 'notificationTypes', function (items) {
|
||||
items.add('postMentioned', {
|
||||
|
@@ -2,6 +2,7 @@ import type MentionableModel from './MentionableModel';
|
||||
import type Model from 'flarum/common/Model';
|
||||
import type Mithril from 'mithril';
|
||||
import MentionsDropdownItem from '../components/MentionsDropdownItem';
|
||||
import { throttle } from 'flarum/common/utils/throttleDebounce';
|
||||
|
||||
export default class MentionableModels {
|
||||
protected mentionables?: MentionableModel[];
|
||||
@@ -32,7 +33,7 @@ export default class MentionableModels {
|
||||
* Don't send API calls searching for models until at least 2 characters have been typed.
|
||||
* This focuses the mention results on models already loaded.
|
||||
*/
|
||||
public readonly search = async (): Promise<void> => {
|
||||
public readonly search = throttle(250, async (): Promise<void> => {
|
||||
if (!this.typed || this.typed.length <= 1) return;
|
||||
|
||||
const typedLower = this.typed.toLowerCase();
|
||||
@@ -50,7 +51,7 @@ export default class MentionableModels {
|
||||
this.searched.push(typedLower);
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
});
|
||||
|
||||
public matches(mentionable: MentionableModel, model: Model): boolean {
|
||||
return mentionable.matches(model, this.typed?.toLowerCase() || '');
|
||||
|
21
extensions/mentions/js/src/forum/utils/getMentionText.js
Normal file
21
extensions/mentions/js/src/forum/utils/getMentionText.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import app from 'flarum/forum/app';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @deprecated Use `app.mentionables.get('user').replacement(user)` instead. Will be removed in 2.0.
|
||||
*/
|
||||
export default function getMentionText(user, postId, group) {
|
||||
if (user !== undefined && postId === undefined) {
|
||||
return app.mentionables.get('user').replacement(user);
|
||||
} else if (user !== undefined && postId !== undefined) {
|
||||
return app.mentionables.get('post').replacement(app.store.getById('posts', postId));
|
||||
} else if (group !== undefined) {
|
||||
return app.mentionables.get('group').replacement(group);
|
||||
}
|
||||
|
||||
throw 'No parameters were passed';
|
||||
}
|
@@ -16,9 +16,10 @@ return [
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
$schema->table('post_mentions_post', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->nullable()->useCurrent()->change();
|
||||
});
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE `{$prefix}post_mentions_post` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
@@ -16,9 +16,10 @@ return [
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
$schema->table('post_mentions_user', function (Blueprint $table) {
|
||||
$table->timestamp('created_at')->nullable()->useCurrent()->change();
|
||||
});
|
||||
// do this manually because dbal doesn't recognize timestamp columns
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement("ALTER TABLE `{$prefix}post_mentions_user` MODIFY created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
82
extensions/mentions/src/Api/LoadMentionedByRelationship.php
Normal file
82
extensions/mentions/src/Api/LoadMentionedByRelationship.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?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\Api;
|
||||
|
||||
use Flarum\Api\Controller\AbstractSerializeController;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Apply visibility permissions to API data's mentionedBy relationship.
|
||||
* And limit mentionedBy to 3 posts only for performance reasons.
|
||||
*/
|
||||
class LoadMentionedByRelationship
|
||||
{
|
||||
public static int $maxMentionedBy = 4;
|
||||
|
||||
public static function mutateRelation(BelongsToMany $query, ServerRequestInterface $request): void
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$query
|
||||
->with(['mentionsPosts', 'mentionsPosts.user', 'mentionsPosts.discussion', 'mentionsUsers'])
|
||||
->whereVisibleTo($actor)
|
||||
->oldest()
|
||||
// Limiting a relationship results is only possible because
|
||||
// the Post model uses the \Staudenmeir\EloquentEagerLimit\HasEagerLimit
|
||||
// trait.
|
||||
->limit(self::$maxMentionedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called using the @see ApiController::prepareDataForSerialization extender.
|
||||
*/
|
||||
public static function countRelation(AbstractSerializeController $controller, mixed $data, ServerRequestInterface $request): array
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$loadable = null;
|
||||
|
||||
if ($data instanceof Discussion) {
|
||||
// We do this because the ShowDiscussionController manipulates the posts
|
||||
// in a way that some of them are just ids.
|
||||
$loadable = $data->posts->filter(function ($post) {
|
||||
return $post instanceof Post;
|
||||
});
|
||||
|
||||
// firstPost and lastPost might have been included in the API response,
|
||||
// so we have to make sure counts are also loaded for them.
|
||||
if ($data->firstPost) {
|
||||
$loadable->push($data->firstPost);
|
||||
}
|
||||
|
||||
if ($data->lastPost) {
|
||||
$loadable->push($data->lastPost);
|
||||
}
|
||||
} elseif ($data instanceof Collection) {
|
||||
$loadable = $data;
|
||||
} elseif ($data instanceof Post) {
|
||||
$loadable = $data->newCollection([$data]);
|
||||
}
|
||||
|
||||
if ($loadable) {
|
||||
$loadable->loadCount([
|
||||
'mentionedBy' => function ($query) use ($actor) {
|
||||
return $query->whereVisibleTo($actor);
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
@@ -1,37 +0,0 @@
|
||||
<?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\Api;
|
||||
|
||||
use Flarum\Api\Schema;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class PostResourceFields
|
||||
{
|
||||
public static int $maxMentionedBy = 4;
|
||||
|
||||
public function __invoke(): array
|
||||
{
|
||||
return [
|
||||
Schema\Integer::make('mentionedByCount')
|
||||
->countRelation('mentionedBy'),
|
||||
|
||||
Schema\Relationship\ToMany::make('mentionedBy')
|
||||
->type('posts')
|
||||
->includable()
|
||||
->scope(fn (Builder $query) => $query->oldest('id')->limit(static::$maxMentionedBy)),
|
||||
Schema\Relationship\ToMany::make('mentionsPosts')
|
||||
->type('posts'),
|
||||
Schema\Relationship\ToMany::make('mentionsUsers')
|
||||
->type('users'),
|
||||
Schema\Relationship\ToMany::make('mentionsGroups')
|
||||
->type('groups'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -9,14 +9,10 @@
|
||||
|
||||
namespace Flarum\Mentions\Filter;
|
||||
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Filter\ValidateFilterTrait;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class MentionedFilter implements FilterInterface
|
||||
{
|
||||
use ValidateFilterTrait;
|
||||
@@ -26,11 +22,11 @@ class MentionedFilter implements FilterInterface
|
||||
return 'mentioned';
|
||||
}
|
||||
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
{
|
||||
$mentionedId = $this->asInt($value);
|
||||
$mentionedId = $this->asInt($filterValue);
|
||||
|
||||
$state
|
||||
$filterState
|
||||
->getQuery()
|
||||
->join('post_mentions_user', 'posts.id', '=', 'post_mentions_user.post_id')
|
||||
->where('post_mentions_user.mentions_user_id', $negate ? '!=' : '=', $mentionedId);
|
||||
|
@@ -9,13 +9,9 @@
|
||||
|
||||
namespace Flarum\Mentions\Filter;
|
||||
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class MentionedPostFilter implements FilterInterface
|
||||
{
|
||||
public function getFilterKey(): string
|
||||
@@ -23,11 +19,11 @@ class MentionedPostFilter implements FilterInterface
|
||||
return 'mentionedPost';
|
||||
}
|
||||
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
{
|
||||
$mentionedId = trim($value, '"');
|
||||
$mentionedId = trim($filterValue, '"');
|
||||
|
||||
$state
|
||||
$filterState
|
||||
->getQuery()
|
||||
->join('post_mentions_post', 'posts.id', '=', 'post_mentions_post.post_id')
|
||||
->where('post_mentions_post.mentions_post_id', $negate ? '!=' : '=', $mentionedId);
|
||||
|
@@ -12,7 +12,6 @@ namespace Flarum\Mentions\Formatter;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\SlugManager;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Post\Post;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use s9e\TextFormatter\Renderer;
|
||||
use s9e\TextFormatter\Utils;
|
||||
@@ -25,22 +24,12 @@ class FormatPostMentions
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure rendering for post mentions.
|
||||
*
|
||||
* @param \s9e\TextFormatter\Renderer $renderer
|
||||
* @param mixed $context
|
||||
* @param string $xml
|
||||
* @param \Psr\Http\Message\ServerRequestInterface|null $request
|
||||
* @return string $xml to be rendered
|
||||
*/
|
||||
public function __invoke(Renderer $renderer, $context, $xml, Request $request = null)
|
||||
public function __invoke(Renderer $renderer, mixed $context, ?string $xml, Request $request = null): string
|
||||
{
|
||||
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($context) {
|
||||
$post = (($context && isset($context->getRelations()['mentionsPosts'])) || $context instanceof Post)
|
||||
? $context->mentionsPosts->find($attributes['id'])
|
||||
: Post::find($attributes['id']);
|
||||
$post = $context;
|
||||
|
||||
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($post) {
|
||||
$post = $post->mentionsPosts->find($attributes['id']);
|
||||
if ($post && $post->user) {
|
||||
$attributes['displayname'] = $post->user->display_name;
|
||||
}
|
||||
|
@@ -10,7 +10,6 @@
|
||||
namespace Flarum\Mentions\Formatter;
|
||||
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Post\Post;
|
||||
use s9e\TextFormatter\Utils;
|
||||
|
||||
class UnparsePostMentions
|
||||
@@ -32,11 +31,10 @@ class UnparsePostMentions
|
||||
*/
|
||||
protected function updatePostMentionTags(mixed $context, string $xml): string
|
||||
{
|
||||
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($context) {
|
||||
$post = (($context && isset($context->getRelations()['mentionsPosts'])) || $context instanceof Post)
|
||||
? $context->mentionsPosts->find($attributes['id'])
|
||||
: Post::find($attributes['id']);
|
||||
$post = $context;
|
||||
|
||||
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($post) {
|
||||
$post = $post->mentionsPosts->find($attributes['id']);
|
||||
if ($post && $post->user) {
|
||||
$attributes['displayname'] = $post->user->display_name;
|
||||
}
|
||||
|
@@ -10,10 +10,8 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
@@ -32,14 +30,14 @@ class GroupMentionsTest extends TestCase
|
||||
$this->extension('flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
User::class => [
|
||||
'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],
|
||||
],
|
||||
Discussion::class => [
|
||||
'discussions' => [
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
||||
],
|
||||
Post::class => [
|
||||
'posts' => [
|
||||
['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>'],
|
||||
@@ -55,7 +53,7 @@ class GroupMentionsTest extends TestCase
|
||||
['group_id' => Group::MEMBER_ID, 'permission' => 'postWithoutThrottle'],
|
||||
['group_id' => 9, 'permission' => 'mentionGroups'],
|
||||
],
|
||||
Group::class => [
|
||||
'groups' => [
|
||||
['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]
|
||||
@@ -93,12 +91,11 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"InvalidGroup"#g99',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
]
|
||||
],
|
||||
],
|
||||
@@ -169,12 +166,11 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Mods"#g4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -202,12 +198,11 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Admins"#g1 @"Mods"#g4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -237,12 +232,11 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Members"#g3 @"Guests"#g2',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -294,12 +288,11 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Mods"#g4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -326,12 +319,11 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 4,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Mods"#g4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -358,12 +350,11 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 4,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Ninjas"#g10',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -390,7 +381,6 @@ class GroupMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => 'New content with @"Mods"#g4 mention',
|
||||
],
|
||||
|
@@ -10,12 +10,9 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api\discussions;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Mentions\Api\PostResourceFields;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Mentions\Api\LoadMentionedByRelationship;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListPostsTest extends TestCase
|
||||
@@ -32,10 +29,10 @@ class ListPostsTest extends TestCase
|
||||
$this->extension('flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
Discussion::class => [
|
||||
'discussions' => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
|
||||
],
|
||||
Post::class => [
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 2, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 3, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
@@ -46,7 +43,7 @@ class ListPostsTest extends TestCase
|
||||
['post_id' => 3, 'mentions_user_id' => 1],
|
||||
['post_id' => 4, 'mentions_user_id' => 2]
|
||||
],
|
||||
User::class => [
|
||||
'users' => [
|
||||
$this->normalUser(),
|
||||
]
|
||||
]);
|
||||
@@ -115,10 +112,10 @@ class ListPostsTest extends TestCase
|
||||
protected function prepareMentionedByData(): void
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
Discussion::class => [
|
||||
'discussions' => [
|
||||
['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 12],
|
||||
],
|
||||
Post::class => [
|
||||
'posts' => [
|
||||
['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 102, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 103, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>', 'is_private' => 1],
|
||||
@@ -170,7 +167,7 @@ class ListPostsTest extends TestCase
|
||||
$mentionedBy = $data['relationships']['mentionedBy']['data'];
|
||||
|
||||
// Only displays a limited amount of mentioned by posts
|
||||
$this->assertCount(PostResourceFields::$maxMentionedBy, $mentionedBy);
|
||||
$this->assertCount(LoadMentionedByRelationship::$maxMentionedBy, $mentionedBy);
|
||||
// Of the limited amount of mentioned by posts, they must be visible to the actor
|
||||
$this->assertEquals([102, 104, 105, 106], Arr::pluck($mentionedBy, 'id'));
|
||||
}
|
||||
@@ -190,14 +187,14 @@ class ListPostsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? [];
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$mentionedBy = $data[0]['relationships']['mentionedBy']['data'];
|
||||
|
||||
// Only displays a limited amount of mentioned by posts
|
||||
$this->assertCount(PostResourceFields::$maxMentionedBy, $mentionedBy);
|
||||
$this->assertCount(LoadMentionedByRelationship::$maxMentionedBy, $mentionedBy);
|
||||
// Of the limited amount of mentioned by posts, they must be visible to the actor
|
||||
$this->assertEquals([102, 104, 105, 106], Arr::pluck($mentionedBy, 'id'));
|
||||
}
|
||||
@@ -206,7 +203,7 @@ class ListPostsTest extends TestCase
|
||||
* @dataProvider mentionedByIncludeProvider
|
||||
* @test
|
||||
*/
|
||||
public function mentioned_by_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(?string $include)
|
||||
public function mentioned_by_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(string $include)
|
||||
{
|
||||
$this->prepareMentionedByData();
|
||||
|
||||
@@ -219,18 +216,15 @@ class ListPostsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$included = json_decode($body = $response->getBody()->getContents(), true)['included'] ?? [];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
$included = json_decode($response->getBody()->getContents(), true)['included'];
|
||||
|
||||
$mentionedBy = collect($included)
|
||||
->where('type', 'posts')
|
||||
->where('id', 101)
|
||||
->first()['relationships']['mentionedBy']['data'] ?? null;
|
||||
->first()['relationships']['mentionedBy']['data'];
|
||||
|
||||
$this->assertNotNull($mentionedBy, 'Mentioned by relation not included');
|
||||
// Only displays a limited amount of mentioned by posts
|
||||
$this->assertCount(PostResourceFields::$maxMentionedBy, $mentionedBy);
|
||||
$this->assertCount(LoadMentionedByRelationship::$maxMentionedBy, $mentionedBy);
|
||||
// Of the limited amount of mentioned by posts, they must be visible to the actor
|
||||
$this->assertEquals([102, 104, 105, 106], Arr::pluck($mentionedBy, 'id'));
|
||||
}
|
||||
@@ -240,7 +234,7 @@ class ListPostsTest extends TestCase
|
||||
return [
|
||||
['posts,posts.mentionedBy'],
|
||||
['posts.mentionedBy'],
|
||||
[null],
|
||||
[''],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -256,54 +250,10 @@ class ListPostsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? [];
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$this->assertEquals(0, $data['attributes']['mentionedByCount']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function mentioned_by_count_works_on_show_endpoint()
|
||||
{
|
||||
$this->prepareMentionedByData();
|
||||
|
||||
// List posts endpoint
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/posts/101', [
|
||||
'authenticatedAs' => 1,
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? [];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$this->assertEquals(10, $data['attributes']['mentionedByCount']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function mentioned_by_count_works_on_list_endpoint()
|
||||
{
|
||||
$this->prepareMentionedByData();
|
||||
|
||||
// List posts endpoint
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/posts', [
|
||||
'authenticatedAs' => 1,
|
||||
])->withQueryParams([
|
||||
'filter' => ['discussion' => 100],
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? [];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$post101 = collect($data)->where('id', 101)->first();
|
||||
$post112 = collect($data)->where('id', 112)->first();
|
||||
|
||||
$this->assertEquals(10, $post101['attributes']['mentionedByCount']);
|
||||
$this->assertEquals(0, $post112['attributes']['mentionedByCount']);
|
||||
}
|
||||
}
|
||||
|
@@ -10,11 +10,8 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Formatter\Formatter;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\DisplayName\DriverInterface;
|
||||
@@ -34,16 +31,16 @@ class PostMentionsTest extends TestCase
|
||||
$this->extension('flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
User::class => [
|
||||
'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],
|
||||
],
|
||||
Discussion::class => [
|
||||
'discussions' => [
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
||||
['id' => 50, 'title' => __CLASS__, 'is_private' => true, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 1],
|
||||
],
|
||||
Post::class => [
|
||||
'posts' => [
|
||||
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="TobyFlarum___" id="5" number="2" discussionid="2" username="toby">@tobyuuu#5</POSTMENTION></r>'],
|
||||
['id' => 5, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="potato" id="4" number="3" discussionid="2" username="potato">@potato#4</POSTMENTION></r>'],
|
||||
['id' => 6, 'number' => 4, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="i_am_a_deleted_user" id="7" number="5" discussionid="2" username="i_am_a_deleted_user">@"i_am_a_deleted_user"#p7</POSTMENTION></r>'],
|
||||
@@ -83,12 +80,11 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@potato#4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -115,12 +111,11 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"POTATO$"#p4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -147,12 +142,11 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"potato"#p50',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -179,12 +173,11 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@“POTATO$”#p4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -211,12 +204,11 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"franzofflarum"#p215',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -243,12 +235,11 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"TOBY$"#p5 @"flarum"#2015 @"franzofflarum"#220 @"POTATO$"#3 @"POTATO$"#p4',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -391,12 +382,11 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad "#p6 User"#p9',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -444,12 +434,11 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#p9',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -476,7 +465,6 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#p9',
|
||||
],
|
||||
@@ -505,7 +493,6 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#p9',
|
||||
],
|
||||
@@ -534,7 +521,6 @@ class PostMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"acme"#p11',
|
||||
],
|
||||
@@ -552,40 +538,6 @@ class PostMentionsTest extends TestCase
|
||||
$this->assertStringContainsString('PostMention', $response['data']['attributes']['contentHtml']);
|
||||
$this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsPosts->find(11));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function rendering_post_mention_with_a_post_context_works()
|
||||
{
|
||||
/** @var Formatter $formatter */
|
||||
$formatter = $this->app()->getContainer()->make(Formatter::class);
|
||||
|
||||
$post = Post::find(4);
|
||||
$user = User::find(1);
|
||||
|
||||
$xml = $formatter->parse($post->content, $post, $user);
|
||||
$renderedHtml = $formatter->render($xml, $post);
|
||||
|
||||
$this->assertStringContainsString('TOBY$', $renderedHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function rendering_post_mention_without_a_context_works()
|
||||
{
|
||||
/** @var Formatter $formatter */
|
||||
$formatter = $this->app()->getContainer()->make(Formatter::class);
|
||||
|
||||
$post = Post::find(4);
|
||||
$user = User::find(1);
|
||||
|
||||
$xml = $formatter->parse($post->content, null, $user);
|
||||
$renderedHtml = $formatter->render($xml);
|
||||
|
||||
$this->assertStringContainsString('TOBY$', $renderedHtml);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomOtherDisplayNameDriver implements DriverInterface
|
||||
|
@@ -10,14 +10,10 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class TagMentionsTest extends TestCase
|
||||
{
|
||||
@@ -30,20 +26,20 @@ class TagMentionsTest extends TestCase
|
||||
$this->extension('flarum-tags', 'flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
User::class => [
|
||||
'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],
|
||||
],
|
||||
Discussion::class => [
|
||||
'discussions' => [
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
||||
],
|
||||
Post::class => [
|
||||
'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>'],
|
||||
],
|
||||
Tag::class => [
|
||||
'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],
|
||||
@@ -72,12 +68,11 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#flarum',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -101,12 +96,11 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#戦い',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -131,12 +125,11 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#franzofflarum',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -162,12 +155,11 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#test',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -191,12 +183,11 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#dev',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -220,12 +211,11 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#dev',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -249,12 +239,11 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#test #flarum #support #laravel #franzofflarum',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -376,7 +365,6 @@ class TagMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '#laravel',
|
||||
],
|
||||
|
@@ -10,10 +10,8 @@
|
||||
namespace Flarum\Mentions\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\DisplayName\DriverInterface;
|
||||
@@ -33,16 +31,16 @@ class UserMentionsTest extends TestCase
|
||||
$this->extension('flarum-mentions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
User::class => [
|
||||
'users' => [
|
||||
$this->normalUser(),
|
||||
['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],
|
||||
],
|
||||
Discussion::class => [
|
||||
'discussions' => [
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
||||
],
|
||||
Post::class => [
|
||||
'posts' => [
|
||||
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="TobyFlarum___" id="4" username="toby">@tobyuuu</USERMENTION></r>'],
|
||||
['id' => 6, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="i_am_a_deleted_user" id="2021" username="i_am_a_deleted_user">@"i_am_a_deleted_user"#2021</USERMENTION></r>'],
|
||||
['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 5, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="Bad "#p6 User" id="5">@"Bad "#p6 User"#5</USERMENTION></r>'],
|
||||
@@ -74,12 +72,11 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@potato',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -108,12 +105,11 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@potato',
|
||||
],
|
||||
'relationships' => [
|
||||
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
|
||||
'discussion' => ['data' => ['id' => 2]],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -140,7 +136,6 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"POTATO$"#3',
|
||||
],
|
||||
@@ -172,7 +167,6 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@“POTATO$”#3',
|
||||
],
|
||||
@@ -204,7 +198,6 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"franzofflarum"#82',
|
||||
],
|
||||
@@ -236,7 +229,6 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"TOBY$"#4 @"POTATO$"#p4 @"franzofflarum"#82 @"POTATO$"#3',
|
||||
],
|
||||
@@ -290,7 +282,6 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"potato_"#3',
|
||||
],
|
||||
@@ -321,7 +312,6 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"potato_"#3',
|
||||
],
|
||||
@@ -377,7 +367,6 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad "#p6 User"#5',
|
||||
],
|
||||
@@ -430,7 +419,6 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#5',
|
||||
],
|
||||
@@ -462,7 +450,6 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#5',
|
||||
],
|
||||
@@ -491,7 +478,6 @@ class UserMentionsTest extends TestCase
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'posts',
|
||||
'attributes' => [
|
||||
'content' => '@"Bad _ User"#5',
|
||||
],
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user