mirror of
https://github.com/flarum/core.git
synced 2025-08-13 03:44:32 +02:00
Compare commits
96 Commits
sm/laravel
...
sm/jas-no-
Author | SHA1 | Date | |
---|---|---|---|
|
96b8b92d42 | ||
|
a03104d61d | ||
|
7504e31399 | ||
|
aa39d0c11b | ||
|
d9e5ab4f11 | ||
|
ac27cd03dd | ||
|
a442aad3be | ||
|
51e2ab8502 | ||
|
a8777c6198 | ||
|
10514709f1 | ||
|
eb6e599df1 | ||
|
5ce1aeab47 | ||
|
389d004ddc | ||
|
72f89c0209 | ||
|
1e7eddb61e | ||
|
1302378141 | ||
|
29ede5aa27 | ||
|
d273b1920f | ||
|
b02f8190ea | ||
|
e0025df3e7 | ||
|
b8e17182e9 | ||
|
2b917372a7 | ||
|
270188b5b0 | ||
|
9149ecc7aa | ||
|
5fc2bb5eb6 | ||
|
24f3a6829f | ||
|
bf523b2325 | ||
|
e771b908d5 | ||
|
721e2eae3d | ||
|
3fbe05fd18 | ||
|
8f29b7af82 | ||
|
734f4a150c | ||
|
0186ca909e | ||
|
1aa7806244 | ||
|
e3350543af | ||
|
d400dcbc2f | ||
|
430709bf5b | ||
|
f784f48906 | ||
|
3a34136e36 | ||
|
fb1703cd9b | ||
|
b58fec7ead | ||
|
537f97a07a | ||
|
c1be00e79a | ||
|
91b89bc698 | ||
|
278617a10d | ||
|
f793e5b8f8 | ||
|
01598555a9 | ||
|
5399c86a1b | ||
|
74ce4cf1a7 | ||
|
b4a82e81fc | ||
|
471ce0ea2a | ||
|
723cb73d48 | ||
|
e4abf93763 | ||
|
9310ce13d8 | ||
|
d4c532c949 | ||
|
46357ee9a9 | ||
|
6cbdfb6aa1 | ||
|
4b126d9f4c | ||
|
9e04b010d8 | ||
|
1c0e0933b0 | ||
|
60ffa78531 | ||
|
294878291c | ||
|
05a7e24836 | ||
|
eaabeab8c9 | ||
|
9ef366493c | ||
|
208b94dc12 | ||
|
5e3f8db095 | ||
|
deb99f0de4 | ||
|
693bce912a | ||
|
3107319812 | ||
|
a9756cb5eb | ||
|
bbdf3b5aba | ||
|
e2281a2123 | ||
|
d01c0e5210 | ||
|
bee50bec73 | ||
|
679d32729e | ||
|
e978e29e00 | ||
|
5cdfa0f640 | ||
|
a3192d2934 | ||
|
96ba2f5f2d | ||
|
87a83d33b3 | ||
|
015529ff1e | ||
|
2950290ad1 | ||
|
d154388468 | ||
|
7c885c72fd | ||
|
577fc3e6a8 | ||
|
e4e0fbff73 | ||
|
94de8b42b4 | ||
|
db0d9cb006 | ||
|
5ab5257ff5 | ||
|
24d13e33bb | ||
|
412cfafb3a | ||
|
ec5cb98c77 | ||
|
23fdddf185 | ||
|
be9eb16d7d | ||
|
ee34217b15 |
@@ -23,3 +23,6 @@ indent_size = 2
|
||||
|
||||
[*.neon]
|
||||
indent_style = tab
|
||||
|
||||
[{install,update}.php]
|
||||
indent_size = 2
|
||||
|
61
.github/workflows/REUSABLE_backend.yml
vendored
61
.github/workflows/REUSABLE_backend.yml
vendored
@@ -31,7 +31,8 @@ on:
|
||||
description: Versions of PHP to test with. Should be array of strings encoded as JSON array
|
||||
type: string
|
||||
required: false
|
||||
default: '["8.1", "8.2"]'
|
||||
# Keep PHP versions synced with build-install-packages.yml
|
||||
default: '["8.1", "8.2", "8.3"]'
|
||||
|
||||
php_extensions:
|
||||
description: PHP extensions to install.
|
||||
@@ -43,7 +44,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", "mariadb"]'
|
||||
default: '["mysql:5.7", "mysql:8.0.30", "mysql:8.1.0", "mariadb", "sqlite:3"]'
|
||||
|
||||
php_ini_values:
|
||||
description: PHP ini values
|
||||
@@ -51,14 +52,26 @@ 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: ubuntu-latest
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -72,25 +85,49 @@ 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)
|
||||
|
||||
@@ -98,10 +135,22 @@ 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 }}
|
||||
image: ${{ matrix.service != 'sqlite:3' && matrix.service || '' }}
|
||||
ports:
|
||||
- 13306:3306
|
||||
|
||||
@@ -124,6 +173,7 @@ 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
|
||||
@@ -153,10 +203,11 @@ jobs:
|
||||
DB_PORT: 13306
|
||||
DB_PASSWORD: root
|
||||
DB_PREFIX: ${{ matrix.prefix }}
|
||||
DB_DRIVER: ${{ matrix.driver }}
|
||||
COMPOSER_PROCESS_TIMEOUT: 600
|
||||
|
||||
phpstan:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
12
.github/workflows/REUSABLE_frontend.yml
vendored
12
.github/workflows/REUSABLE_frontend.yml
vendored
@@ -86,20 +86,30 @@ 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: ubuntu-latest
|
||||
runs-on: ${{ inputs.runner_type }}
|
||||
|
||||
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
Normal file
29
.github/workflows/build-install-packages.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
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,3 +38,4 @@ 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\\PackageManager\\": "extensions/package-manager/src",
|
||||
"Flarum\\ExtensionManager\\": "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\\PackageManager\\Tests\\": "extensions/package-manager/tests",
|
||||
"Flarum\\ExtensionManager\\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/package-manager": "self.version",
|
||||
"flarum/extension-manager": "self.version",
|
||||
"flarum/pusher": "self.version",
|
||||
"flarum/statistics": "self.version",
|
||||
"flarum/sticky": "self.version",
|
||||
@@ -108,10 +108,11 @@
|
||||
"php": "^8.1",
|
||||
"ext-json": "*",
|
||||
"components/font-awesome": "^5.15.0",
|
||||
"composer/composer": "^2.0",
|
||||
"composer/composer": "^2.7",
|
||||
"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",
|
||||
@@ -130,7 +131,7 @@
|
||||
"illuminate/support": "^10.0",
|
||||
"illuminate/validation": "^10.0",
|
||||
"illuminate/view": "^10.0",
|
||||
"intervention/image": "^2.7.2",
|
||||
"intervention/image": "^3.2",
|
||||
"jenssegers/agent": "^2.6",
|
||||
"laminas/laminas-diactoros": "^3.0",
|
||||
"laminas/laminas-httphandlerrunner": "^2.6",
|
||||
@@ -150,7 +151,6 @@
|
||||
"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,13 +162,14 @@
|
||||
"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",
|
||||
"nunomaduro/larastan": "^2.6",
|
||||
"larastan/larastan": "^2.7",
|
||||
"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,9 +7,10 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Api\Serializer\BasicDiscussionSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Approval\Access;
|
||||
use Flarum\Approval\Api\PostResourceFields;
|
||||
use Flarum\Approval\Event\PostWasApproved;
|
||||
use Flarum\Approval\Listener;
|
||||
use Flarum\Discussion\Discussion;
|
||||
@@ -36,17 +37,13 @@ return [
|
||||
->default('is_approved', true)
|
||||
->cast('is_approved', 'bool'),
|
||||
|
||||
(new Extend\ApiSerializer(BasicDiscussionSerializer::class))
|
||||
->attribute('isApproved', function (BasicDiscussionSerializer $serializer, Discussion $discussion): bool {
|
||||
return $discussion->is_approved;
|
||||
}),
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Boolean::make('isApproved'),
|
||||
]),
|
||||
|
||||
(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\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class),
|
||||
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
|
29
extensions/approval/src/Api/PostResourceFields.php
Normal file
29
extensions/approval/src/Api/PostResourceFields.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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,6 +11,7 @@ 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
|
||||
@@ -20,23 +21,42 @@ 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 (! empty($attributes['isHidden']) && $event->actor->can('approve', $post)) {
|
||||
} elseif (isset($attributes['isHidden']) && $event->actor->can('approve', $post)) {
|
||||
$isApproved = true;
|
||||
$approvingSilently = $attributes['isHidden'];
|
||||
}
|
||||
|
||||
if (! empty($isApproved)) {
|
||||
$post->is_approved = true;
|
||||
|
||||
$post->raise(new PostWasApproved($post, $event->actor));
|
||||
if (! $approvingSilently) {
|
||||
$post->raise(new PostWasApproved($post, $event->actor));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,19 +10,23 @@
|
||||
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([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['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' => [
|
||||
Discussion::class => [
|
||||
['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],
|
||||
@@ -31,7 +35,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],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['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],
|
||||
@@ -45,7 +49,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],
|
||||
],
|
||||
'groups' => [
|
||||
Group::class => [
|
||||
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0]
|
||||
],
|
||||
'group_user' => [
|
||||
|
123
extensions/approval/tests/integration/api/ApprovePostsTest.php
Normal file
123
extensions/approval/tests/integration/api/ApprovePostsTest.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
153
extensions/approval/tests/integration/api/CreatePostsTest.php
Normal file
153
extensions/approval/tests/integration/api/CreatePostsTest.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?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 @shadow-color);
|
||||
box-shadow: 0 2px 6px var(--shadow-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
margin: 0;
|
||||
|
||||
&, a {
|
||||
color: @muted-color;
|
||||
color: var(--muted-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"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(() => dropdown.active)
|
||||
.onUp(() => dropdown.navigate(-1))
|
||||
.onDown(() => dropdown.navigate(1))
|
||||
.onSelect(dropdown.complete.bind(dropdown))
|
||||
.onCancel(dropdown.hide.bind(dropdown))
|
||||
.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))
|
||||
.bindTo($editor);
|
||||
|
||||
$editor.after($container);
|
||||
$editor.after($('<div class="ComposerBody-emojiDropdownContainer"></div>'));
|
||||
});
|
||||
|
||||
extend('flarum/common/components/TextEditor', 'buildEditorParams', function (params) {
|
||||
const emojiKeys = Object.keys(emojiMap);
|
||||
|
||||
let relEmojiStart;
|
||||
let absEmojiStart;
|
||||
let typed;
|
||||
|
||||
const applySuggestion = (replacement) => {
|
||||
this.attrs.composer.editor.replaceBeforeCursor(absEmojiStart - 1, replacement + ' ');
|
||||
|
||||
dropdown.hide();
|
||||
};
|
||||
const autocompleteReader = new AutocompleteReader(':');
|
||||
|
||||
params.inputListeners.push(() => {
|
||||
const selection = this.attrs.composer.editor.getSelectionRange();
|
||||
@@ -50,42 +50,34 @@ 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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
const autocompleting = autocompleteReader.check(lastChunk, cursor, /[a-z0-9]|\+|\-|_|\:/);
|
||||
|
||||
dropdown.hide();
|
||||
dropdown.active = false;
|
||||
this.emojiDropdown.hide();
|
||||
this.emojiDropdown.active = false;
|
||||
|
||||
if (absEmojiStart) {
|
||||
typed = lastChunk.substring(relEmojiStart).toLowerCase();
|
||||
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();
|
||||
};
|
||||
|
||||
const makeSuggestion = function ({ emoji, name, code }) {
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -99,7 +91,7 @@ export default function addComposerAutocomplete() {
|
||||
};
|
||||
const regTyped = fuzzyRegexp(typed);
|
||||
|
||||
let maxSuggestions = 7;
|
||||
let maxSuggestions = 40;
|
||||
|
||||
const findMatchingEmojis = (matcher) => {
|
||||
for (let i = 0; i < emojiKeys.length && maxSuggestions > 0; i++) {
|
||||
@@ -108,7 +100,7 @@ export default function addComposerAutocomplete() {
|
||||
if (similarEmoji.indexOf(curEmoji) === -1) {
|
||||
const names = emojiMap[curEmoji];
|
||||
for (let name of names) {
|
||||
if (matcher(name)) {
|
||||
if (matcher(name, curEmoji)) {
|
||||
--maxSuggestions;
|
||||
similarEmoji.push(curEmoji);
|
||||
break;
|
||||
@@ -119,10 +111,17 @@ export default function addComposerAutocomplete() {
|
||||
};
|
||||
|
||||
// First, try to find all emojis starting with the given string
|
||||
findMatchingEmojis((emoji) => emoji.indexOf(typed) === 0);
|
||||
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;
|
||||
});
|
||||
|
||||
// If there are still suggestions left, try for some fuzzy matches
|
||||
findMatchingEmojis((emoji) => regTyped.test(emoji));
|
||||
findMatchingEmojis((emojiName) => regTyped.test(emojiName));
|
||||
|
||||
const suggestions = similarEmoji
|
||||
.map((emoji) => ({
|
||||
@@ -133,14 +132,14 @@ export default function addComposerAutocomplete() {
|
||||
.map(makeSuggestion);
|
||||
|
||||
if (suggestions.length) {
|
||||
dropdown.items = suggestions;
|
||||
m.render($container[0], dropdown.render());
|
||||
this.emojiDropdown.items = suggestions;
|
||||
m.render(this.$('.ComposerBody-emojiDropdownContainer')[0], this.emojiDropdown.render());
|
||||
|
||||
dropdown.show();
|
||||
const coordinates = this.attrs.composer.editor.getCaretCoordinates(absEmojiStart);
|
||||
const width = dropdown.$().outerWidth();
|
||||
const height = dropdown.$().outerHeight();
|
||||
const parent = dropdown.$().offsetParent();
|
||||
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();
|
||||
let left = coordinates.left;
|
||||
let top = coordinates.top + 15;
|
||||
|
||||
@@ -156,15 +155,15 @@ export default function addComposerAutocomplete() {
|
||||
top = Math.max(-(parent.offset().top - $(document).scrollTop()), top);
|
||||
left = Math.max(-parent.offset().left, left);
|
||||
|
||||
dropdown.show(left, top);
|
||||
this.emojiDropdown.show(left, top);
|
||||
}
|
||||
};
|
||||
|
||||
buildSuggestions();
|
||||
|
||||
dropdown.setIndex(0);
|
||||
dropdown.$().scrollTop(0);
|
||||
dropdown.active = true;
|
||||
this.emojiDropdown.setIndex(0);
|
||||
this.emojiDropdown.$().scrollTop(0);
|
||||
this.emojiDropdown.active = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -7,29 +7,28 @@ img.emoji {
|
||||
.EmojiDropdown {
|
||||
max-width: 500px;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
margin: 5px 0 !important;
|
||||
padding: 8px;
|
||||
|
||||
> li > button {
|
||||
color: @text-color;
|
||||
font-weight: bold;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
padding-left: 45px;
|
||||
> li {
|
||||
display: inline-block;
|
||||
|
||||
.emoji {
|
||||
float: left;
|
||||
margin-left: -30px;
|
||||
> button {
|
||||
color: var(--text-color);
|
||||
font-weight: bold;
|
||||
padding: 8px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
.Dropdown-header {
|
||||
color: @muted-more-color;
|
||||
> .Dropdown-header {
|
||||
display: block;
|
||||
color: var(--muted-more-color);
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
padding-bottom: 5px;
|
||||
font-size: 11px;
|
||||
padding: 4px 8px;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -7,25 +7,17 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
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\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
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\Controller\ListFlagsController;
|
||||
use Flarum\Flags\Api\Serializer\FlagSerializer;
|
||||
use Flarum\Flags\Api\ForumResourceFields;
|
||||
use Flarum\Flags\Api\PostResourceFields;
|
||||
use Flarum\Flags\Api\Resource\FlagResource;
|
||||
use Flarum\Flags\Api\UserResourceFields;
|
||||
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;
|
||||
@@ -41,8 +33,6 @@ 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))
|
||||
@@ -51,27 +41,26 @@ return [
|
||||
(new Extend\Model(Post::class))
|
||||
->hasMany('flags', Flag::class, 'post_id'),
|
||||
|
||||
(new Extend\ApiSerializer(PostSerializer::class))
|
||||
->hasMany('flags', FlagSerializer::class)
|
||||
->attribute('canFlag', AddCanFlagAttribute::class),
|
||||
new Extend\ApiResource(FlagResource::class),
|
||||
|
||||
(new Extend\ApiSerializer(CurrentUserSerializer::class))
|
||||
->attribute('newFlagCount', AddNewFlagCountAttribute::class),
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(PostResourceFields::class),
|
||||
|
||||
(new Extend\ApiSerializer(ForumSerializer::class))
|
||||
->attributes(AddFlagsApiAttributes::class),
|
||||
(new Extend\ApiResource(Resource\UserResource::class))
|
||||
->fields(UserResourceFields::class),
|
||||
|
||||
(new Extend\ApiController(ShowDiscussionController::class))
|
||||
->addInclude(['posts.flags', 'posts.flags.user']),
|
||||
(new Extend\ApiResource(Resource\ForumResource::class))
|
||||
->fields(ForumResourceFields::class),
|
||||
|
||||
(new Extend\ApiController(ListPostsController::class))
|
||||
->addInclude(['flags', '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(ShowPostController::class))
|
||||
->addInclude(['flags', 'flags.user']),
|
||||
|
||||
(new Extend\ApiController(AbstractSerializeController::class))
|
||||
->prepareDataForSerialization(PrepareFlagsApiData::class),
|
||||
(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\Settings())
|
||||
->serializeToForum('guidelinesUrl', 'flarum-flags.guidelines_url'),
|
||||
|
17
extensions/flags/js/dist-typings/forum/components/FlagList.d.ts
generated
vendored
17
extensions/flags/js/dist-typings/forum/components/FlagList.d.ts
generated
vendored
@@ -1,7 +1,12 @@
|
||||
export default class FlagList extends Component<import("flarum/common/Component").ComponentAttrs, undefined> {
|
||||
constructor();
|
||||
oninit(vnode: any): void;
|
||||
state: any;
|
||||
view(): JSX.Element;
|
||||
import Component from 'flarum/common/Component';
|
||||
import type { ComponentAttrs } from 'flarum/common/Component';
|
||||
import type Mithril from 'mithril';
|
||||
import type FlagListState from '../states/FlagListState';
|
||||
export interface IFlagListAttrs extends ComponentAttrs {
|
||||
state: FlagListState;
|
||||
}
|
||||
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;
|
||||
}
|
||||
import Component from "flarum/common/Component";
|
||||
|
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 Modal<import("flarum/common/components/Modal").IInternalModalAttrs, undefined> {
|
||||
export default class FlagPostModal extends FormModal<import("flarum/common/components/FormModal").IFormModalAttrs, undefined> {
|
||||
constructor();
|
||||
oninit(vnode: any): void;
|
||||
success: boolean | undefined;
|
||||
@@ -10,6 +10,6 @@ export default class FlagPostModal extends Modal<import("flarum/common/component
|
||||
flagReasons(): ItemList<any>;
|
||||
onsubmit(e: any): void;
|
||||
}
|
||||
import Modal from "flarum/common/components/Modal";
|
||||
import FormModal from "flarum/common/components/FormModal";
|
||||
import Stream from "flarum/common/utils/Stream";
|
||||
import ItemList from "flarum/common/utils/ItemList";
|
||||
|
17
extensions/flags/js/dist-typings/forum/components/FlagsDropdown.d.ts
generated
vendored
17
extensions/flags/js/dist-typings/forum/components/FlagsDropdown.d.ts
generated
vendored
@@ -1,7 +1,12 @@
|
||||
export default class FlagsDropdown extends NotificationsDropdown<import("flarum/common/components/Dropdown").IDropdownAttrs> {
|
||||
static initAttrs(attrs: any): void;
|
||||
constructor();
|
||||
getUnreadCount(): any;
|
||||
getNewCount(): unknown;
|
||||
/// <reference types="mithril" />
|
||||
import HeaderDropdown from 'flarum/forum/components/HeaderDropdown';
|
||||
import type { IHeaderDropdownAttrs } from 'flarum/forum/components/HeaderDropdown';
|
||||
export interface IFlagsDropdownAttrs extends IHeaderDropdownAttrs {
|
||||
}
|
||||
export default class FlagsDropdown<CustomAttrs extends IFlagsDropdownAttrs = IFlagsDropdownAttrs> extends HeaderDropdown<CustomAttrs> {
|
||||
static initAttrs(attrs: IFlagsDropdownAttrs): void;
|
||||
getContent(): JSX.Element;
|
||||
goToRoute(): void;
|
||||
getUnreadCount(): number;
|
||||
getNewCount(): number;
|
||||
}
|
||||
import NotificationsDropdown from "flarum/forum/components/NotificationsDropdown";
|
||||
|
19
extensions/flags/js/dist-typings/forum/states/FlagListState.d.ts
generated
vendored
19
extensions/flags/js/dist-typings/forum/states/FlagListState.d.ts
generated
vendored
@@ -1,16 +1,13 @@
|
||||
export default class FlagListState {
|
||||
constructor(app: any);
|
||||
app: any;
|
||||
/**
|
||||
* Whether or not the flags are loading.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
loading: boolean;
|
||||
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> {
|
||||
app: ForumApplication;
|
||||
constructor(app: ForumApplication);
|
||||
get type(): string;
|
||||
/**
|
||||
* Load flags into the application's cache if they haven't already
|
||||
* been loaded.
|
||||
*/
|
||||
load(): void;
|
||||
cache: any;
|
||||
load(): Promise<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
@@ -1,4 +1,4 @@
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import { extend, override } from 'flarum/common/extend';
|
||||
import app from 'flarum/forum/app';
|
||||
import Post from 'flarum/forum/components/Post';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
@@ -54,7 +54,7 @@ export default function () {
|
||||
|
||||
const controls = PostControls.destructiveControls(this.attrs.post);
|
||||
|
||||
Object.keys(controls.items).forEach((k) => {
|
||||
Object.keys(controls.toObject()).forEach((k) => {
|
||||
const attrs = controls.get(k).attrs;
|
||||
|
||||
attrs.className = 'Button';
|
||||
@@ -75,7 +75,7 @@ export default function () {
|
||||
return items;
|
||||
};
|
||||
|
||||
extend(Post.prototype, 'content', function (vdom) {
|
||||
override(Post.prototype, 'header', function (vdom) {
|
||||
const post = this.attrs.post;
|
||||
const flags = post.flags();
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function () {
|
||||
|
||||
if (post.isHidden()) this.revealContent = true;
|
||||
|
||||
vdom.unshift(
|
||||
return (
|
||||
<div className="Post-flagged">
|
||||
<div className="Post-flagged-flags">
|
||||
{flags.map((flag) => (
|
||||
|
@@ -1,65 +0,0 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import Component from 'flarum/common/Component';
|
||||
import Link from 'flarum/common/components/Link';
|
||||
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
||||
import avatar from 'flarum/common/helpers/avatar';
|
||||
import username from 'flarum/common/helpers/username';
|
||||
import icon from 'flarum/common/helpers/icon';
|
||||
import humanTime from 'flarum/common/helpers/humanTime';
|
||||
|
||||
export default class FlagList extends Component {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
this.state = this.attrs.state;
|
||||
}
|
||||
|
||||
view() {
|
||||
const flags = this.state.cache || [];
|
||||
|
||||
return (
|
||||
<div className="NotificationList FlagList">
|
||||
<div className="NotificationList-header">
|
||||
<h4 className="App-titleControl App-titleControl--text">{app.translator.trans('flarum-flags.forum.flagged_posts.title')}</h4>
|
||||
</div>
|
||||
<div className="NotificationList-content">
|
||||
<ul className="NotificationGroup-content">
|
||||
{flags.length ? (
|
||||
flags.map((flag) => {
|
||||
const post = flag.post();
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
href={app.route.post(post)}
|
||||
className="Notification Flag"
|
||||
onclick={(e) => {
|
||||
app.flags.index = post;
|
||||
e.redraw = false;
|
||||
}}
|
||||
>
|
||||
{avatar(post.user())}
|
||||
{icon('fas fa-flag', { className: 'Notification-icon' })}
|
||||
<span className="Notification-content">
|
||||
{app.translator.trans('flarum-flags.forum.flagged_posts.item_text', {
|
||||
username: username(post.user()),
|
||||
em: <em />,
|
||||
discussion: post.discussion().title(),
|
||||
})}
|
||||
</span>
|
||||
{humanTime(flag.createdAt())}
|
||||
<div className="Notification-excerpt">{post.contentPlain()}</div>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
) : !this.state.loading ? (
|
||||
<div className="NotificationList-empty">{app.translator.trans('flarum-flags.forum.flagged_posts.empty_text')}</div>
|
||||
) : (
|
||||
<LoadingIndicator className="LoadingIndicator--block" />
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
72
extensions/flags/js/src/forum/components/FlagList.tsx
Normal file
72
extensions/flags/js/src/forum/components/FlagList.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import Component from 'flarum/common/Component';
|
||||
import type { ComponentAttrs } from 'flarum/common/Component';
|
||||
import Avatar from 'flarum/common/components/Avatar';
|
||||
import username from 'flarum/common/helpers/username';
|
||||
import HeaderList from 'flarum/forum/components/HeaderList';
|
||||
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;
|
||||
}
|
||||
|
||||
export default class FlagList<CustomAttrs extends IFlagListAttrs = IFlagListAttrs> extends Component<CustomAttrs, FlagListState> {
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
}
|
||||
|
||||
view() {
|
||||
const state = this.attrs.state;
|
||||
|
||||
return (
|
||||
<HeaderList
|
||||
className="FlagList"
|
||||
title={app.translator.trans('flarum-flags.forum.flagged_posts.title')}
|
||||
hasItems={state.hasItems()}
|
||||
loading={state.isLoading()}
|
||||
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>
|
||||
</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,12 +1,13 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import Modal from 'flarum/common/components/Modal';
|
||||
import FormModal from 'flarum/common/components/FormModal';
|
||||
import Form from 'flarum/common/components/Form';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
|
||||
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 Modal {
|
||||
export default class FlagPostModal extends FormModal {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
@@ -28,31 +29,31 @@ export default class FlagPostModal extends Modal {
|
||||
if (this.success) {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<Form className="Form--centered">
|
||||
<p className="helpText">{app.translator.trans('flarum-flags.forum.flag_post.confirmation_message')}</p>
|
||||
<div className="Form-group">
|
||||
<div className="Form-group Form-controls">
|
||||
<Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}>
|
||||
{app.translator.trans('flarum-flags.forum.flag_post.dismiss_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<Form className="Form--centered">
|
||||
<div className="Form-group">
|
||||
<div>{this.flagReasons().toArray()}</div>
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
<div className="Form-group Form-controls">
|
||||
<Button className="Button Button--primary Button--block" type="submit" loading={this.loading} disabled={!this.reason()}>
|
||||
{app.translator.trans('flarum-flags.forum.flag_post.submit_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -150,7 +151,6 @@ export default class FlagPostModal extends Modal {
|
||||
reason: this.reason() === 'other' ? null : this.reason(),
|
||||
reasonDetail: this.reasonDetail(),
|
||||
relationships: {
|
||||
user: app.session.user,
|
||||
post: this.attrs.post,
|
||||
},
|
||||
},
|
||||
|
@@ -1,33 +0,0 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import NotificationsDropdown from 'flarum/forum/components/NotificationsDropdown';
|
||||
|
||||
import FlagList from './FlagList';
|
||||
|
||||
export default class FlagsDropdown extends NotificationsDropdown {
|
||||
static initAttrs(attrs) {
|
||||
attrs.label = attrs.label || app.translator.trans('flarum-flags.forum.flagged_posts.tooltip');
|
||||
attrs.icon = attrs.icon || 'fas fa-flag';
|
||||
|
||||
super.initAttrs(attrs);
|
||||
}
|
||||
|
||||
getMenu() {
|
||||
return (
|
||||
<div className={'Dropdown-menu ' + this.attrs.menuClassName} onclick={this.menuClick.bind(this)}>
|
||||
{this.showing && <FlagList state={this.attrs.state} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
goToRoute() {
|
||||
m.route.set(app.route('flags'));
|
||||
}
|
||||
|
||||
getUnreadCount() {
|
||||
return app.flags.cache ? app.flags.cache.length : app.forum.attribute('flagCount');
|
||||
}
|
||||
|
||||
getNewCount() {
|
||||
return app.session.user.attribute('newFlagCount');
|
||||
}
|
||||
}
|
34
extensions/flags/js/src/forum/components/FlagsDropdown.tsx
Normal file
34
extensions/flags/js/src/forum/components/FlagsDropdown.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import HeaderDropdown from 'flarum/forum/components/HeaderDropdown';
|
||||
import type { IHeaderDropdownAttrs } from 'flarum/forum/components/HeaderDropdown';
|
||||
import classList from 'flarum/common/utils/classList';
|
||||
|
||||
import FlagList from './FlagList';
|
||||
|
||||
export interface IFlagsDropdownAttrs extends IHeaderDropdownAttrs {}
|
||||
|
||||
export default class FlagsDropdown<CustomAttrs extends IFlagsDropdownAttrs = IFlagsDropdownAttrs> extends HeaderDropdown<CustomAttrs> {
|
||||
static initAttrs(attrs: IFlagsDropdownAttrs) {
|
||||
attrs.className = classList('FlagsDropdown', attrs.className);
|
||||
attrs.label = attrs.label || app.translator.trans('flarum-flags.forum.flagged_posts.tooltip');
|
||||
attrs.icon = attrs.icon || 'fas fa-flag';
|
||||
|
||||
super.initAttrs(attrs);
|
||||
}
|
||||
|
||||
getContent() {
|
||||
return <FlagList state={this.attrs.state} />;
|
||||
}
|
||||
|
||||
goToRoute() {
|
||||
m.route.set(app.route('flags'));
|
||||
}
|
||||
|
||||
getUnreadCount() {
|
||||
return app.forum.attribute<number>('flagCount');
|
||||
}
|
||||
|
||||
getNewCount() {
|
||||
return app.session.user!.attribute<number>('newFlagCount');
|
||||
}
|
||||
}
|
@@ -1,37 +0,0 @@
|
||||
export default class FlagListState {
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
|
||||
/**
|
||||
* Whether or not the flags are loading.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load flags into the application's cache if they haven't already
|
||||
* been loaded.
|
||||
*/
|
||||
load() {
|
||||
if (this.cache && !this.app.session.user.attribute('newFlagCount')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
this.app.store
|
||||
.find('flags')
|
||||
.then((flags) => {
|
||||
this.app.session.user.pushAttributes({ newFlagCount: 0 });
|
||||
this.cache = flags.sort((a, b) => b.createdAt() - a.createdAt());
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
}
|
33
extensions/flags/js/src/forum/states/FlagListState.tsx
Normal file
33
extensions/flags/js/src/forum/states/FlagListState.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
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> {
|
||||
public app: ForumApplication;
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
if (this.pages.length > 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return super.loadNext();
|
||||
}
|
||||
}
|
@@ -1,6 +1,9 @@
|
||||
.Post--flagged {
|
||||
--border-width: 2px;
|
||||
padding-top: 0 !important;
|
||||
border: 2px solid @primary-color;
|
||||
padding-left: var(--post-padding);
|
||||
margin-left: calc(~"0px - var(--post-padding)");
|
||||
border: var(--border-width) solid var(--primary-color);
|
||||
}
|
||||
|
||||
.Post-header .item-flagged {
|
||||
@@ -8,29 +11,22 @@
|
||||
margin: 0;
|
||||
}
|
||||
.Post-flagged {
|
||||
background: @primary-color;
|
||||
margin-top: -2px;
|
||||
margin-bottom: 20px;
|
||||
margin-left: -22px;
|
||||
margin-right: -22px;
|
||||
background: var(--primary-color);
|
||||
margin: calc(~"0px - var(--border-width)") calc(~"0px - var(--border-width) - var(--post-padding)") var(--post-padding);
|
||||
padding: 10px;
|
||||
border-radius: @border-radius @border-radius 0 0;
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
overflow: hidden;
|
||||
.light-contents(@color: @body-bg; @control-color: @body-bg);
|
||||
|
||||
@media @tablet-up {
|
||||
margin-left: -22px - 85px;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&, a {
|
||||
color: @body-bg !important;
|
||||
color: var(--body-bg) !important;
|
||||
}
|
||||
}
|
||||
.Post-flagged-flags {
|
||||
@media @tablet-up {
|
||||
float: left;
|
||||
}
|
||||
|
||||
font-size: 14px;
|
||||
margin: 7px 10px;
|
||||
text-align: left;
|
||||
@@ -42,19 +38,10 @@
|
||||
font-weight: normal;
|
||||
}
|
||||
.Post-flagged-actions {
|
||||
@media @tablet-up {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
.Post-flagged-actions .Button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.FlagsDropdown .Dropdown-toggle {
|
||||
.Button-label,
|
||||
.Button-caret {
|
||||
display: none;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.FlagPostModal {
|
||||
@@ -66,7 +53,16 @@
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
color: @text-color;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Flag .HeaderListItem-title {
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.Flag .HeaderListItem-time {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
@@ -10,7 +10,6 @@
|
||||
namespace Flarum\Flags\Access;
|
||||
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
@@ -23,31 +22,26 @@ class ScopeFlagVisibility
|
||||
|
||||
public function __invoke(User $actor, Builder $query): void
|
||||
{
|
||||
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');
|
||||
});
|
||||
$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 (! $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->orWhereDoesntHave('post.discussion.tags');
|
||||
}
|
||||
}
|
||||
|
||||
if (! $actor->hasPermission('discussion.viewFlags')) {
|
||||
$query->orWhere('flags.user_id', $actor->id);
|
||||
}
|
||||
if (! $actor->hasPermission('discussion.viewFlags')) {
|
||||
$query->orWhere('flags.user_id', $actor->id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,38 +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 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;
|
||||
}
|
||||
}
|
@@ -1,40 +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 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');
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
@@ -1,43 +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\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', []))
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,54 +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\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;
|
||||
}
|
||||
}
|
32
extensions/flags/src/Api/ForumResourceFields.php
Normal file
32
extensions/flags/src/Api/ForumResourceFields.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\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');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
42
extensions/flags/src/Api/PostResourceFields.php
Normal file
42
extensions/flags/src/Api/PostResourceFields.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
165
extensions/flags/src/Api/Resource/FlagResource.php
Normal file
165
extensions/flags/src/Api/Resource/FlagResource.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
@@ -1,48 +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\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(
|
||||
get_class($this).' 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);
|
||||
}
|
||||
}
|
36
extensions/flags/src/Api/UserResourceFields.php
Normal file
36
extensions/flags/src/Api/UserResourceFields.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,79 +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\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,7 +10,6 @@
|
||||
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;
|
||||
@@ -31,9 +30,6 @@ 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));
|
||||
}
|
||||
|
@@ -1,27 +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\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,6 +14,7 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -30,6 +31,11 @@ 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'];
|
||||
|
||||
|
30
extensions/flags/src/FlagFactory.php
Normal file
30
extensions/flags/src/FlagFactory.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,64 +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 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,9 +9,13 @@
|
||||
|
||||
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
|
||||
@@ -28,7 +32,7 @@ class ListTest extends TestCase
|
||||
$this->extension('flarum-flags');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
[
|
||||
'id' => 3,
|
||||
@@ -44,20 +48,22 @@ class ListTest extends TestCase
|
||||
'group_permission' => [
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => '', 'user_id' => 1, 'comment_count' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['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],
|
||||
],
|
||||
'flags' => [
|
||||
Flag::class => [
|
||||
['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],
|
||||
]
|
||||
]);
|
||||
}
|
||||
@@ -65,7 +71,7 @@ class ListTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function admin_can_see_one_flag_per_post()
|
||||
public function admin_can_see_one_flag_per_visible_post()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/flags', [
|
||||
@@ -73,9 +79,9 @@ class ListTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body = $response->getBody()->getContents());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
$data = json_decode($body, true)['data'];
|
||||
|
||||
$ids = Arr::pluck($data, 'id');
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5'], $ids);
|
||||
@@ -84,7 +90,7 @@ class ListTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function regular_user_sees_own_flags()
|
||||
public function regular_user_sees_own_flags_of_visible_posts()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/flags', [
|
||||
@@ -103,7 +109,7 @@ class ListTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function mod_can_see_one_flag_per_post()
|
||||
public function mod_can_see_one_flag_per_visible_post()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/flags', [
|
||||
|
@@ -9,9 +9,14 @@
|
||||
|
||||
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
|
||||
@@ -29,13 +34,13 @@ class ListWithTagsTest extends TestCase
|
||||
$this->extension('flarum-tags');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'tags' => [
|
||||
Tag::class => [
|
||||
['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],
|
||||
],
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
[
|
||||
'id' => 3,
|
||||
@@ -50,12 +55,12 @@ class ListWithTagsTest extends TestCase
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag2.viewDiscussions'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag2.viewForum'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag3.discussion.viewFlags'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewDiscussions'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewForum'],
|
||||
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.discussion.viewFlags'],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['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],
|
||||
@@ -68,7 +73,7 @@ class ListWithTagsTest extends TestCase
|
||||
['discussion_id' => 4, 'tag_id' => 4],
|
||||
['discussion_id' => 5, 'tag_id' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
// 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>'],
|
||||
@@ -79,7 +84,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>'],
|
||||
],
|
||||
'flags' => [
|
||||
Flag::class => [
|
||||
// From regular ListTest
|
||||
['id' => 1, 'post_id' => 1, 'user_id' => 1],
|
||||
['id' => 2, 'post_id' => 1, 'user_id' => 2],
|
||||
@@ -149,9 +154,7 @@ class ListWithTagsTest extends TestCase
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
|
||||
$ids = Arr::pluck($data, 'id');
|
||||
// 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);
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5', '8', '9'], $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -0,0 +1,145 @@
|
||||
<?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,19 +9,20 @@
|
||||
|
||||
namespace Flarum\Likes;
|
||||
|
||||
use Flarum\Api\Controller;
|
||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Likes\Api\LoadLikesRelationship;
|
||||
use Flarum\Likes\Api\PostResourceFields;
|
||||
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\Filter\PostFilterer;
|
||||
use Flarum\Post\Event\Deleted;
|
||||
use Flarum\Post\Filter\PostSearcher;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\Filter\UserFilterer;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\User\Search\UserSearcher;
|
||||
use Flarum\User\User;
|
||||
|
||||
return [
|
||||
@@ -38,49 +39,32 @@ return [
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(PostLikedBlueprint::class, PostSerializer::class, ['alert']),
|
||||
->type(PostLikedBlueprint::class, ['alert']),
|
||||
|
||||
(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\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\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)
|
||||
->subscribe(Listener\SaveLikesToDatabase::class),
|
||||
->listen(Deleted::class, function (Deleted $event) {
|
||||
$event->post->likes()->detach();
|
||||
}),
|
||||
|
||||
(new Extend\Filter(PostFilterer::class))
|
||||
->addFilter(LikedByFilter::class),
|
||||
|
||||
(new Extend\Filter(UserFilterer::class))
|
||||
->addFilter(LikedFilter::class),
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(PostSearcher::class, LikedByFilter::class)
|
||||
->addFilter(UserSearcher::class, 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
@@ -4,7 +4,7 @@ import CommentPost from 'flarum/forum/components/CommentPost';
|
||||
import Link from 'flarum/common/components/Link';
|
||||
import punctuateSeries from 'flarum/common/helpers/punctuateSeries';
|
||||
import username from 'flarum/common/helpers/username';
|
||||
import icon from 'flarum/common/helpers/icon';
|
||||
import Icon from 'flarum/common/components/Icon';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
|
||||
import PostLikesModal from './components/PostLikesModal';
|
||||
@@ -58,7 +58,7 @@ export default function () {
|
||||
items.add(
|
||||
'liked',
|
||||
<div className="Post-likedBy">
|
||||
{icon('far fa-thumbs-up')}
|
||||
<Icon name={'far fa-thumbs-up'} />
|
||||
{app.translator.trans(`flarum-likes.forum.post.liked_by${likes[0] === app.session.user ? '_self' : ''}_text`, {
|
||||
count: names.length,
|
||||
users: punctuateSeries(names),
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import Modal from 'flarum/common/components/Modal';
|
||||
import Link from 'flarum/common/components/Link';
|
||||
import avatar from 'flarum/common/helpers/avatar';
|
||||
import Avatar from 'flarum/common/components/Avatar';
|
||||
import username from 'flarum/common/helpers/username';
|
||||
import type { IInternalModalAttrs } from 'flarum/common/components/Modal';
|
||||
import type Post from 'flarum/common/models/Post';
|
||||
@@ -9,6 +9,7 @@ import type Mithril from 'mithril';
|
||||
import PostLikesModalState from '../states/PostLikesModalState';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
||||
import Form from 'flarum/common/components/Form';
|
||||
|
||||
export interface IPostLikesModalAttrs extends IInternalModalAttrs {
|
||||
post: Post;
|
||||
@@ -47,7 +48,7 @@ export default class PostLikesModal<CustomAttrs extends IPostLikesModalAttrs = I
|
||||
page.items.map((user) => (
|
||||
<li>
|
||||
<Link href={app.route.user(user)}>
|
||||
{avatar(user)} {username(user)}
|
||||
<Avatar user={user} /> {username(user)}
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
@@ -57,13 +58,13 @@ export default class PostLikesModal<CustomAttrs extends IPostLikesModalAttrs = I
|
||||
</div>
|
||||
{this.state.hasNext() ? (
|
||||
<div className="Modal-footer">
|
||||
<div className="Form Form--centered">
|
||||
<Form className="Form--centered">
|
||||
<div className="Form-group">
|
||||
<Button className="Button Button--block" onclick={() => this.state.loadNext()} loading={this.state.isLoadingNext()}>
|
||||
{app.translator.trans('flarum-likes.forum.post_likes.load_more_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
@@ -2,11 +2,15 @@ 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,14 +3,11 @@ 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();
|
||||
|
@@ -4,7 +4,7 @@
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
color: @text-color;
|
||||
color: var(--text-color);
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
|
@@ -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\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 [];
|
||||
}
|
||||
}
|
65
extensions/likes/src/Api/PostResourceFields.php
Normal file
65
extensions/likes/src/Api/PostResourceFields.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\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);
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,55 +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\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,10 +9,14 @@
|
||||
|
||||
namespace Flarum\Likes\Query;
|
||||
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Filter\ValidateFilterTrait;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class LikedByFilter implements FilterInterface
|
||||
{
|
||||
use ValidateFilterTrait;
|
||||
@@ -22,11 +26,11 @@ class LikedByFilter implements FilterInterface
|
||||
return 'likedBy';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
$likedId = $this->asInt($filterValue);
|
||||
$likedId = $this->asInt($value);
|
||||
|
||||
$filterState
|
||||
$state
|
||||
->getQuery()
|
||||
->whereIn('id', function ($query) use ($likedId, $negate) {
|
||||
$query->select('post_id')
|
||||
|
@@ -9,10 +9,14 @@
|
||||
|
||||
namespace Flarum\Likes\Query;
|
||||
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Filter\ValidateFilterTrait;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class LikedFilter implements FilterInterface
|
||||
{
|
||||
use ValidateFilterTrait;
|
||||
@@ -22,11 +26,11 @@ class LikedFilter implements FilterInterface
|
||||
return 'liked';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
$likedId = $this->asString($filterValue);
|
||||
$likedId = $this->asString($value);
|
||||
|
||||
$filterState
|
||||
$state
|
||||
->getQuery()
|
||||
->whereIn('id', function ($query) use ($likedId) {
|
||||
$query->select('user_id')
|
||||
|
@@ -10,9 +10,13 @@
|
||||
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
|
||||
@@ -26,21 +30,21 @@ class LikePostTest extends TestCase
|
||||
$this->extension('flarum-likes');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['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],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 2],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['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>'],
|
||||
],
|
||||
'groups' => [
|
||||
Group::class => [
|
||||
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
['id' => 6, 'name_singular' => 'Acme1', 'name_plural' => 'Acme1', 'is_hidden' => 0]
|
||||
],
|
||||
@@ -72,7 +76,7 @@ class LikePostTest extends TestCase
|
||||
|
||||
$post = CommentPost::query()->find($postId);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
$this->assertNotNull($post->likes->where('id', $authenticatedAs)->first(), $message);
|
||||
}
|
||||
|
||||
@@ -92,7 +96,7 @@ class LikePostTest extends TestCase
|
||||
|
||||
$post = CommentPost::query()->find($postId);
|
||||
|
||||
$this->assertEquals(403, $response->getStatusCode(), $message);
|
||||
$this->assertContainsEquals($response->getStatusCode(), [401, 403], $message);
|
||||
$this->assertNull($post->likes->where('id', $authenticatedAs)->first());
|
||||
}
|
||||
|
||||
|
@@ -10,10 +10,13 @@
|
||||
namespace Flarum\Likes\Tests\integration\api\discussions;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Likes\Api\LoadLikesRelationship;
|
||||
use Flarum\Likes\Api\PostResourceFields;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListPostsTest extends TestCase
|
||||
@@ -30,13 +33,13 @@ class ListPostsTest extends TestCase
|
||||
$this->extension('flarum-likes');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
],
|
||||
'users' => [
|
||||
User::class => [
|
||||
$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],
|
||||
@@ -132,7 +135,7 @@ class ListPostsTest extends TestCase
|
||||
$likes = $data['relationships']['likes']['data'];
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
$this->assertCount(PostResourceFields::$maxLikes, $likes);
|
||||
// Displays the correct count of likes
|
||||
$this->assertEquals(11, $data['attributes']['likesCount']);
|
||||
// Of the limited amount of likes, the actor always appears
|
||||
@@ -159,7 +162,7 @@ class ListPostsTest extends TestCase
|
||||
$likes = $data[0]['relationships']['likes']['data'];
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
$this->assertCount(PostResourceFields::$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
|
||||
@@ -170,7 +173,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(
|
||||
@@ -181,22 +184,27 @@ class ListPostsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$included = json_decode($response->getBody()->getContents(), true)['included'];
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$included = json_decode($body, true)['included'] ?? [];
|
||||
|
||||
$likes = collect($included)
|
||||
->where('type', 'posts')
|
||||
->where('id', 101)
|
||||
->first()['relationships']['likes']['data'];
|
||||
->first()['relationships']['likes']['data'] ?? null;
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
$this->assertNotNull($likes, $body);
|
||||
$this->assertCount(PostResourceFields::$maxLikes, $likes);
|
||||
// Displays the correct count of likes
|
||||
$this->assertEquals(11, collect($included)
|
||||
->where('type', 'posts')
|
||||
->where('id', 101)
|
||||
->first()['attributes']['likesCount']);
|
||||
->first()['attributes']['likesCount'] ?? null, $body);
|
||||
// Of the limited amount of likes, the actor always appears
|
||||
$this->assertEquals([2, 102, 104, 105], Arr::pluck($likes, 'id'));
|
||||
$this->assertEquals([2, 102, 104, 105], Arr::pluck($likes, 'id'), $body);
|
||||
}
|
||||
|
||||
public function likesIncludeProvider(): array
|
||||
@@ -204,7 +212,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\Serializer\BasicDiscussionSerializer;
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
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\Lock\Query\LockedFilterGambit;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
|
||||
return [
|
||||
(new Extend\Frontend('forum'))
|
||||
@@ -33,33 +33,44 @@ return [
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(DiscussionLockedBlueprint::class, BasicDiscussionSerializer::class, ['alert']),
|
||||
->type(DiscussionLockedBlueprint::class, ['alert']),
|
||||
|
||||
(new Extend\Model(Discussion::class))
|
||||
->cast('is_locked', 'bool'),
|
||||
|
||||
(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\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\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\Filter(DiscussionFilterer::class))
|
||||
->addFilter(LockedFilterGambit::class),
|
||||
|
||||
(new Extend\SimpleFlarumSearch(DiscussionSearcher::class))
|
||||
->addGambit(LockedFilterGambit::class),
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(DiscussionSearcher::class, LockedFilter::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: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})();
|
||||
(()=>{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})();
|
||||
//# 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,+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":""}
|
||||
{"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":""}
|
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: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()})}}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 L=flarum.reg.get("core","forum/components/EventPost");var h=o.n(L);class x extends(h()){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",x);const P=[(new(y().PostTypes)).add("discussionLocked",x),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})();
|
||||
(()=>{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})();
|
||||
//# 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
extensions/lock/js/src/admin/extend.ts
Normal file
1
extensions/lock/js/src/admin/extend.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as default } from '../common/extend';
|
@@ -1,5 +1,7 @@
|
||||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('lock', () => {
|
||||
app.extensionData.for('flarum-lock').registerPermission(
|
||||
{
|
||||
|
7
extensions/lock/js/src/common/extend.ts
Normal file
7
extensions/lock/js/src/common/extend.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import LockedGambit from './query/discussions/LockedGambit';
|
||||
|
||||
export default [
|
||||
new Extend.Search() //
|
||||
.gambit('discussions', LockedGambit),
|
||||
];
|
@@ -0,0 +1,12 @@
|
||||
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';
|
||||
}
|
||||
}
|
@@ -15,4 +15,8 @@ export default class DiscussionLockedNotification extends Notification {
|
||||
content() {
|
||||
return app.translator.trans('flarum-lock.forum.notifications.discussion_locked_text', { user: this.attrs.notification.fromUser() });
|
||||
}
|
||||
|
||||
excerpt() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@@ -2,10 +2,18 @@ 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,15 +1,12 @@
|
||||
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,3 +35,12 @@ 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
|
||||
|
36
extensions/lock/src/Filter/LockedFilter.php
Normal file
36
extensions/lock/src/Filter/LockedFilter.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
@@ -1,40 +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\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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,44 +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\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);
|
||||
}
|
||||
}
|
2
extensions/markdown/js/dist/admin.js
generated
vendored
2
extensions/markdown/js/dist/admin.js
generated
vendored
@@ -1,3 +1,3 @@
|
||||
/*! For license information please see admin.js.LICENSE.txt */
|
||||
(()=>{var t={n:e=>{var o=e&&e.__esModule?()=>e.default:()=>e;return t.d(o,{a:o}),o},d:(e,o)=>{for(var r in o)t.o(o,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:o[r]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};(()=>{"use strict";t.r(e);const o=flarum.reg.get("core","admin/app");var r=t.n(o);const i=flarum.reg.get("core","common/app");var n=t.n(i);const a=flarum.reg.get("core","common/extend"),l=flarum.reg.get("core","common/utils/BasicEditorDriver");var c=t.n(l);const s=flarum.reg.get("core","common/utils/styleSelectedText");var d=t.n(s);const f=flarum.reg.get("core","common/Component");var u=t.n(f);class p extends(u()){view(t){return m("div",{className:"MarkdownToolbar"},t.children)}}flarum.reg.add("flarum-markdown","common/components/MarkdownToolbar",p);const k=flarum.reg.get("core","common/helpers/icon");var h=t.n(k);const g=flarum.reg.get("core","common/components/Tooltip");var x=t.n(g);class b extends(u()){oncreate(t){super.oncreate(t)}view(){const t=m("button",{className:"Button Button--icon Button--link",type:"button","data-hotkey":this.attrs.hotkey,onkeydown:this.keydown.bind(this),onclick:this.attrs.onclick},h()(this.attrs.icon));return this.attrs.title?m(x(),{text:this.attrs.title},t):t}keydown(t){" "!==t.key&&"Enter"!==t.key||(t.preventDefault(),this.element.click())}}flarum.reg.add("flarum-markdown","common/components/MarkdownButton",b);const y=flarum.reg.get("core","common/utils/ItemList");var v=t.n(y);const w=navigator.userAgent.match(/Macintosh/)?"⌘":"ctrl",_={header:{prefix:"### "},bold:{prefix:"**",suffix:"**",trimFirst:!0},italic:{prefix:"_",suffix:"_",trimFirst:!0},strikethrough:{prefix:"~~",suffix:"~~",trimFirst:!0},quote:{prefix:"> ",multiline:!0,surroundWithNewlines:!0},code:{prefix:"`",suffix:"`",blockPrefix:"```",blockSuffix:"```"},link:{prefix:"[",suffix:"](https://)",replaceNext:"https://",scanFor:"https?://"},image:{prefix:"",replaceNext:"https://",scanFor:"https?://"},unordered_list:{prefix:"- ",multiline:!0,surroundWithNewlines:!0},ordered_list:{prefix:"1. ",multiline:!0,orderedList:!0},spoiler:{prefix:">!",suffix:"!<",blockPrefix:">! ",multiline:!0,trimFirst:!0}},T=(t,e)=>{d()(e.el,_[t])};function S(t,e,o){return function(r){r.key===e&&(r.metaKey&&"⌘"===w||r.ctrlKey&&"ctrl"===w)&&(r.preventDefault(),T(t,o))}}function M(t){const e="function"==typeof t?t():new(v());function o(t,e){return n().translator.trans("flarum-markdown.lib.composer.".concat(t,"_tooltip"))+(e?" <".concat(w,"-").concat(e,">"):"")}const r=t=>()=>T(t,this.attrs.composer.editor);return e.add("header",m(b,{title:o("header"),icon:"fas fa-heading",onclick:r("header")}),1e3),e.add("bold",m(b,{title:o("bold","b"),icon:"fas fa-bold",onclick:r("bold")}),900),e.add("italic",m(b,{title:o("italic","i"),icon:"fas fa-italic",onclick:r("italic")}),800),e.add("strikethrough",m(b,{title:o("strikethrough"),icon:"fas fa-strikethrough",onclick:r("strikethrough")}),700),e.add("quote",m(b,{title:o("quote"),icon:"fas fa-quote-left",onclick:r("quote")}),600),e.add("spoiler",m(b,{title:o("spoiler"),icon:"fas fa-exclamation-triangle",onclick:r("spoiler")}),500),e.add("code",m(b,{title:o("code"),icon:"fas fa-code",onclick:r("code")}),400),e.add("link",m(b,{title:o("link"),icon:"fas fa-link",onclick:r("link")}),300),e.add("image",m(b,{title:o("image"),icon:"fas fa-image",onclick:r("image")}),200),e.add("unordered_list",m(b,{title:o("unordered_list"),icon:"fas fa-list-ul",onclick:r("unordered_list")}),100),e.add("ordered_list",m(b,{title:o("ordered_list"),icon:"fas fa-list-ol",onclick:r("ordered_list")}),0),e}r().initializers.add("flarum-markdown",(function(t){(0,a.extend)(c().prototype,"keyHandlers",(function(t){t.add("bold",S("bold","b",this)),t.add("italic",S("italic","i",this))})),(0,a.override)("flarum/common/components/TextEditor","markdownToolbarItems",M),(0,a.extend)("flarum/common/components/TextEditor","toolbarItems",(function(t){t.add("markdown",m(p,{for:this.textareaId,setShortcutHandler:t=>shortcutHandler=t},this.markdownToolbarItems().toArray()),100)}))}))})(),module.exports=e})();
|
||||
(()=>{var t={n:e=>{var o=e&&e.__esModule?()=>e.default:()=>e;return t.d(o,{a:o}),o},d:(e,o)=>{for(var r in o)t.o(o,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:o[r]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};(()=>{"use strict";t.r(e);const o=flarum.reg.get("core","admin/app");var r=t.n(o);const i=flarum.reg.get("core","common/app");var n=t.n(i);const a=flarum.reg.get("core","common/extend"),l=flarum.reg.get("core","common/utils/BasicEditorDriver");var c=t.n(l);const s=flarum.reg.get("core","common/utils/styleSelectedText");var d=t.n(s);const f=flarum.reg.get("core","common/Component");var u=t.n(f);class p extends(u()){view(t){return m("div",{className:"MarkdownToolbar"},t.children)}}flarum.reg.add("flarum-markdown","common/components/MarkdownToolbar",p);const k=flarum.reg.get("core","common/components/Icon");var h=t.n(k);const g=flarum.reg.get("core","common/components/Tooltip");var x=t.n(g);class b extends(u()){oncreate(t){super.oncreate(t)}view(){const t=m("button",{className:"Button Button--icon Button--link",type:"button","data-hotkey":this.attrs.hotkey,onkeydown:this.keydown.bind(this),onclick:this.attrs.onclick},m(h(),{name:this.attrs.icon}));return this.attrs.title?m(x(),{text:this.attrs.title},t):t}keydown(t){" "!==t.key&&"Enter"!==t.key||(t.preventDefault(),this.element.click())}}flarum.reg.add("flarum-markdown","common/components/MarkdownButton",b);const y=flarum.reg.get("core","common/utils/ItemList");var v=t.n(y);const w=navigator.userAgent.match(/Macintosh/)?"⌘":"ctrl",_={header:{prefix:"### "},bold:{prefix:"**",suffix:"**",trimFirst:!0},italic:{prefix:"_",suffix:"_",trimFirst:!0},strikethrough:{prefix:"~~",suffix:"~~",trimFirst:!0},quote:{prefix:"> ",multiline:!0,surroundWithNewlines:!0},code:{prefix:"`",suffix:"`",blockPrefix:"```",blockSuffix:"```"},link:{prefix:"[",suffix:"](https://)",replaceNext:"https://",scanFor:"https?://"},image:{prefix:"",replaceNext:"https://",scanFor:"https?://"},unordered_list:{prefix:"- ",multiline:!0,surroundWithNewlines:!0},ordered_list:{prefix:"1. ",multiline:!0,orderedList:!0},spoiler:{prefix:">!",suffix:"!<",blockPrefix:">! ",multiline:!0,trimFirst:!0}},T=(t,e)=>{d()(e.el,_[t])};function S(t,e,o){return function(r){r.key===e&&(r.metaKey&&"⌘"===w||r.ctrlKey&&"ctrl"===w)&&(r.preventDefault(),T(t,o))}}function M(t){const e="function"==typeof t?t():new(v());function o(t,e){return n().translator.trans("flarum-markdown.lib.composer.".concat(t,"_tooltip"))+(e?" <".concat(w,"-").concat(e,">"):"")}const r=t=>()=>T(t,this.attrs.composer.editor);return e.add("header",m(b,{title:o("header"),icon:"fas fa-heading",onclick:r("header")}),1e3),e.add("bold",m(b,{title:o("bold","b"),icon:"fas fa-bold",onclick:r("bold")}),900),e.add("italic",m(b,{title:o("italic","i"),icon:"fas fa-italic",onclick:r("italic")}),800),e.add("strikethrough",m(b,{title:o("strikethrough"),icon:"fas fa-strikethrough",onclick:r("strikethrough")}),700),e.add("quote",m(b,{title:o("quote"),icon:"fas fa-quote-left",onclick:r("quote")}),600),e.add("spoiler",m(b,{title:o("spoiler"),icon:"fas fa-exclamation-triangle",onclick:r("spoiler")}),500),e.add("code",m(b,{title:o("code"),icon:"fas fa-code",onclick:r("code")}),400),e.add("link",m(b,{title:o("link"),icon:"fas fa-link",onclick:r("link")}),300),e.add("image",m(b,{title:o("image"),icon:"fas fa-image",onclick:r("image")}),200),e.add("unordered_list",m(b,{title:o("unordered_list"),icon:"fas fa-list-ul",onclick:r("unordered_list")}),100),e.add("ordered_list",m(b,{title:o("ordered_list"),icon:"fas fa-list-ol",onclick:r("ordered_list")}),0),e}r().initializers.add("flarum-markdown",(function(t){(0,a.extend)(c().prototype,"keyHandlers",(function(t){t.add("bold",S("bold","b",this)),t.add("italic",S("italic","i",this))})),(0,a.override)("flarum/common/components/TextEditor","markdownToolbarItems",M),(0,a.extend)("flarum/common/components/TextEditor","toolbarItems",(function(t){t.add("markdown",m(p,{for:this.textareaId,setShortcutHandler:t=>shortcutHandler=t},this.markdownToolbarItems().toArray()),100)}))}))})(),module.exports=e})();
|
||||
//# sourceMappingURL=admin.js.map
|
2
extensions/markdown/js/dist/admin.js.map
generated
vendored
2
extensions/markdown/js/dist/admin.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/markdown/js/dist/forum.js
generated
vendored
2
extensions/markdown/js/dist/forum.js
generated
vendored
@@ -1,3 +1,3 @@
|
||||
/*! For license information please see forum.js.LICENSE.txt */
|
||||
(()=>{var t={n:e=>{var o=e&&e.__esModule?()=>e.default:()=>e;return t.d(o,{a:o}),o},d:(e,o)=>{for(var r in o)t.o(o,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:o[r]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};(()=>{"use strict";t.r(e);const o=flarum.reg.get("core","forum/app");var r=t.n(o);const i=flarum.reg.get("core","common/app");var n=t.n(i);const a=flarum.reg.get("core","common/extend"),l=flarum.reg.get("core","common/utils/BasicEditorDriver");var c=t.n(l);const s=flarum.reg.get("core","common/utils/styleSelectedText");var d=t.n(s);const f=flarum.reg.get("core","common/Component");var u=t.n(f);class p extends(u()){view(t){return m("div",{className:"MarkdownToolbar"},t.children)}}flarum.reg.add("flarum-markdown","common/components/MarkdownToolbar",p);const k=flarum.reg.get("core","common/helpers/icon");var h=t.n(k);const g=flarum.reg.get("core","common/components/Tooltip");var x=t.n(g);class b extends(u()){oncreate(t){super.oncreate(t)}view(){const t=m("button",{className:"Button Button--icon Button--link",type:"button","data-hotkey":this.attrs.hotkey,onkeydown:this.keydown.bind(this),onclick:this.attrs.onclick},h()(this.attrs.icon));return this.attrs.title?m(x(),{text:this.attrs.title},t):t}keydown(t){" "!==t.key&&"Enter"!==t.key||(t.preventDefault(),this.element.click())}}flarum.reg.add("flarum-markdown","common/components/MarkdownButton",b);const y=flarum.reg.get("core","common/utils/ItemList");var v=t.n(y);const w=navigator.userAgent.match(/Macintosh/)?"⌘":"ctrl",_={header:{prefix:"### "},bold:{prefix:"**",suffix:"**",trimFirst:!0},italic:{prefix:"_",suffix:"_",trimFirst:!0},strikethrough:{prefix:"~~",suffix:"~~",trimFirst:!0},quote:{prefix:"> ",multiline:!0,surroundWithNewlines:!0},code:{prefix:"`",suffix:"`",blockPrefix:"```",blockSuffix:"```"},link:{prefix:"[",suffix:"](https://)",replaceNext:"https://",scanFor:"https?://"},image:{prefix:"",replaceNext:"https://",scanFor:"https?://"},unordered_list:{prefix:"- ",multiline:!0,surroundWithNewlines:!0},ordered_list:{prefix:"1. ",multiline:!0,orderedList:!0},spoiler:{prefix:">!",suffix:"!<",blockPrefix:">! ",multiline:!0,trimFirst:!0}},T=(t,e)=>{d()(e.el,_[t])};function S(t,e,o){return function(r){r.key===e&&(r.metaKey&&"⌘"===w||r.ctrlKey&&"ctrl"===w)&&(r.preventDefault(),T(t,o))}}function M(t){const e="function"==typeof t?t():new(v());function o(t,e){return n().translator.trans("flarum-markdown.lib.composer.".concat(t,"_tooltip"))+(e?" <".concat(w,"-").concat(e,">"):"")}const r=t=>()=>T(t,this.attrs.composer.editor);return e.add("header",m(b,{title:o("header"),icon:"fas fa-heading",onclick:r("header")}),1e3),e.add("bold",m(b,{title:o("bold","b"),icon:"fas fa-bold",onclick:r("bold")}),900),e.add("italic",m(b,{title:o("italic","i"),icon:"fas fa-italic",onclick:r("italic")}),800),e.add("strikethrough",m(b,{title:o("strikethrough"),icon:"fas fa-strikethrough",onclick:r("strikethrough")}),700),e.add("quote",m(b,{title:o("quote"),icon:"fas fa-quote-left",onclick:r("quote")}),600),e.add("spoiler",m(b,{title:o("spoiler"),icon:"fas fa-exclamation-triangle",onclick:r("spoiler")}),500),e.add("code",m(b,{title:o("code"),icon:"fas fa-code",onclick:r("code")}),400),e.add("link",m(b,{title:o("link"),icon:"fas fa-link",onclick:r("link")}),300),e.add("image",m(b,{title:o("image"),icon:"fas fa-image",onclick:r("image")}),200),e.add("unordered_list",m(b,{title:o("unordered_list"),icon:"fas fa-list-ul",onclick:r("unordered_list")}),100),e.add("ordered_list",m(b,{title:o("ordered_list"),icon:"fas fa-list-ol",onclick:r("ordered_list")}),0),e}r().initializers.add("flarum-markdown",(function(t){(0,a.extend)(c().prototype,"keyHandlers",(function(t){t.add("bold",S("bold","b",this)),t.add("italic",S("italic","i",this))})),(0,a.override)("flarum/common/components/TextEditor","markdownToolbarItems",M),(0,a.extend)("flarum/common/components/TextEditor","toolbarItems",(function(t){t.add("markdown",m(p,{for:this.textareaId,setShortcutHandler:t=>shortcutHandler=t},this.markdownToolbarItems().toArray()),100)}))}))})(),module.exports=e})();
|
||||
(()=>{var t={n:e=>{var o=e&&e.__esModule?()=>e.default:()=>e;return t.d(o,{a:o}),o},d:(e,o)=>{for(var r in o)t.o(o,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:o[r]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};(()=>{"use strict";t.r(e);const o=flarum.reg.get("core","forum/app");var r=t.n(o);const i=flarum.reg.get("core","common/app");var n=t.n(i);const a=flarum.reg.get("core","common/extend"),l=flarum.reg.get("core","common/utils/BasicEditorDriver");var c=t.n(l);const s=flarum.reg.get("core","common/utils/styleSelectedText");var d=t.n(s);const f=flarum.reg.get("core","common/Component");var u=t.n(f);class p extends(u()){view(t){return m("div",{className:"MarkdownToolbar"},t.children)}}flarum.reg.add("flarum-markdown","common/components/MarkdownToolbar",p);const k=flarum.reg.get("core","common/components/Icon");var h=t.n(k);const g=flarum.reg.get("core","common/components/Tooltip");var x=t.n(g);class b extends(u()){oncreate(t){super.oncreate(t)}view(){const t=m("button",{className:"Button Button--icon Button--link",type:"button","data-hotkey":this.attrs.hotkey,onkeydown:this.keydown.bind(this),onclick:this.attrs.onclick},m(h(),{name:this.attrs.icon}));return this.attrs.title?m(x(),{text:this.attrs.title},t):t}keydown(t){" "!==t.key&&"Enter"!==t.key||(t.preventDefault(),this.element.click())}}flarum.reg.add("flarum-markdown","common/components/MarkdownButton",b);const y=flarum.reg.get("core","common/utils/ItemList");var v=t.n(y);const w=navigator.userAgent.match(/Macintosh/)?"⌘":"ctrl",_={header:{prefix:"### "},bold:{prefix:"**",suffix:"**",trimFirst:!0},italic:{prefix:"_",suffix:"_",trimFirst:!0},strikethrough:{prefix:"~~",suffix:"~~",trimFirst:!0},quote:{prefix:"> ",multiline:!0,surroundWithNewlines:!0},code:{prefix:"`",suffix:"`",blockPrefix:"```",blockSuffix:"```"},link:{prefix:"[",suffix:"](https://)",replaceNext:"https://",scanFor:"https?://"},image:{prefix:"",replaceNext:"https://",scanFor:"https?://"},unordered_list:{prefix:"- ",multiline:!0,surroundWithNewlines:!0},ordered_list:{prefix:"1. ",multiline:!0,orderedList:!0},spoiler:{prefix:">!",suffix:"!<",blockPrefix:">! ",multiline:!0,trimFirst:!0}},T=(t,e)=>{d()(e.el,_[t])};function S(t,e,o){return function(r){r.key===e&&(r.metaKey&&"⌘"===w||r.ctrlKey&&"ctrl"===w)&&(r.preventDefault(),T(t,o))}}function M(t){const e="function"==typeof t?t():new(v());function o(t,e){return n().translator.trans("flarum-markdown.lib.composer.".concat(t,"_tooltip"))+(e?" <".concat(w,"-").concat(e,">"):"")}const r=t=>()=>T(t,this.attrs.composer.editor);return e.add("header",m(b,{title:o("header"),icon:"fas fa-heading",onclick:r("header")}),1e3),e.add("bold",m(b,{title:o("bold","b"),icon:"fas fa-bold",onclick:r("bold")}),900),e.add("italic",m(b,{title:o("italic","i"),icon:"fas fa-italic",onclick:r("italic")}),800),e.add("strikethrough",m(b,{title:o("strikethrough"),icon:"fas fa-strikethrough",onclick:r("strikethrough")}),700),e.add("quote",m(b,{title:o("quote"),icon:"fas fa-quote-left",onclick:r("quote")}),600),e.add("spoiler",m(b,{title:o("spoiler"),icon:"fas fa-exclamation-triangle",onclick:r("spoiler")}),500),e.add("code",m(b,{title:o("code"),icon:"fas fa-code",onclick:r("code")}),400),e.add("link",m(b,{title:o("link"),icon:"fas fa-link",onclick:r("link")}),300),e.add("image",m(b,{title:o("image"),icon:"fas fa-image",onclick:r("image")}),200),e.add("unordered_list",m(b,{title:o("unordered_list"),icon:"fas fa-list-ul",onclick:r("unordered_list")}),100),e.add("ordered_list",m(b,{title:o("ordered_list"),icon:"fas fa-list-ol",onclick:r("ordered_list")}),0),e}r().initializers.add("flarum-markdown",(function(t){(0,a.extend)(c().prototype,"keyHandlers",(function(t){t.add("bold",S("bold","b",this)),t.add("italic",S("italic","i",this))})),(0,a.override)("flarum/common/components/TextEditor","markdownToolbarItems",M),(0,a.extend)("flarum/common/components/TextEditor","toolbarItems",(function(t){t.add("markdown",m(p,{for:this.textareaId,setShortcutHandler:t=>shortcutHandler=t},this.markdownToolbarItems().toArray()),100)}))}))})(),module.exports=e})();
|
||||
//# sourceMappingURL=forum.js.map
|
2
extensions/markdown/js/dist/forum.js.map
generated
vendored
2
extensions/markdown/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
import Component from 'flarum/common/Component';
|
||||
import icon from 'flarum/common/helpers/icon';
|
||||
import Icon from 'flarum/common/components/Icon';
|
||||
import Tooltip from 'flarum/common/components/Tooltip';
|
||||
|
||||
export default class MarkdownButton extends Component {
|
||||
@@ -16,7 +16,7 @@ export default class MarkdownButton extends Component {
|
||||
onkeydown={this.keydown.bind(this)}
|
||||
onclick={this.attrs.onclick}
|
||||
>
|
||||
{icon(this.attrs.icon)}
|
||||
<Icon name={this.attrs.icon} />
|
||||
</button>
|
||||
);
|
||||
|
||||
|
@@ -9,25 +9,22 @@
|
||||
|
||||
namespace Flarum\Mentions;
|
||||
|
||||
use Flarum\Api\Controller;
|
||||
use Flarum\Api\Serializer\BasicPostSerializer;
|
||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
||||
use Flarum\Api\Serializer\CurrentUserSerializer;
|
||||
use Flarum\Api\Serializer\GroupSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Approval\Event\PostWasApproved;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Mentions\Api\LoadMentionedByRelationship;
|
||||
use Flarum\Mentions\Api\PostResourceFields;
|
||||
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\PostFilterer;
|
||||
use Flarum\Post\Filter\PostSearcher;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Tags\Api\Serializer\TagSerializer;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\User\User;
|
||||
|
||||
return [
|
||||
@@ -60,50 +57,49 @@ return [
|
||||
->namespace('flarum-mentions', __DIR__.'/views'),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(Notification\PostMentionedBlueprint::class, PostSerializer::class, ['alert'])
|
||||
->type(Notification\UserMentionedBlueprint::class, PostSerializer::class, ['alert'])
|
||||
->type(Notification\GroupMentionedBlueprint::class, PostSerializer::class, ['alert']),
|
||||
->type(Notification\PostMentionedBlueprint::class, ['alert'])
|
||||
->type(Notification\UserMentionedBlueprint::class, ['alert'])
|
||||
->type(Notification\GroupMentionedBlueprint::class, ['alert']),
|
||||
|
||||
(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\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\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\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\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\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\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'),
|
||||
|
||||
@@ -115,34 +111,34 @@ return [
|
||||
->listen(Hidden::class, Listener\UpdateMentionsMetadataWhenInvisible::class)
|
||||
->listen(Deleted::class, Listener\UpdateMentionsMetadataWhenInvisible::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');
|
||||
}),
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addFilter(PostSearcher::class, Filter\MentionedFilter::class)
|
||||
->addFilter(PostSearcher::class, Filter\MentionedPostFilter::class),
|
||||
|
||||
// Tag mentions
|
||||
(new Extend\Conditional())
|
||||
->whenExtensionEnabled('flarum-tags', [
|
||||
->whenExtensionEnabled('flarum-tags', fn () => [
|
||||
(new Extend\Formatter)
|
||||
->render(Formatter\FormatTagMentions::class)
|
||||
->unparse(Formatter\UnparseTagMentions::class),
|
||||
|
||||
(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\PostResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Relationship\ToMany::make('mentionsTags')
|
||||
->type('tags'),
|
||||
]),
|
||||
|
||||
(new Extend\ApiController(Controller\ListPostsController::class))
|
||||
->load(['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']);
|
||||
}),
|
||||
]),
|
||||
];
|
||||
|
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,45 +2,33 @@ 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(() => dropdown.active)
|
||||
.onUp(() => dropdown.navigate(-1))
|
||||
.onDown(() => dropdown.navigate(1))
|
||||
.onSelect(dropdown.complete.bind(dropdown))
|
||||
.onCancel(dropdown.hide.bind(dropdown))
|
||||
.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))
|
||||
.bindTo($editor);
|
||||
|
||||
$editor.after($container);
|
||||
$editor.after($('<div class="ComposerBody-mentionsDropdownContainer"></div>'));
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
@@ -48,30 +36,27 @@ 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;
|
||||
for (let i = lastChunk.length - 1; i >= 0; i--) {
|
||||
const character = lastChunk.substr(i, 1);
|
||||
activeFormat = app.mentionFormats.get(character);
|
||||
const autocompleteReader = new AutocompleteReader((character) => !!(activeFormat = app.mentionFormats.get(character)));
|
||||
const autocompleting = autocompleteReader.check(this.attrs.composer.editor.getLastNChars(30), cursor, /\S+/);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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();
|
||||
},
|
||||
});
|
||||
|
||||
dropdown.hide();
|
||||
dropdown.active = false;
|
||||
this.mentionsDropdown.hide();
|
||||
this.mentionsDropdown.active = false;
|
||||
|
||||
if (absMentionStart) {
|
||||
const typed = lastChunk.substring(relMentionStart).toLowerCase();
|
||||
matchTyped = activeFormat.queryFromTyped(typed);
|
||||
if (autocompleting) {
|
||||
mentionables.init(activeFormat.makeMentionables());
|
||||
matchTyped = activeFormat.queryFromTyped(autocompleting.typed);
|
||||
|
||||
if (!matchTyped) return;
|
||||
|
||||
@@ -83,14 +68,14 @@ export default function addComposerAutocomplete() {
|
||||
const suggestions = mentionables.buildSuggestions();
|
||||
|
||||
if (suggestions.length) {
|
||||
dropdown.items = suggestions;
|
||||
m.render($container[0], dropdown.render());
|
||||
this.mentionsDropdown.items = suggestions;
|
||||
m.render(this.$('.ComposerBody-mentionsDropdownContainer')[0], this.mentionsDropdown.render());
|
||||
|
||||
dropdown.show();
|
||||
const coordinates = this.attrs.composer.editor.getCaretCoordinates(absMentionStart);
|
||||
const width = dropdown.$().outerWidth();
|
||||
const height = dropdown.$().outerHeight();
|
||||
const parent = dropdown.$().offsetParent();
|
||||
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();
|
||||
let left = coordinates.left;
|
||||
let top = coordinates.top + 15;
|
||||
|
||||
@@ -106,21 +91,21 @@ export default function addComposerAutocomplete() {
|
||||
top = Math.max(-(parent.offset().top - $(document).scrollTop()), top);
|
||||
left = Math.max(-parent.offset().left, left);
|
||||
|
||||
dropdown.show(left, top);
|
||||
this.mentionsDropdown.show(left, top);
|
||||
} else {
|
||||
dropdown.active = false;
|
||||
dropdown.hide();
|
||||
this.mentionsDropdown.active = false;
|
||||
this.mentionsDropdown.hide();
|
||||
}
|
||||
};
|
||||
|
||||
dropdown.active = true;
|
||||
this.mentionsDropdown.active = true;
|
||||
|
||||
buildSuggestions();
|
||||
|
||||
dropdown.setIndex(0);
|
||||
dropdown.$().scrollTop(0);
|
||||
this.mentionsDropdown.setIndex(0);
|
||||
this.mentionsDropdown.$().scrollTop(0);
|
||||
|
||||
mentionables.search()?.then(buildSuggestions);
|
||||
this.searchMentions(mentionables, buildSuggestions);
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -5,7 +5,7 @@ import Link from 'flarum/common/components/Link';
|
||||
import PostPreview from 'flarum/forum/components/PostPreview';
|
||||
import punctuateSeries from 'flarum/common/helpers/punctuateSeries';
|
||||
import username from 'flarum/common/helpers/username';
|
||||
import icon from 'flarum/common/helpers/icon';
|
||||
import Icon from 'flarum/common/components/Icon';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import MentionedByModal from './components/MentionedByModal';
|
||||
|
||||
@@ -54,7 +54,9 @@ export default function addMentionedByList() {
|
||||
}}
|
||||
>
|
||||
<span className="PostPreview-content">
|
||||
<span className="PostPreview-badge Avatar">{icon('fas fa-reply-all')}</span>
|
||||
<span className="PostPreview-badge Avatar">
|
||||
<Icon name={'fas fa-reply-all'} />
|
||||
</span>
|
||||
<span>
|
||||
{app.translator.trans('flarum-mentions.forum.post.mentioned_by_more_text', { count: post.mentionedByCount() - replies.length })}
|
||||
</span>
|
||||
@@ -145,7 +147,7 @@ export default function addMentionedByList() {
|
||||
'replies',
|
||||
<div className="Post-mentionedBy">
|
||||
<span className="Post-mentionedBy-summary">
|
||||
{icon('fas fa-reply')}
|
||||
<Icon name={'fas fa-reply'} />
|
||||
{app.translator.trans(`flarum-mentions.forum.post.mentioned_by${repliers[0].user() === app.session.user ? '_self' : ''}_text`, {
|
||||
count: names.length,
|
||||
users: punctuateSeries(names),
|
||||
|
@@ -5,7 +5,7 @@ import CommentPost from 'flarum/forum/components/CommentPost';
|
||||
|
||||
import reply from './utils/reply';
|
||||
|
||||
export default function () {
|
||||
export default function addPostReplyAction() {
|
||||
extend(CommentPost.prototype, 'actionItems', function (items) {
|
||||
const post = this.attrs.post;
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import type Post from 'flarum/common/models/Post';
|
||||
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import MentionedByModalState from '../state/MentionedByModalState';
|
||||
import Form from 'flarum/common/components/Form';
|
||||
|
||||
export interface IMentionedByModalAttrs extends IInternalModalAttrs {
|
||||
post: Post;
|
||||
@@ -58,13 +59,13 @@ export default class MentionedByModal<CustomAttrs extends IMentionedByModalAttrs
|
||||
</div>
|
||||
{this.state.hasNext() && (
|
||||
<div className="Modal-footer">
|
||||
<div className="Form Form--centered">
|
||||
<Form className="Form--centered">
|
||||
<div className="Form-group">
|
||||
<Button className="Button Button--block" onclick={() => this.state.loadNext()} loading={this.state.isLoadingNext()}>
|
||||
{app.translator.trans('flarum-mentions.forum.mentioned_by.load_more_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
@@ -2,6 +2,9 @@ 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() //
|
||||
@@ -11,6 +14,11 @@ 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,7 +5,6 @@ import './components/UserMentionedNotification';
|
||||
import './fragments/AutocompleteDropdown';
|
||||
import './fragments/PostQuoteButton';
|
||||
import './utils/getCleanDisplayName';
|
||||
import './utils/getMentionText';
|
||||
import './utils/reply';
|
||||
import './utils/selectedText';
|
||||
import './utils/textFormatter';
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user