1
0
mirror of https://github.com/flarum/core.git synced 2025-09-03 13:12:50 +02:00

Compare commits

...

52 Commits

Author SHA1 Message Date
Sami Mazouz
2b917372a7 feat: eloquent factories (primarily for tests) (#3982) 2024-05-03 09:20:27 +01:00
Sami Mazouz
270188b5b0 fix: compiling split chunks in production 2024-04-26 14:25:31 +01:00
flarum-bot
9149ecc7aa Bundled output for commit 5fc2bb5eb6
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2024-04-07 12:23:32 +00:00
Sami Mazouz
5fc2bb5eb6 fix: broken assets 2024-04-07 13:19:57 +01:00
flarum-bot
24f3a6829f Bundled output for commit bf523b2325
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2024-04-06 13:55:44 +00:00
Sami Mazouz
bf523b2325 chore: extract buildSettingComponent method into a FormGroup component (#3927)
* chore: extract `buildSettingComponent` method into a `FormGroup` component

* chore: typings

* feat: move to common
2024-04-06 14:52:13 +01:00
Daniël Klabbers
e771b908d5 Patch vulnerability advisory (#3966)
Seems composer has a vulnerability, see https://github.com/advisories/GHSA-7c6p-848j-wh5h


Affected versions
>= 2.0.0-alpha1, < 2.2.23 -- patched in 2.2.23
>= 2.3.0-rc1, < 2.7.0 -- patched in 2.7.0

---

Let's raise the minimum to enforce the latest.

Thank you @peopleinside for reporting this.
2024-02-22 11:40:56 +01:00
flarum-bot
721e2eae3d Bundled output for commit 3fbe05fd18
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2024-01-22 18:01:17 +00:00
Sami Mazouz
3fbe05fd18 feat(em): port extension manager from 1.0 (#3959)
* feat(em): port extension manager from 1.0

* Apply fixes from StyleCI

* chore: phpstan

---------

Co-authored-by: StyleCI Bot <bot@styleci.io>
2024-01-22 18:58:08 +01:00
IanM
8f29b7af82 feat: support composer auth (#3961) 2024-01-22 18:57:54 +01:00
Ngô Quốc Đạt
734f4a150c chore: use hex_color rule for color validation (#3936) 2024-01-19 12:09:22 +01:00
flarum-bot
0186ca909e Bundled output for commit 1aa7806244
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2024-01-19 11:07:33 +00:00
Ngô Quốc Đạt
1aa7806244 Fix notify for all posts switch field loading state (#3938) 2024-01-19 12:04:25 +01:00
IanM
e3350543af feat: upgrade intervention/image to 3.2 (#3947)
* chore: create standalone imageprovider

* chore: upgrade intervention to v3

* Apply fixes from StyleCI

* use new static instatiation

* Revert "Apply fixes from StyleCI"

This reverts commit 096b4d9a79fa41c948a7572cf65316ebc6b07d36.

* get avatar from remote

* Apply fixes from StyleCI

* fix: incorrect gid exception namespace

* fix test

* remove debug code

---------

Co-authored-by: StyleCI Bot <bot@styleci.io>
2024-01-19 11:49:00 +01:00
Sami Mazouz
d400dcbc2f feat: dispatch event to flarum/installation-packages on release (#3625) 2024-01-19 09:54:26 +01:00
Davide Iadeluca
430709bf5b [2.x] fix(Mentions): allow renderer to be used without context (#3954)
* fix(Mentions): allow renderer to be used without context

* test(Mentions): implement test for rendering post without context

* Update UnparsePostMentions.php

* Update PostMentionsTest.php

---------

Co-authored-by: IanM <16573496+imorland@users.noreply.github.com>
2024-01-10 11:17:11 +00:00
flarum-bot
f784f48906 Bundled output for commit 3a34136e36
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2024-01-09 21:54:15 +00:00
Sami Mazouz
3a34136e36 feat: search UI/UX revamp (#3941)
* feat: first iteration

* chore: tweak

* feat: second iteration

* chore: incorrect code organization

* feat: gambit input suggestions

* feat: gambit keyboard navigation

* chore: bugs

* feat: negative gambits

* feat: improve gambit highlighting

* refactor: localize gambits

* feat: negative and positive gambit buttons

* fix: permissions

* chore: wat

* per: lazy load search modal

* fix: extensibility and bug fixes

* fix: bugs

* feat: reusable autocomplete dropdown

* chore: format

* fix: tag filter
2024-01-09 21:51:01 +00:00
flarum-bot
fb1703cd9b Bundled output for commit b58fec7ead
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2024-01-09 18:13:57 +00:00
Ngô Quốc Đạt
b58fec7ead Fix width issue in edit user modal (#3939)
* Fix width input issue in EditUserModal

* update
2024-01-09 18:10:55 +00:00
flarum-bot
537f97a07a Bundled output for commit c1be00e79a
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2024-01-09 18:04:43 +00:00
Sami Mazouz
c1be00e79a chore: improve debugging experience (#3944) 2024-01-09 18:01:29 +00:00
dependabot[bot]
91b89bc698 chore(deps): bump follow-redirects from 1.15.2 to 1.15.4 (#3957)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 18:19:28 +01:00
flarum-bot
278617a10d Bundled output for commit f793e5b8f8
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2024-01-09 17:10:55 +00:00
IanM
f793e5b8f8 fix: ts error causing build to fail (#3956) 2024-01-09 17:07:38 +00:00
IanM
01598555a9 chore: larastan changed namespace (#3955) 2024-01-09 17:07:26 +00:00
StyleCI Bot
5399c86a1b Apply fixes from StyleCI 2024-01-09 15:06:12 +00:00
IanM
74ce4cf1a7 chore: dummy commit for StyleCI purposes 2024-01-09 15:05:47 +00:00
Ngô Quốc Đạt
b4a82e81fc fix: overflowing forum stats (#3940) 2023-12-01 11:42:36 +01:00
Rafał Całka
471ce0ea2a fix(approval): PostWasApproved event triggered incorrectly 2023-11-23 18:50:47 +01:00
IanM
723cb73d48 Enable testing on PHP 8.3 (#3932) 2023-11-23 18:48:24 +01:00
flarum-bot
e4abf93763 Bundled output for commit 9310ce13d8
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2023-11-23 17:19:53 +00:00
Sami Mazouz
9310ce13d8 chore: yarn format 2023-11-23 18:16:49 +01:00
flarum-bot
d4c532c949 Bundled output for commit 46357ee9a9
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2023-11-23 17:14:53 +00:00
Tristian Kelly
46357ee9a9 perf(flags): add pagination to flags list (#3931) 2023-11-23 18:11:46 +01:00
flarum-bot
6cbdfb6aa1 Bundled output for commit 4b126d9f4c
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2023-11-11 18:46:13 +00:00
Sami Mazouz
4b126d9f4c feat: revamp search (#3893)
* refactor: move gambits to frontend (#3885)
* refactor: move gambits to frontend
* test: GambitManager
* refactor: merge filterer and searcher concepts (#3892)
* chore: drop remaining backend regex gambits
* refactor: merge filterer & searcher concept
* refactor: adapt extenders
* refactor: no longer need to push gambits to `q`
* refactor: filters to gambits
* refactor: drop shred `Query` namespace
* chore: cleanup
* chore: leftover gambit references on the backend (#3894)
* chore: leftover gambit references on the backend
* chore: namespace
* feat: search driver backend extension API (#3902)
* feat: first iteration of search drivers
* feat: indexer API & tweaks
* feat: changes after POC driver
* fix: properly fire custom observables
* chore: remove debugging code
* fix: phpstan
* fix: custom eloquent events
* chore: drop POC usage
* test: indexer extender API
* fix: extension searcher fails without filters
* fix: phpstan
* fix: frontend created gambit
* feat: advanced page and localized driver settings (#3905)
* feat: allow getting total search results and replacing filters (#3906)
* feat: allow accessing total search results
* feat: allow replacing filters
* chore: phpstan
2023-11-11 19:43:09 +01:00
flarum-bot
9e04b010d8 Bundled output for commit 1c0e0933b0
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2023-11-11 18:38:17 +00:00
Sami Mazouz
1c0e0933b0 feat: improve emoji autocomplete (#3923)
* feat: improve emoji autocomplete
* chore: improve dropdown header
2023-11-11 19:31:53 +01:00
flarum-bot
60ffa78531 Bundled output for commit 294878291c
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2023-11-10 22:16:11 +00:00
IanM
294878291c chore: format js 2023-11-10 22:09:50 +00:00
flarum-bot
05a7e24836 Bundled output for commit eaabeab8c9
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2023-11-10 22:04:30 +00:00
Sami Mazouz
eaabeab8c9 chore: extract FormModal from Modal (#3922) 2023-11-10 21:59:34 +00:00
flarum-bot
9ef366493c Bundled output for commit 208b94dc12
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2023-11-10 21:50:02 +00:00
Sami Mazouz
208b94dc12 chore(mentions,emoji): tie autocomplete to editor instance (#3913) 2023-11-10 21:44:00 +00:00
Sami Mazouz
5e3f8db095 fix: handled API errors break preloaded content (#3920)
* fix: handled API errors break preloaded content

* Apply fixes from StyleCI

---------

Co-authored-by: StyleCI Bot <bot@styleci.io>
2023-11-10 21:39:08 +00:00
flarum-bot
deb99f0de4 Bundled output for commit 693bce912a
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2023-11-10 21:38:19 +00:00
Sami Mazouz
693bce912a fix(testing): use cookie for testing authentication (#3924) 2023-11-10 21:35:24 +00:00
Sami Mazouz
3107319812 fix: paginated list limit hard to change (#3918)
* fix: paginated list limit hard to change

* chore: use the default value

* chore: apply to other list states

* chore: remove debugging code

* fix: typings
2023-11-10 21:31:46 +00:00
Sami Mazouz
a9756cb5eb fix: only set actor on events that have it (#3914) 2023-11-10 21:25:10 +00:00
flarum-bot
bbdf3b5aba Bundled output for commit e2281a2123
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2023-11-10 21:23:23 +00:00
Sami Mazouz
e2281a2123 chore: handle deprecations from 1.x (#3909)
* chore: drop deprecated `post_number_index` column

* chore: remove deprecated `FlagsWillBeDeleted` event

* chore: `Migration::addSettings` can still be needed

* chore: `settings->get` default can still be needed

* chore: deprecated `$default` in `Settings::serializeToForum` extender

* chore: deprecated request `actor` attribute

* chore: already handled

* chore: remove `RecompileFrontendAssets::whenSettingsSaved`

* chore: remove `getReadIds`

* chore: `Model::dateAttribute` extender

* chore: `evented` js util

* chore: `WelcomeHero` js hidden prop

* chore: attributes pushData with relations

* chore: app request options `extract`

* chore: itemlist deprecations

* chore: `search` state

* chore: `getMentionText`

* chore: deprecated non-registered store type

* chore: `Button` title

* chore: `Modal` deprecations

* chore

* chore: deprecated `less` mixins

* Apply fixes from StyleCI

* fix

* fix: phpstan

* fix

* fix

---------

Co-authored-by: StyleCI Bot <bot@styleci.io>
2023-11-10 21:20:18 +00:00
687 changed files with 10013 additions and 5844 deletions

View File

@@ -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.
@@ -51,10 +52,16 @@ on:
required: false
default: error_reporting=E_ALL
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:

View File

@@ -90,11 +90,15 @@ on:
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:

View 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) }}

View File

@@ -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).

View File

@@ -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",
@@ -112,6 +112,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",
@@ -168,7 +169,7 @@
"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": {

View File

@@ -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,

View File

@@ -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));
}
}
}
}

View File

@@ -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' => [

View File

@@ -50,7 +50,7 @@
padding: 15px 15px;
.scrolled & {
.box-shadow(0 2px 6px var(--shadow-color));
box-shadow: 0 2px 6px var(--shadow-color);
}
}

View File

@@ -1,6 +0,0 @@
{
"printWidth": 150,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}

2
extensions/emoji/js/dist/forum.js generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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;
}
});
});

View File

@@ -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: var(--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 {
> .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;
}
}

View File

@@ -8,4 +8,5 @@ export interface IFlagListAttrs extends ComponentAttrs {
export default class FlagList<CustomAttrs extends IFlagListAttrs = IFlagListAttrs> extends Component<CustomAttrs, FlagListState> {
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
view(): JSX.Element;
content(state: FlagListState): JSX.Element[][] | null;
}

View File

@@ -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";

View File

@@ -1,15 +1,13 @@
import type ForumApplication from 'flarum/forum/ForumApplication';
import type Flag from '../models/Flag';
import type Post from 'flarum/common/models/Post';
export default class FlagListState {
import PaginatedListState from 'flarum/common/states/PaginatedListState';
export default class FlagListState extends PaginatedListState<Flag> {
app: ForumApplication;
loading: boolean;
cache: Flag[] | null;
index: Post | false | null;
constructor(app: ForumApplication);
get type(): string;
/**
* Load flags into the application's cache if they haven't already
* been loaded.
*/
load(): void;
load(): Promise<void>;
}

2
extensions/flags/js/dist/forum.js generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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';

View File

@@ -8,6 +8,8 @@ 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;
@@ -16,49 +18,55 @@ export interface IFlagListAttrs extends ComponentAttrs {
export default class FlagList<CustomAttrs extends IFlagListAttrs = IFlagListAttrs> extends Component<CustomAttrs, FlagListState> {
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.state = this.attrs.state;
}
view() {
const flags = this.state.cache || [];
const state = this.attrs.state;
return (
<HeaderList
className="FlagList"
title={app.translator.trans('flarum-flags.forum.flagged_posts.title')}
hasItems={flags.length}
loading={this.state.loading}
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.state.loading &&
flags.map((flag) => {
const post = flag.post() as Post;
return (
<li>
<HeaderListItem
className="Flag"
avatar={<Avatar user={post.user() || null} />}
icon="fas fa-flag"
content={app.translator.trans('flarum-flags.forum.flagged_posts.item_text', {
username: username(post.user()),
em: <em />,
discussion: post.discussion().title(),
})}
excerpt={post.contentPlain()}
datetime={flag.createdAt()}
href={app.route.post(post)}
onclick={(e: MouseEvent) => {
app.flags.index = post;
e.redraw = false;
}}
/>
</li>
);
})}
</ul>
<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;
}
}

View File

@@ -1,5 +1,5 @@
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';
@@ -7,7 +7,7 @@ import Stream from 'flarum/common/utils/Stream';
import withAttr from 'flarum/common/utils/withAttr';
import ItemList from 'flarum/common/utils/ItemList';
export default class FlagPostModal extends Modal {
export default class FlagPostModal extends FormModal {
oninit(vnode) {
super.oninit(vnode);

View File

@@ -25,7 +25,7 @@ export default class FlagsDropdown<CustomAttrs extends IFlagsDropdownAttrs = IFl
}
getUnreadCount() {
return app.flags.cache ? app.flags.cache.length : app.forum.attribute<number>('flagCount');
return app.forum.attribute<number>('flagCount');
}
getNewCount() {

View File

@@ -1,39 +1,33 @@
import type ForumApplication from 'flarum/forum/ForumApplication';
import type Flag from '../models/Flag';
import type Post from 'flarum/common/models/Post';
import PaginatedListState from 'flarum/common/states/PaginatedListState';
export default class FlagListState {
export default class FlagListState extends PaginatedListState<Flag> {
public app: ForumApplication;
public loading = false;
public cache: Flag[] | null = null;
public index: Post | false | null = null;
constructor(app: ForumApplication) {
super({}, 1, null);
this.app = app;
}
get type(): string {
return 'flags';
}
/**
* Load flags into the application's cache if they haven't already
* been loaded.
*/
load() {
if (this.cache && !this.app.session.user!.attribute<number>('newFlagCount')) {
return;
load(): Promise<void> {
if (this.app.session.user?.attribute<number>('newFlagCount')) {
this.pages = [];
this.location = { page: 1 };
}
this.loading = true;
m.redraw();
if (this.pages.length > 0) {
return Promise.resolve();
}
this.app.store
.find<Flag[]>('flags')
.then((flags) => {
this.app.session.user!.pushAttributes({ newFlagCount: 0 });
this.cache = flags.sort((a, b) => b.createdAt()!.getTime() - a.createdAt()!.getTime());
})
.catch(() => {})
.then(() => {
this.loading = false;
m.redraw();
});
return super.loadNext();
}
}

View File

@@ -32,6 +32,7 @@ class AddCanFlagAttribute
// 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;
}

View File

@@ -14,6 +14,7 @@ use Flarum\Api\Controller\AbstractListController;
use Flarum\Flags\Api\Serializer\FlagSerializer;
use Flarum\Flags\Flag;
use Flarum\Http\RequestUtil;
use Flarum\Http\UrlGenerator;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -28,26 +29,52 @@ class ListFlagsController extends AbstractListController
'post.discussion'
];
public function __construct(
protected UrlGenerator $url
) {
}
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();
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$include = $this->extractInclude($request);
if (in_array('post.user', $include)) {
$include[] = 'post.user.groups';
}
$this->loadRelations($flags, $include);
$flags = Flag::whereVisibleTo($actor)
->latest('flags.created_at')
->groupBy('post_id')
->limit($limit + 1)
->offset($offset)
->get();
$this->loadRelations($flags, $include, $request);
$flags = $flags->all();
$areMoreResults = false;
if (count($flags) > $limit) {
array_pop($flags);
$areMoreResults = true;
}
$this->addPaginationData(
$document,
$request,
$this->url->to('api')->route('flags.index'),
$areMoreResults ? null : 0
);
return $flags;
}

View File

@@ -29,10 +29,10 @@ class FlagSerializer extends AbstractSerializer
}
return [
'type' => $model->type,
'reason' => $model->reason,
'type' => $model->type,
'reason' => $model->reason,
'reasonDetail' => $model->reason_detail,
'createdAt' => $this->formatDate($model->created_at),
'createdAt' => $this->formatDate($model->created_at),
];
}

View File

@@ -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));
}

View File

@@ -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 = []
) {
}
}

View File

@@ -9,9 +9,12 @@
namespace Flarum\Flags\Tests\integration\api\flags;
use Flarum\Discussion\Discussion;
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 +31,7 @@ class ListTest extends TestCase
$this->extension('flarum-flags');
$this->prepareDatabase([
'users' => [
User::class => [
$this->normalUser(),
[
'id' => 3,
@@ -44,10 +47,10 @@ 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>'],

View File

@@ -9,9 +9,13 @@
namespace Flarum\Flags\Tests\integration\api\flags;
use Flarum\Discussion\Discussion;
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 +33,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,
@@ -55,7 +59,7 @@ class ListWithTagsTest extends TestCase
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewDiscussions'],
['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 +72,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>'],

View File

@@ -19,9 +19,10 @@ 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\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 [
@@ -76,11 +77,9 @@ return [
->listen(PostWasUnliked::class, Listener\SendNotificationWhenPostIsUnliked::class)
->subscribe(Listener\SaveLikesToDatabase::class),
(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),

View File

@@ -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')

View File

@@ -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')

View File

@@ -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]
],

View File

@@ -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\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],

View File

@@ -11,16 +11,16 @@ use Flarum\Api\Serializer\BasicDiscussionSerializer;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\Event\Saving;
use Flarum\Discussion\Filter\DiscussionFilterer;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Extend;
use Flarum\Lock\Access;
use Flarum\Lock\Event\DiscussionWasLocked;
use Flarum\Lock\Event\DiscussionWasUnlocked;
use Flarum\Lock\Filter\LockedFilter;
use Flarum\Lock\Listener;
use Flarum\Lock\Notification\DiscussionLockedBlueprint;
use Flarum\Lock\Post\DiscussionLockedPost;
use Flarum\Lock\Query\LockedFilterGambit;
use Flarum\Search\Database\DatabaseSearchDriver;
return [
(new Extend\Frontend('forum'))
@@ -57,9 +57,6 @@ return [
(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
View File

@@ -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

View File

@@ -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
View File

@@ -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()})}excerpt(){return null}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedNotification",a);const i=flarum.reg.get("core","common/models/Discussion");var u=o.n(i);const l=flarum.reg.get("core","common/components/Badge");var d=o.n(l);const f=flarum.reg.get("core","forum/utils/DiscussionControls");var k=o.n(f);const g=flarum.reg.get("core","forum/components/DiscussionPage");var p=o.n(g);const b=flarum.reg.get("core","common/components/Button");var _=o.n(b);const v=flarum.reg.get("core","common/extenders");var y=o.n(v);const x=flarum.reg.get("core","forum/components/EventPost");var L=o.n(x);class h extends(L()){icon(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"}descriptionKey(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedPost",h);const P=[(new(y().PostTypes)).add("discussionLocked",h),new(y().Model)(u()).attribute("isLocked").attribute("canLock")];r().initializers.add("flarum-lock",(()=>{r().notificationComponents.discussionLocked=a,(0,e.extend)(u().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(d(),{type:"locked",label:r().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,e.extend)(k(),"moderationControls",(function(o,t){t.canLock()&&o.add("lock",m(_(),{icon:"fas fa-lock",onclick:this.lockAction.bind(t)},r().translator.trans("flarum-lock.forum.discussion_controls.".concat(t.isLocked()?"unlock":"lock","_button"))))})),k().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((()=>{r().current.matches(p())&&r().current.get("stream").update(),m.redraw()}))},(0,e.extend)("flarum/forum/components/NotificationGrid","notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:r().translator.trans("flarum-lock.forum.settings.notify_discussion_locked_label")})}))}))})(),module.exports=t})();
(()=>{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:()=>D});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","forum/components/Notification");var c=o.n(s);class a extends(c()){icon(){return"fas fa-lock"}href(){const o=this.attrs.notification;return r().route.discussion(o.subject(),o.content().postNumber)}content(){return r().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})}excerpt(){return null}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedNotification",a);const i=flarum.reg.get("core","common/models/Discussion");var l=o.n(i);const u=flarum.reg.get("core","common/components/Badge");var d=o.n(u);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 y=o.n(b);const _=flarum.reg.get("core","common/extenders");var v=o.n(_);const x=flarum.reg.get("core","forum/components/EventPost");var L=o.n(x);class h extends(L()){icon(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"}descriptionKey(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"}}flarum.reg.add("flarum-lock","forum/components/DiscussionLockedPost",h);const P=flarum.reg.get("core","common/query/IGambit"),S=flarum.reg.get("core","common/app");var j=o.n(S);class w extends P.BooleanGambit{key(){return j().translator.trans("flarum-lock.lib.gambits.discussions.locked.key",{},!0)}filterKey(){return"locked"}}flarum.reg.add("flarum-lock","common/query/discussions/LockedGambit",w);const D=[(new(v().Search)).gambit("discussions",w),(new(v().PostTypes)).add("discussionLocked",h),new(v().Model)(l()).attribute("isLocked").attribute("canLock")];r().initializers.add("flarum-lock",(()=>{r().notificationComponents.discussionLocked=a,(0,t.extend)(l().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,t.extend)(k(),"moderationControls",(function(o,e){e.canLock()&&o.add("lock",m(y(),{icon:"fas fa-lock",onclick:this.lockAction.bind(e)},r().translator.trans("flarum-lock.forum.discussion_controls.".concat(e.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,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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
export { default as default } from '../common/extend';

View File

@@ -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(
{

View 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),
];

View File

@@ -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';
}
}

View File

@@ -2,7 +2,11 @@ import Extend from 'flarum/common/extenders';
import Discussion from 'flarum/common/models/Discussion';
import DiscussionLockedPost from './components/DiscussionLockedPost';
import commonExtend from '../common/extend';
export default [
...commonExtend,
new Extend.PostTypes() //
.add('discussionLocked', DiscussionLockedPost),

View File

@@ -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

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -24,8 +24,9 @@ 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\Search\Database\DatabaseSearchDriver;
use Flarum\Tags\Api\Serializer\TagSerializer;
use Flarum\User\User;
@@ -114,9 +115,9 @@ 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\SearchDriver(DatabaseSearchDriver::class))
->addFilter(PostSearcher::class, Filter\MentionedFilter::class)
->addFilter(PostSearcher::class, Filter\MentionedPostFilter::class),
(new Extend\ApiSerializer(CurrentUserSerializer::class))
->attribute('canMentionGroups', function (CurrentUserSerializer $serializer, User $user): bool {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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);
}
};

View File

@@ -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';

View File

@@ -2,7 +2,6 @@ import type MentionableModel from './MentionableModel';
import type Model from 'flarum/common/Model';
import type Mithril from 'mithril';
import MentionsDropdownItem from '../components/MentionsDropdownItem';
import { throttle } from 'flarum/common/utils/throttleDebounce';
export default class MentionableModels {
protected mentionables?: MentionableModel[];
@@ -33,7 +32,7 @@ export default class MentionableModels {
* Don't send API calls searching for models until at least 2 characters have been typed.
* This focuses the mention results on models already loaded.
*/
public readonly search = throttle(250, async (): Promise<void> => {
public readonly search = async (): Promise<void> => {
if (!this.typed || this.typed.length <= 1) return;
const typedLower = this.typed.toLowerCase();
@@ -51,7 +50,7 @@ export default class MentionableModels {
this.searched.push(typedLower);
return Promise.resolve();
});
};
public matches(mentionable: MentionableModel, model: Model): boolean {
return mentionable.matches(model, this.typed?.toLowerCase() || '');

View File

@@ -1,21 +0,0 @@
import app from 'flarum/forum/app';
/**
* Fetches the mention text for a specified user (and optionally a post ID for replies or group).
*
* Automatically determines which mention syntax to be used based on the option in the
* admin dashboard. Also performs display name clean-up automatically.
*
* @deprecated Use `app.mentionables.get('user').replacement(user)` instead. Will be removed in 2.0.
*/
export default function getMentionText(user, postId, group) {
if (user !== undefined && postId === undefined) {
return app.mentionables.get('user').replacement(user);
} else if (user !== undefined && postId !== undefined) {
return app.mentionables.get('post').replacement(app.store.getById('posts', postId));
} else if (group !== undefined) {
return app.mentionables.get('group').replacement(group);
}
throw 'No parameters were passed';
}

View File

@@ -9,10 +9,14 @@
namespace Flarum\Mentions\Filter;
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 MentionedFilter implements FilterInterface
{
use ValidateFilterTrait;
@@ -22,11 +26,11 @@ class MentionedFilter implements FilterInterface
return 'mentioned';
}
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
public function filter(SearchState $state, string|array $value, bool $negate): void
{
$mentionedId = $this->asInt($filterValue);
$mentionedId = $this->asInt($value);
$filterState
$state
->getQuery()
->join('post_mentions_user', 'posts.id', '=', 'post_mentions_user.post_id')
->where('post_mentions_user.mentions_user_id', $negate ? '!=' : '=', $mentionedId);

View File

@@ -9,9 +9,13 @@
namespace Flarum\Mentions\Filter;
use Flarum\Filter\FilterInterface;
use Flarum\Filter\FilterState;
use Flarum\Search\Database\DatabaseSearchState;
use Flarum\Search\Filter\FilterInterface;
use Flarum\Search\SearchState;
/**
* @implements FilterInterface<DatabaseSearchState>
*/
class MentionedPostFilter implements FilterInterface
{
public function getFilterKey(): string
@@ -19,11 +23,11 @@ class MentionedPostFilter implements FilterInterface
return 'mentionedPost';
}
public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void
public function filter(SearchState $state, string|array $value, bool $negate): void
{
$mentionedId = trim($filterValue, '"');
$mentionedId = trim($value, '"');
$filterState
$state
->getQuery()
->join('post_mentions_post', 'posts.id', '=', 'post_mentions_post.post_id')
->where('post_mentions_post.mentions_post_id', $negate ? '!=' : '=', $mentionedId);

View File

@@ -12,6 +12,7 @@ namespace Flarum\Mentions\Formatter;
use Flarum\Discussion\Discussion;
use Flarum\Http\SlugManager;
use Flarum\Locale\TranslatorInterface;
use Flarum\Post\Post;
use Psr\Http\Message\ServerRequestInterface as Request;
use s9e\TextFormatter\Renderer;
use s9e\TextFormatter\Utils;
@@ -24,12 +25,22 @@ class FormatPostMentions
) {
}
public function __invoke(Renderer $renderer, mixed $context, ?string $xml, Request $request = null): string
/**
* Configure rendering for post mentions.
*
* @param \s9e\TextFormatter\Renderer $renderer
* @param mixed $context
* @param string $xml
* @param \Psr\Http\Message\ServerRequestInterface|null $request
* @return string $xml to be rendered
*/
public function __invoke(Renderer $renderer, $context, $xml, Request $request = null)
{
$post = $context;
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($context) {
$post = (($context && isset($context->getRelations()['mentionsPosts'])) || $context instanceof Post)
? $context->mentionsPosts->find($attributes['id'])
: Post::find($attributes['id']);
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($post) {
$post = $post->mentionsPosts->find($attributes['id']);
if ($post && $post->user) {
$attributes['displayname'] = $post->user->display_name;
}

View File

@@ -10,6 +10,7 @@
namespace Flarum\Mentions\Formatter;
use Flarum\Locale\TranslatorInterface;
use Flarum\Post\Post;
use s9e\TextFormatter\Utils;
class UnparsePostMentions
@@ -31,10 +32,11 @@ class UnparsePostMentions
*/
protected function updatePostMentionTags(mixed $context, string $xml): string
{
$post = $context;
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($context) {
$post = (($context && isset($context->getRelations()['mentionsPosts'])) || $context instanceof Post)
? $context->mentionsPosts->find($attributes['id'])
: Post::find($attributes['id']);
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($post) {
$post = $post->mentionsPosts->find($attributes['id']);
if ($post && $post->user) {
$attributes['displayname'] = $post->user->display_name;
}

View File

@@ -10,8 +10,10 @@
namespace Flarum\Mentions\Tests\integration\api;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Group\Group;
use Flarum\Post\CommentPost;
use Flarum\Post\Post;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\User;
@@ -30,14 +32,14 @@ class GroupMentionsTest extends TestCase
$this->extension('flarum-mentions');
$this->prepareDatabase([
'users' => [
User::class => [
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
],
'discussions' => [
Discussion::class => [
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
],
'posts' => [
Post::class => [
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p>One of the <GROUPMENTION groupname="Mods" id="4">@"Mods"#g4</GROUPMENTION> will look at this</p></r>'],
['id' => 6, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p><GROUPMENTION groupname="OldGroupName" id="100">@"OldGroupName"#g100</GROUPMENTION></p></r>'],
['id' => 7, 'number' => 4, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><p><GROUPMENTION groupname="OldGroupName" id="11">@"OldGroupName"#g11</GROUPMENTION></p></r>'],
@@ -53,7 +55,7 @@ class GroupMentionsTest extends TestCase
['group_id' => Group::MEMBER_ID, 'permission' => 'postWithoutThrottle'],
['group_id' => 9, 'permission' => 'mentionGroups'],
],
'groups' => [
Group::class => [
['id' => 9, 'name_singular' => 'HasPermissionToMentionGroups', 'name_plural' => 'test'],
['id' => 10, 'name_singular' => 'Hidden', 'name_plural' => 'Ninjas', 'icon' => 'fas fa-wrench', 'color' => '#000', 'is_hidden' => 1],
['id' => 11, 'name_singular' => 'Fresh Name', 'name_plural' => 'Fresh Name', 'color' => '#ccc', 'icon' => 'fas fa-users', 'is_hidden' => 0]

View File

@@ -10,9 +10,12 @@
namespace Flarum\Mentions\Tests\integration\api\discussions;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Mentions\Api\LoadMentionedByRelationship;
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
@@ -29,10 +32,10 @@ class ListPostsTest extends TestCase
$this->extension('flarum-mentions');
$this->prepareDatabase([
'discussions' => [
Discussion::class => [
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
],
'posts' => [
Post::class => [
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
['id' => 2, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
['id' => 3, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
@@ -43,7 +46,7 @@ class ListPostsTest extends TestCase
['post_id' => 3, 'mentions_user_id' => 1],
['post_id' => 4, 'mentions_user_id' => 2]
],
'users' => [
User::class => [
$this->normalUser(),
]
]);
@@ -112,10 +115,10 @@ class ListPostsTest extends TestCase
protected function prepareMentionedByData(): void
{
$this->prepareDatabase([
'discussions' => [
Discussion::class => [
['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 12],
],
'posts' => [
Post::class => [
['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
['id' => 102, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
['id' => 103, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>', 'is_private' => 1],

View File

@@ -10,8 +10,11 @@
namespace Flarum\Mentions\Tests\integration\api;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Extend;
use Flarum\Formatter\Formatter;
use Flarum\Post\CommentPost;
use Flarum\Post\Post;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\DisplayName\DriverInterface;
@@ -31,16 +34,16 @@ class PostMentionsTest extends TestCase
$this->extension('flarum-mentions');
$this->prepareDatabase([
'users' => [
User::class => [
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
['id' => 5, 'username' => 'bad_user', 'email' => 'bad_user@machine.local', 'is_email_confirmed' => 1],
],
'discussions' => [
Discussion::class => [
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
['id' => 50, 'title' => __CLASS__, 'is_private' => true, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 1],
],
'posts' => [
Post::class => [
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="TobyFlarum___" id="5" number="2" discussionid="2" username="toby">@tobyuuu#5</POSTMENTION></r>'],
['id' => 5, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="potato" id="4" number="3" discussionid="2" username="potato">@potato#4</POSTMENTION></r>'],
['id' => 6, 'number' => 4, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="i_am_a_deleted_user" id="7" number="5" discussionid="2" username="i_am_a_deleted_user">@"i_am_a_deleted_user"#p7</POSTMENTION></r>'],
@@ -538,6 +541,40 @@ class PostMentionsTest extends TestCase
$this->assertStringContainsString('PostMention', $response['data']['attributes']['contentHtml']);
$this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsPosts->find(11));
}
/**
* @test
*/
public function rendering_post_mention_with_a_post_context_works()
{
/** @var Formatter $formatter */
$formatter = $this->app()->getContainer()->make(Formatter::class);
$post = Post::find(4);
$user = User::find(1);
$xml = $formatter->parse($post->content, $post, $user);
$renderedHtml = $formatter->render($xml, $post);
$this->assertStringContainsString('TOBY$', $renderedHtml);
}
/**
* @test
*/
public function rendering_post_mention_without_a_context_works()
{
/** @var Formatter $formatter */
$formatter = $this->app()->getContainer()->make(Formatter::class);
$post = Post::find(4);
$user = User::find(1);
$xml = $formatter->parse($post->content, null, $user);
$renderedHtml = $formatter->render($xml);
$this->assertStringContainsString('TOBY$', $renderedHtml);
}
}
class CustomOtherDisplayNameDriver implements DriverInterface

View File

@@ -10,10 +10,14 @@
namespace Flarum\Mentions\Tests\integration\api;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Group\Group;
use Flarum\Post\CommentPost;
use Flarum\Post\Post;
use Flarum\Tags\Tag;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\User;
class TagMentionsTest extends TestCase
{
@@ -26,20 +30,20 @@ class TagMentionsTest extends TestCase
$this->extension('flarum-tags', 'flarum-mentions');
$this->prepareDatabase([
'users' => [
User::class => [
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
],
'discussions' => [
Discussion::class => [
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
],
'posts' => [
Post::class => [
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><TAGMENTION id="1" slug="test_old_slug" tagname="TestOldName">#test_old_slug</TAGMENTION></r>'],
['id' => 7, 'number' => 5, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 2021, 'type' => 'comment', 'content' => '<r><TAGMENTION id="3" slug="support" tagname="Support">#deleted_relation</TAGMENTION></r>'],
['id' => 8, 'number' => 6, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><TAGMENTION id="2020" slug="i_am_a_deleted_tag" tagname="i_am_a_deleted_tag">#i_am_a_deleted_tag</TAGMENTION></r>'],
['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><TAGMENTION id="5" slug="laravel">#laravel</TAGMENTION></r>'],
],
'tags' => [
Tag::class => [
['id' => 1, 'name' => 'Test', 'slug' => 'test', 'is_restricted' => 0],
['id' => 2, 'name' => 'Flarum', 'slug' => 'flarum', 'is_restricted' => 0],
['id' => 3, 'name' => 'Support', 'slug' => 'support', 'is_restricted' => 0],

View File

@@ -10,8 +10,10 @@
namespace Flarum\Mentions\Tests\integration\api;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Extend;
use Flarum\Post\CommentPost;
use Flarum\Post\Post;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\DisplayName\DriverInterface;
@@ -31,16 +33,16 @@ class UserMentionsTest extends TestCase
$this->extension('flarum-mentions');
$this->prepareDatabase([
'users' => [
User::class => [
$this->normalUser(),
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
['id' => 5, 'username' => 'bad_user', 'email' => 'bad_user@machine.local', 'is_email_confirmed' => 1],
],
'discussions' => [
Discussion::class => [
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
],
'posts' => [
Post::class => [
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="TobyFlarum___" id="4" username="toby">@tobyuuu</USERMENTION></r>'],
['id' => 6, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="i_am_a_deleted_user" id="2021" username="i_am_a_deleted_user">@"i_am_a_deleted_user"#2021</USERMENTION></r>'],
['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 5, 'type' => 'comment', 'content' => '<r><USERMENTION displayname="Bad &quot;#p6 User" id="5">@"Bad "#p6 User"#5</USERMENTION></r>'],

View File

@@ -12,6 +12,7 @@ namespace Flarum\Nicknames;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Extend;
use Flarum\Nicknames\Access\UserPolicy;
use Flarum\Search\Database\DatabaseSearchDriver;
use Flarum\User\Event\Saving;
use Flarum\User\Search\UserSearcher;
use Flarum\User\User;
@@ -44,15 +45,16 @@ return [
->default('flarum-nicknames.set_on_registration', true)
->default('flarum-nicknames.min', 1)
->default('flarum-nicknames.max', 150)
->serializeToForum('displayNameDriver', 'display_name_driver', null, 'username')
->default('display_name_driver', 'username')
->serializeToForum('displayNameDriver', 'display_name_driver')
->serializeToForum('setNicknameOnRegistration', 'flarum-nicknames.set_on_registration', 'boolval')
->serializeToForum('randomizeUsernameOnRegistration', 'flarum-nicknames.random_username', 'boolval'),
(new Extend\Validator(UserValidator::class))
->configure(AddNicknameValidation::class),
(new Extend\SimpleFlarumSearch(UserSearcher::class))
->setFullTextGambit(NicknameFullTextGambit::class),
(new Extend\SearchDriver(DatabaseSearchDriver::class))
->setFulltext(UserSearcher::class, NicknameFullTextFilter::class),
(new Extend\Policy())
->modelPolicy(User::class, UserPolicy::class),

View File

@@ -1,2 +1,2 @@
(()=>{var e={n:n=>{var a=n&&n.__esModule?()=>n.default:()=>n;return e.d(a,{a}),a},d:(n,a)=>{for(var t in a)e.o(a,t)&&!e.o(n,t)&&Object.defineProperty(n,t,{enumerable:!0,get:a[t]})},o:(e,n)=>Object.prototype.hasOwnProperty.call(e,n),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},n={};(()=>{"use strict";e.r(n);const a=flarum.reg.get("core","admin/app");var t=e.n(a);const r=flarum.reg.get("core","common/components/Alert");var s=e.n(r);const i=flarum.reg.get("core","common/components/Link");var l=e.n(i);t().initializers.add("flarum/nicknames",(()=>{t().extensionData.for("flarum-nicknames").registerSetting((function(){if("nickname"!==t().data.settings.display_name_driver)return m("div",{className:"Form-group"},m(s(),{dismissible:!1},t().translator.trans("flarum-nicknames.admin.wrong_driver",{a:m(l(),{href:t().route("basics")})})))})).registerSetting({setting:"flarum-nicknames.set_on_registration",type:"boolean",label:t().translator.trans("flarum-nicknames.admin.settings.set_on_registration_label")}).registerSetting({setting:"flarum-nicknames.random_username",type:"boolean",label:t().translator.trans("flarum-nicknames.admin.settings.random_username_label"),help:t().translator.trans("flarum-nicknames.admin.settings.random_username_help")}).registerSetting({setting:"flarum-nicknames.unique",type:"boolean",label:t().translator.trans("flarum-nicknames.admin.settings.unique_label")}).registerSetting({setting:"flarum-nicknames.regex",type:"text",label:t().translator.trans("flarum-nicknames.admin.settings.regex_label")}).registerSetting({setting:"flarum-nicknames.min",type:"number",label:t().translator.trans("flarum-nicknames.admin.settings.min_label")}).registerSetting({setting:"flarum-nicknames.max",type:"number",label:t().translator.trans("flarum-nicknames.admin.settings.max_label")}).registerPermission({icon:"fas fa-user-tag",label:t().translator.trans("flarum-nicknames.admin.permissions.edit_own_nickname_label"),permission:"user.editOwnNickname"},"start")}))})(),module.exports=n})();
(()=>{var e={n:n=>{var a=n&&n.__esModule?()=>n.default:()=>n;return e.d(a,{a}),a},d:(n,a)=>{for(var t in a)e.o(a,t)&&!e.o(n,t)&&Object.defineProperty(n,t,{enumerable:!0,get:a[t]})},o:(e,n)=>Object.prototype.hasOwnProperty.call(e,n),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},n={};(()=>{"use strict";e.r(n);const a=flarum.reg.get("core","admin/app");var t=e.n(a);const r=flarum.reg.get("core","common/components/Alert");var s=e.n(r);const i=flarum.reg.get("core","common/components/Link");var l=e.n(i);const o=flarum.reg.get("core","admin/components/BasicsPage");var c=e.n(o);const g=flarum.reg.get("core","common/utils/extractText");var u=e.n(g);const d=flarum.reg.get("core","common/extend");t().initializers.add("flarum/nicknames",(()=>{t().extensionData.for("flarum-nicknames").registerSetting((function(){if("nickname"!==t().data.settings.display_name_driver)return m("div",{className:"Form-group"},m(s(),{dismissible:!1},t().translator.trans("flarum-nicknames.admin.wrong_driver",{a:m(l(),{href:t().route("basics")})})))})).registerSetting({setting:"flarum-nicknames.set_on_registration",type:"boolean",label:t().translator.trans("flarum-nicknames.admin.settings.set_on_registration_label")}).registerSetting({setting:"flarum-nicknames.random_username",type:"boolean",label:t().translator.trans("flarum-nicknames.admin.settings.random_username_label"),help:t().translator.trans("flarum-nicknames.admin.settings.random_username_help")}).registerSetting({setting:"flarum-nicknames.unique",type:"boolean",label:t().translator.trans("flarum-nicknames.admin.settings.unique_label")}).registerSetting({setting:"flarum-nicknames.regex",type:"text",label:t().translator.trans("flarum-nicknames.admin.settings.regex_label")}).registerSetting({setting:"flarum-nicknames.min",type:"number",label:t().translator.trans("flarum-nicknames.admin.settings.min_label")}).registerSetting({setting:"flarum-nicknames.max",type:"number",label:t().translator.trans("flarum-nicknames.admin.settings.max_label")}).registerPermission({icon:"fas fa-user-tag",label:t().translator.trans("flarum-nicknames.admin.permissions.edit_own_nickname_label"),permission:"user.editOwnNickname"},"start"),(0,d.extend)(c().prototype,"driverLocale",(function(e){e.display_name.nickname=u()(t().translator.trans("flarum-nicknames.admin.basics.display_name_driver_options.nickname"))}))}))})(),module.exports=n})();
//# sourceMappingURL=admin.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,9 @@
import app from 'flarum/admin/app';
import Alert from 'flarum/common/components/Alert';
import Link from 'flarum/common/components/Link';
import BasicsPage from 'flarum/admin/components/BasicsPage';
import extractText from 'flarum/common/utils/extractText';
import { extend } from 'flarum/common/extend';
app.initializers.add('flarum/nicknames', () => {
app.extensionData
@@ -55,4 +58,8 @@ app.initializers.add('flarum/nicknames', () => {
},
'start'
);
extend(BasicsPage.prototype, 'driverLocale', function (locale) {
locale.display_name['nickname'] = extractText(app.translator.trans('flarum-nicknames.admin.basics.display_name_driver_options.nickname'));
});
});

View File

@@ -1,10 +1,10 @@
import app from 'flarum/forum/app';
import Modal from 'flarum/common/components/Modal';
import FormModal from 'flarum/common/components/FormModal';
import Button from 'flarum/common/components/Button';
import Stream from 'flarum/common/utils/Stream';
import Form from '@flarum/core/src/common/components/Form';
export default class NicknameModal extends Modal {
export default class NicknameModal extends FormModal {
oninit(vnode) {
super.oninit(vnode);
this.nickname = Stream(app.session.user.displayName());

View File

@@ -1,5 +1,8 @@
flarum-nicknames:
admin:
basics:
display_name_driver_options:
nickname: Nickname
permissions:
edit_own_nickname_label: Edit own nickname
settings:

View File

@@ -9,19 +9,16 @@
namespace Flarum\Nicknames;
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Flarum\Search\GambitInterface;
use Flarum\Search\AbstractFulltextFilter;
use Flarum\Search\Database\DatabaseSearchState;
use Flarum\Search\SearchState;
use Flarum\User\UserRepository;
use Illuminate\Database\Eloquent\Builder;
class NicknameFullTextGambit implements GambitInterface
/**
* @extends AbstractFulltextFilter<DatabaseSearchState>
*/
class NicknameFullTextFilter extends AbstractFulltextFilter
{
public function __construct(
protected UserRepository $users
@@ -37,14 +34,12 @@ class NicknameFullTextGambit implements GambitInterface
->orWhere('nickname', 'like', "{$searchValue}%");
}
public function apply(SearchState $search, string $bit): bool
public function search(SearchState $state, string $value): void
{
$search->getQuery()
$state->getQuery()
->whereIn(
'id',
$this->getUserSearchSubQuery($bit)
$this->getUserSearchSubQuery($value)
);
return true;
}
}

View File

@@ -27,7 +27,7 @@ class UpdateTest extends TestCase
$this->extension('flarum-nicknames');
$this->prepareDatabase([
'users' => [
User::class => [
$this->normalUser(),
],
]);

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) Sami Mazouz
Copyright (c) 2024 Stichting Flarum (Flarum Foundation)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,5 +1,18 @@
# Package Manager
# Extension Manager
*An Experiment.*
The extension manager is a tool that allows you to easily install and manage extensions. It runs [composer](https://getcomposer.org/) under the hood.
Read: https://github.com/flarum/package-manager/wiki
## Security
If admin access is given to untrustworthy users, they can install malicious extensions. Please be careful.
This extension is optional and can be removed for those who prefer to manually manage installs and updates through the command line interface.
## Troubleshooting
If you have many extensions installed, you may run into memory issues when using the extension manager. If this happens, you can use an asynchronous queue that will run the extension manager in the background.
* Simple database queue guide: https://discuss.flarum.org/d/28151-database-queue-the-simplest-queue-even-for-shared-hosting
* (Advanced) Redis queue: https://discuss.flarum.org/d/21873-redis-sessions-cache-queues
You can find detailed logs on the extension manager operations in the `storage/logs/composer` directory. Please include the latest log file when reporting issues in the [Flarum support forum](https://discuss.flarum.org/t/support).

View File

@@ -1,6 +1,6 @@
{
"name": "flarum/package-manager",
"description": "A Flarum Package Manager.",
"name": "flarum/extension-manager",
"description": "An extension manager to install, update and remove extension packages from the interface (Wrapper around composer).",
"keywords": [
"extensions",
"composer",
@@ -18,12 +18,12 @@
}
],
"support": {
"issues": "https://github.com/flarum/package-manager/issues",
"source": "https://github.com/flarum/package-manager"
"issues": "https://github.com/flarum/framework/issues",
"source": "https://github.com/flarum/extension-manager"
},
"require": {
"flarum/core": "^2.0",
"composer/composer": "^2.3"
"composer/composer": "^2.7"
},
"require-dev": {
"flarum/testing": "^2.0",
@@ -31,7 +31,7 @@
},
"extra": {
"flarum-extension": {
"title": "Package Manager",
"title": "Extension Manager",
"icon": {
"name": "fas fa-box-open",
"backgroundColor": "#117187",
@@ -69,12 +69,12 @@
},
"autoload": {
"psr-4": {
"Flarum\\PackageManager\\": "src/"
"Flarum\\ExtensionManager\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Flarum\\PackageManager\\Tests\\": "tests/"
"Flarum\\ExtensionManager\\Tests\\": "tests/"
}
},
"scripts": {

View File

@@ -7,32 +7,26 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager;
namespace Flarum\ExtensionManager;
use Flarum\Extend;
use Flarum\Foundation\Paths;
use Flarum\Frontend\Document;
use Flarum\PackageManager\Exception\ComposerCommandFailedException;
use Flarum\PackageManager\Exception\ComposerRequireFailedException;
use Flarum\PackageManager\Exception\ComposerUpdateFailedException;
use Flarum\PackageManager\Exception\ExceptionHandler;
use Flarum\PackageManager\Exception\MajorUpdateFailedException;
use Flarum\PackageManager\Settings\LastUpdateCheck;
use Flarum\PackageManager\Settings\LastUpdateRun;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Queue\SyncQueue;
return [
(new Extend\Routes('api'))
->post('/package-manager/extensions', 'package-manager.extensions.require', Api\Controller\RequireExtensionController::class)
->patch('/package-manager/extensions/{id}', 'package-manager.extensions.update', Api\Controller\UpdateExtensionController::class)
->delete('/package-manager/extensions/{id}', 'package-manager.extensions.remove', Api\Controller\RemoveExtensionController::class)
->post('/package-manager/check-for-updates', 'package-manager.check-for-updates', Api\Controller\CheckForUpdatesController::class)
->post('/package-manager/why-not', 'package-manager.why-not', Api\Controller\WhyNotController::class)
->post('/package-manager/minor-update', 'package-manager.minor-update', Api\Controller\MinorUpdateController::class)
->post('/package-manager/major-update', 'package-manager.major-update', Api\Controller\MajorUpdateController::class)
->post('/package-manager/global-update', 'package-manager.global-update', Api\Controller\GlobalUpdateController::class)
->get('/package-manager-tasks', 'package-manager.tasks.index', Api\Controller\ListTasksController::class),
->post('/extension-manager/extensions', 'extension-manager.extensions.require', Api\Controller\RequireExtensionController::class)
->patch('/extension-manager/extensions/{id}', 'extension-manager.extensions.update', Api\Controller\UpdateExtensionController::class)
->delete('/extension-manager/extensions/{id}', 'extension-manager.extensions.remove', Api\Controller\RemoveExtensionController::class)
->post('/extension-manager/check-for-updates', 'extension-manager.check-for-updates', Api\Controller\CheckForUpdatesController::class)
->post('/extension-manager/why-not', 'extension-manager.why-not', Api\Controller\WhyNotController::class)
->post('/extension-manager/minor-update', 'extension-manager.minor-update', Api\Controller\MinorUpdateController::class)
->post('/extension-manager/major-update', 'extension-manager.major-update', Api\Controller\MajorUpdateController::class)
->post('/extension-manager/global-update', 'extension-manager.global-update', Api\Controller\GlobalUpdateController::class)
->get('/extension-manager-tasks', 'extension-manager.tasks.index', Api\Controller\ListTasksController::class)
->post('/extension-manager/composer', 'extension-manager.composer', Api\Controller\ConfigureComposerController::class),
(new Extend\Frontend('admin'))
->css(__DIR__.'/less/admin.less')
@@ -40,31 +34,34 @@ return [
->content(function (Document $document) {
$paths = resolve(Paths::class);
$document->payload['flarum-package-manager.writable_dirs'] = is_writable($paths->vendor)
$document->payload['flarum-extension-manager.writable_dirs'] = is_writable($paths->vendor)
&& is_writable($paths->storage)
&& (! file_exists($paths->storage.'/.composer') || is_writable($paths->storage.'/.composer'))
&& is_writable($paths->base.'/composer.json')
&& is_writable($paths->base.'/composer.lock');
$document->payload['flarum-package-manager.using_sync_queue'] = resolve(Queue::class) instanceof SyncQueue;
$document->payload['flarum-extension-manager.using_sync_queue'] = resolve(Queue::class) instanceof SyncQueue;
}),
new Extend\Locales(__DIR__.'/locale'),
(new Extend\Settings())
->default(LastUpdateCheck::key(), json_encode(LastUpdateCheck::default()))
->default(LastUpdateRun::key(), json_encode(LastUpdateRun::default()))
->default('flarum-package-manager.queue_jobs', false),
->default(Settings\LastUpdateCheck::key(), json_encode(Settings\LastUpdateCheck::default()))
->default(Settings\LastUpdateRun::key(), json_encode(Settings\LastUpdateRun::default()))
->default('flarum-extension-manager.queue_jobs', '0')
->default('flarum-extension-manager.minimum_stability', 'stable')
->default('flarum-extension-manager.task_retention_days', 7),
(new Extend\ServiceProvider)
->register(PackageManagerServiceProvider::class),
->register(ExtensionManagerServiceProvider::class),
(new Extend\ErrorHandling)
->handler(ComposerCommandFailedException::class, ExceptionHandler::class)
->handler(ComposerRequireFailedException::class, ExceptionHandler::class)
->handler(ComposerUpdateFailedException::class, ExceptionHandler::class)
->handler(MajorUpdateFailedException::class, ExceptionHandler::class)
->handler(Exception\ComposerCommandFailedException::class, Exception\ExceptionHandler::class)
->handler(Exception\ComposerRequireFailedException::class, Exception\ExceptionHandler::class)
->handler(Exception\ComposerUpdateFailedException::class, Exception\ExceptionHandler::class)
->handler(Exception\MajorUpdateFailedException::class, Exception\ExceptionHandler::class)
->status('extension_already_installed', 409)
->status('extension_not_installed', 409)
->status('no_new_major_version', 409),
->status('no_new_major_version', 409)
->status('extension_not_directly_dependency', 409),
];

View File

@@ -0,0 +1,19 @@
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
import Mithril from 'mithril';
import Stream from 'flarum/common/utils/Stream';
export interface IAuthMethodModalAttrs extends IInternalModalAttrs {
onsubmit: (type: string, host: string, token: string) => void;
type?: string;
host?: string;
token?: string;
}
export default class AuthMethodModal<CustomAttrs extends IAuthMethodModalAttrs = IAuthMethodModalAttrs> extends Modal<CustomAttrs> {
protected type: Stream<string>;
protected host: Stream<string>;
protected token: Stream<string>;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
className(): string;
title(): Mithril.Children;
content(): Mithril.Children;
submit(): void;
}

View File

@@ -0,0 +1,10 @@
import type Mithril from 'mithril';
import ConfigureJson, { IConfigureJson } from './ConfigureJson';
export default class ConfigureAuth extends ConfigureJson<IConfigureJson> {
protected type: string;
title(): Mithril.Children;
className(): string;
content(): Mithril.Children;
submitButton(): Mithril.Children[];
onchange(oldHost: string | null, type: string, host: string, token: string): void;
}

View File

@@ -0,0 +1,14 @@
import type Mithril from 'mithril';
import ConfigureJson, { type IConfigureJson } from './ConfigureJson';
export declare type Repository = {
type: 'composer' | 'vcs' | 'path';
url: string;
};
export default class ConfigureComposer extends ConfigureJson<IConfigureJson> {
protected type: string;
title(): Mithril.Children;
className(): string;
content(): Mithril.Children;
submitButton(): Mithril.Children[];
onchange(repository: Repository, name: string): void;
}

View File

@@ -0,0 +1,24 @@
import type Mithril from 'mithril';
import Component, { type ComponentAttrs } from 'flarum/common/Component';
import { CommonSettingsItemOptions, type SettingsComponentOptions } from '@flarum/core/src/admin/components/AdminPage';
import type ItemList from 'flarum/common/utils/ItemList';
import Stream from 'flarum/common/utils/Stream';
export interface IConfigureJson extends ComponentAttrs {
buildSettingComponent: (entry: ((this: this) => Mithril.Children) | SettingsComponentOptions) => Mithril.Children;
}
export default abstract class ConfigureJson<CustomAttrs extends IConfigureJson = IConfigureJson> extends Component<CustomAttrs> {
protected settings: Record<string, Stream<any>>;
protected initialSettings: Record<string, any> | null;
protected loading: boolean;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
protected abstract type: string;
abstract title(): Mithril.Children;
abstract content(): Mithril.Children;
className(): string;
view(): Mithril.Children;
submitButton(): Mithril.Children[];
customSettingComponents(): ItemList<(attributes: CommonSettingsItemOptions) => Mithril.Children>;
setting(key: string): Stream<any>;
submit(readOnly: boolean): void;
isDirty(): boolean;
}

View File

@@ -5,7 +5,10 @@ import { UpdatedPackage } from '../states/ControlSectionState';
export interface ExtensionItemAttrs extends ComponentAttrs {
extension: Extension;
updates: UpdatedPackage;
onClickUpdate: CallableFunction;
onClickUpdate: CallableFunction | {
soft: CallableFunction;
hard: CallableFunction;
};
whyNotWarning?: boolean;
isCore?: boolean;
updatable?: boolean;

View File

@@ -0,0 +1,18 @@
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
import Mithril from 'mithril';
import Stream from 'flarum/common/utils/Stream';
import { type Repository } from './ConfigureComposer';
export interface IRepositoryModalAttrs extends IInternalModalAttrs {
onsubmit: (repository: Repository, key: string) => void;
name?: string;
repository?: Repository;
}
export default class RepositoryModal<CustomAttrs extends IRepositoryModalAttrs = IRepositoryModalAttrs> extends Modal<CustomAttrs> {
protected name: Stream<string>;
protected repository: Stream<Repository>;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
className(): string;
title(): Mithril.Children;
content(): Mithril.Children;
submit(): void;
}

View File

@@ -2,5 +2,7 @@ import type Mithril from 'mithril';
import ExtensionPage, { ExtensionPageAttrs } from 'flarum/admin/components/ExtensionPage';
import ItemList from 'flarum/common/utils/ItemList';
export default class SettingsPage extends ExtensionPage {
content(): JSX.Element;
sections(vnode: Mithril.VnodeDOM<ExtensionPageAttrs, this>): ItemList<unknown>;
onsaved(): void;
}

View File

@@ -1,5 +1,5 @@
/// <reference types="mithril" />
/// <reference types="flarum/@types/translator-icu-rich" />
/// <reference types="@flarum/core/dist-typings/@types/translator-icu-rich" />
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
import Task from '../models/Task';
interface TaskOutputModalAttrs extends IInternalModalAttrs {

View File

@@ -1,4 +1,4 @@
/// <reference types="flarum/@types/translator-icu-rich" />
/// <reference types="@flarum/core/dist-typings/@types/translator-icu-rich" />
import type Mithril from 'mithril';
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
export interface WhyNotModalAttrs extends IInternalModalAttrs {

View File

@@ -6,6 +6,7 @@ export default class Task extends Model {
command(): string;
package(): string;
output(): string;
guessedCause(): string;
createdAt(): Date | null | undefined;
startedAt(): Date;
finishedAt(): Date;

View File

@@ -9,6 +9,8 @@ export declare type UpdatedPackage = {
'latest-minor': string | null;
'latest-major': string | null;
'latest-status': string;
'required-as': string;
'direct-dependency': boolean;
description: string;
};
export declare type ComposerUpdates = {
@@ -31,7 +33,7 @@ export declare type LastUpdateRun = {
} & {
limitedPackages: () => string[];
};
export declare type LoadingTypes = UpdaterLoadingTypes | InstallerLoadingTypes | MajorUpdaterLoadingTypes;
export declare type LoadingTypes = UpdaterLoadingTypes | InstallerLoadingTypes | MajorUpdaterLoadingTypes | 'queued-action';
export declare type CoreUpdate = {
package: UpdatedPackage;
extension: Extension;
@@ -45,13 +47,17 @@ export default class ControlSectionState {
get lastUpdateRun(): LastUpdateRun;
constructor();
isLoading(name?: LoadingTypes): boolean;
isLoadingOtherThan(name: LoadingTypes): boolean;
hasOperationRunning(): boolean;
setLoading(name: LoadingTypes): void;
requirePackage(data: any): void;
checkForUpdates(): void;
updateCoreMinor(): void;
updateExtension(extension: Extension): void;
updateExtension(extension: Extension, updateMode: 'soft' | 'hard'): void;
updateGlobally(): void;
formatExtensionUpdates(lastUpdateCheck: LastUpdateCheck): Extension[];
formatCoreUpdate(lastUpdateCheck: LastUpdateCheck): CoreUpdate | null;
majorUpdate({ dryRun }: {
dryRun: boolean;
}): void;
}
export {};

View File

@@ -0,0 +1,6 @@
import QueueState from './QueueState';
import ControlSectionState from './ControlSectionState';
export default class ExtensionManagerState {
queue: QueueState;
control: ControlSectionState;
}

View File

@@ -1,11 +1,12 @@
import Task from '../models/Task';
import { ApiQueryParamsPlural } from 'flarum/common/Store';
export default class QueueState {
private polling;
private tasks;
private limit;
private offset;
private total;
load(params?: ApiQueryParamsPlural): Promise<import("flarum/common/Store").ApiResponsePlural<Task>>;
load(params?: ApiQueryParamsPlural, actionTaken?: boolean): Promise<Task[]>;
getItems(): Task[] | null;
getTotalPages(): number;
pageNumber(): number;
@@ -13,4 +14,6 @@ export default class QueueState {
hasNext(): boolean;
prev(): void;
next(): void;
pollQueue(actionTaken?: boolean): void;
hasPending(): boolean;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
{
"name": "@flarum/package-manager",
"name": "@flarum/extension-manager",
"version": "0.0.0",
"private": true,
"prettier": "@flarum/prettier-config",

View File

@@ -0,0 +1,88 @@
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
import Mithril from 'mithril';
import app from 'flarum/admin/app';
import Select from 'flarum/common/components/Select';
import Stream from 'flarum/common/utils/Stream';
import Button from 'flarum/common/components/Button';
import extractText from 'flarum/common/utils/extractText';
export interface IAuthMethodModalAttrs extends IInternalModalAttrs {
onsubmit: (type: string, host: string, token: string) => void;
type?: string;
host?: string;
token?: string;
}
export default class AuthMethodModal<CustomAttrs extends IAuthMethodModalAttrs = IAuthMethodModalAttrs> extends Modal<CustomAttrs> {
protected type!: Stream<string>;
protected host!: Stream<string>;
protected token!: Stream<string>;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.type = Stream(this.attrs.type || 'bearer');
this.host = Stream(this.attrs.host || '');
this.token = Stream(this.attrs.token || '');
}
className(): string {
return 'AuthMethodModal Modal--small';
}
title(): Mithril.Children {
const context = this.attrs.host ? 'edit' : 'add';
return app.translator.trans(`flarum-extension-manager.admin.auth_config.${context}_label`);
}
content(): Mithril.Children {
const types = {
'github-oauth': app.translator.trans('flarum-extension-manager.admin.auth_config.types.github-oauth'),
'gitlab-oauth': app.translator.trans('flarum-extension-manager.admin.auth_config.types.gitlab-oauth'),
'gitlab-token': app.translator.trans('flarum-extension-manager.admin.auth_config.types.gitlab-token'),
bearer: app.translator.trans('flarum-extension-manager.admin.auth_config.types.bearer'),
};
return (
<div className="Modal-body">
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.type_label')}</label>
<Select options={types} value={this.type()} onchange={this.type} />
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.host_label')}</label>
<input
className="FormControl"
bidi={this.host}
placeholder={app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.host_placeholder')}
/>
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.token_label')}</label>
<textarea
className="FormControl"
oninput={(e: InputEvent) => this.token((e.target as HTMLTextAreaElement).value)}
rows="6"
placeholder={
this.token().startsWith('unchanged:')
? extractText(app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.unchanged_token_placeholder'))
: ''
}
>
{this.token().startsWith('unchanged:') ? '' : this.token()}
</textarea>
</div>
<div className="Form-group">
<Button className="Button Button--primary" onclick={this.submit.bind(this)}>
{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.submit_button')}
</Button>
</div>
</div>
);
}
submit() {
this.attrs.onsubmit(this.type(), this.host(), this.token());
this.hide();
}
}

View File

@@ -0,0 +1,120 @@
import app from 'flarum/admin/app';
import type Mithril from 'mithril';
import ConfigureJson, { IConfigureJson } from './ConfigureJson';
import Button from 'flarum/common/components/Button';
import AuthMethodModal from './AuthMethodModal';
import extractText from 'flarum/common/utils/extractText';
export default class ConfigureAuth extends ConfigureJson<IConfigureJson> {
protected type = 'auth';
title(): Mithril.Children {
return app.translator.trans('flarum-extension-manager.admin.auth_config.title');
}
className(): string {
return 'ConfigureAuth';
}
content(): Mithril.Children {
const authSettings = Object.keys(this.settings);
const hasAuthSettings =
authSettings.length &&
authSettings.every((type) => {
const data = this.settings[type]();
return Array.isArray(data) ? data.length : Object.keys(data).length;
});
return (
<div className="ExtensionManager-SettingsGroups-content">
{hasAuthSettings ? (
authSettings.map((type) => {
const hosts = this.settings[type]();
return (
<div className="Form-group">
<label>{app.translator.trans(`flarum-extension-manager.admin.auth_config.types.${type}`)}</label>
<div className="ConfigureAuth-hosts">
{Object.keys(hosts).map((host) => {
const data = hosts[host] as string | Record<string, string>;
return (
<div className="ButtonGroup ButtonGroup--full">
<Button
className="Button"
icon="fas fa-key"
onclick={() =>
app.modal.show(AuthMethodModal, {
type,
host,
token: data,
onsubmit: this.onchange.bind(this, host),
})
}
>
{host}
</Button>
<Button
className="Button Button--icon"
icon="fas fa-trash"
aria-label={app.translator.trans('flarum-extension-manager.admin.auth_config.delete_label')}
onclick={() => {
if (confirm(extractText(app.translator.trans('flarum-extension-manager.admin.auth_config.delete_confirmation')))) {
const newType = { ...this.setting(type)() };
delete newType[host];
if (Object.keys(newType).length) {
this.setting(type)(newType);
} else {
delete this.settings[type];
}
}
}}
/>
</div>
);
})}
</div>
</div>
);
})
) : (
<span className="helpText">{app.translator.trans('flarum-extension-manager.admin.auth_config.no_auth_methods_configured')}</span>
)}
</div>
);
}
submitButton(): Mithril.Children[] {
const items = super.submitButton();
items.push(
<Button
className="Button"
loading={this.loading}
onclick={() =>
app.modal.show(AuthMethodModal, {
onsubmit: this.onchange.bind(this, null),
})
}
>
{app.translator.trans('flarum-extension-manager.admin.auth_config.add_label')}
</Button>
);
return items;
}
onchange(oldHost: string | null, type: string, host: string, token: string) {
const data = { ...this.setting(type)() };
if (oldHost) {
delete data[oldHost];
}
data[host] = token;
this.setting(type)(data);
}
}

View File

@@ -0,0 +1,115 @@
import app from 'flarum/admin/app';
import type Mithril from 'mithril';
import ConfigureJson, { type IConfigureJson } from './ConfigureJson';
import Button from 'flarum/common/components/Button';
import extractText from 'flarum/common/utils/extractText';
import RepositoryModal from './RepositoryModal';
export type Repository = {
type: 'composer' | 'vcs' | 'path';
url: string;
};
export default class ConfigureComposer extends ConfigureJson<IConfigureJson> {
protected type = 'composer';
title(): Mithril.Children {
return app.translator.trans('flarum-extension-manager.admin.composer.title');
}
className(): string {
return 'ConfigureComposer';
}
content(): Mithril.Children {
return (
<div className="Form ExtensionManager-SettingsGroups-content">
{this.attrs.buildSettingComponent.call(this, {
setting: 'minimum-stability',
label: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.label'),
help: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.help'),
type: 'select',
options: {
stable: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.stable'),
RC: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.rc'),
beta: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.beta'),
alpha: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.alpha'),
dev: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.dev'),
},
})}
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.composer.repositories.label')}</label>
<div className="helpText">{app.translator.trans('flarum-extension-manager.admin.composer.repositories.help')}</div>
<div className="ConfigureComposer-repositories">
{Object.keys(this.setting('repositories')() || {}).map((name) => {
const repository = this.setting('repositories')()[name] as Repository;
return (
<div className="ButtonGroup ButtonGroup--full">
<Button
className="Button"
icon={
{
composer: 'fas fa-cubes',
vcs: 'fas fa-code-branch',
path: 'fas fa-folder',
}[repository.type]
}
onclick={() =>
app.modal.show(RepositoryModal, {
name,
repository,
onsubmit: (repository: Repository, newName: string) => {
const repositories = this.setting('repositories')();
delete repositories[name];
this.setting('repositories')(repositories);
this.onchange(repository, newName);
},
})
}
>
{name} ({repository.type})
</Button>
<Button
className="Button Button--icon"
icon="fas fa-trash"
aria-label={app.translator.trans('flarum-extension-manager.admin.composer.delete_repository_label')}
onclick={() => {
if (confirm(extractText(app.translator.trans('flarum-extension-manager.admin.composer.delete_repository_confirmation')))) {
const repositories = { ...this.setting('repositories')() };
delete repositories[name];
this.setting('repositories')(repositories);
}
}}
/>
</div>
);
})}
</div>
</div>
</div>
);
}
submitButton(): Mithril.Children[] {
const items = super.submitButton();
items.push(
<Button className="Button" onclick={() => app.modal.show(RepositoryModal, { onsubmit: this.onchange.bind(this) })}>
{app.translator.trans('flarum-extension-manager.admin.composer.add_repository_label')}
</Button>
);
return items;
}
onchange(repository: Repository, name: string) {
this.setting('repositories')({
...this.setting('repositories')(),
[name]: repository,
});
}
}

View File

@@ -0,0 +1,94 @@
import app from 'flarum/admin/app';
import type Mithril from 'mithril';
import Component, { type ComponentAttrs } from 'flarum/common/Component';
import { CommonSettingsItemOptions, type SettingsComponentOptions } from '@flarum/core/src/admin/components/AdminPage';
import AdminPage from 'flarum/admin/components/AdminPage';
import type ItemList from 'flarum/common/utils/ItemList';
import Stream from 'flarum/common/utils/Stream';
import Button from 'flarum/common/components/Button';
import classList from 'flarum/common/utils/classList';
export interface IConfigureJson extends ComponentAttrs {
buildSettingComponent: (entry: ((this: this) => Mithril.Children) | SettingsComponentOptions) => Mithril.Children;
}
export default abstract class ConfigureJson<CustomAttrs extends IConfigureJson = IConfigureJson> extends Component<CustomAttrs> {
protected settings: Record<string, Stream<any>> = {};
protected initialSettings: Record<string, any> | null = null;
protected loading: boolean = false;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.submit(true);
}
protected abstract type: string;
abstract title(): Mithril.Children;
abstract content(): Mithril.Children;
className(): string {
return '';
}
view(): Mithril.Children {
return (
<div className={classList('FormSection', this.className())}>
<label>{this.title()}</label>
{this.content()}
<div className="Form-group Form-controls">{this.submitButton()}</div>
</div>
);
}
submitButton(): Mithril.Children[] {
return [
<Button className="Button Button--primary" loading={this.loading} onclick={() => this.submit(false)} disabled={!this.isDirty()}>
{app.translator.trans('core.admin.settings.submit_button')}
</Button>,
];
}
customSettingComponents(): ItemList<(attributes: CommonSettingsItemOptions) => Mithril.Children> {
return AdminPage.prototype.customSettingComponents();
}
setting(key: string) {
return this.settings[key] ?? (this.settings[key] = Stream());
}
submit(readOnly: boolean) {
this.loading = true;
const configuration: any = {};
Object.keys(this.settings).forEach((key) => {
configuration[key] = this.settings[key]();
});
app
.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/extension-manager/composer',
body: {
type: this.type,
data: readOnly ? null : configuration,
},
})
.then(({ data }: any) => {
Object.keys(data).forEach((key) => {
this.settings[key] = Stream(data[key]);
});
this.initialSettings = Array.isArray(data) ? {} : data;
})
.finally(() => {
this.loading = false;
m.redraw();
});
}
isDirty() {
return JSON.stringify(this.initialSettings) !== JSON.stringify(this.settings);
}
}

View File

@@ -6,6 +6,7 @@ import { ComponentAttrs } from 'flarum/common/Component';
import Installer from './Installer';
import Updater from './Updater';
import Mithril from 'mithril';
import Form from 'flarum/common/components/Form';
export default class ControlSection extends Component<ComponentAttrs> {
oninit(vnode: Mithril.Vnode<ComponentAttrs, this>) {
@@ -14,22 +15,22 @@ export default class ControlSection extends Component<ComponentAttrs> {
view() {
return (
<div className="ExtensionPage-permissions PackageManager-controlSection">
<div className="ExtensionPage-permissions ExtensionManager-controlSection">
<div className="ExtensionPage-permissions-header">
<div className="container">
<h2 className="ExtensionTitle">{app.translator.trans('flarum-package-manager.admin.sections.control.title')}</h2>
<h2 className="ExtensionTitle">{app.translator.trans('flarum-extension-manager.admin.sections.control.title')}</h2>
</div>
</div>
<div className="container">
{app.data['flarum-package-manager.writable_dirs'] ? (
<>
{app.data['flarum-extension-manager.writable_dirs'] ? (
<Form>
<Installer />
<Updater />
</>
</Form>
) : (
<div className="Form-group">
<Alert type="warning" dismissible={false}>
{app.translator.trans('flarum-package-manager.admin.file_permissions')}
{app.translator.trans('flarum-extension-manager.admin.file_permissions')}
</Alert>
</div>
)}

Some files were not shown because too many files have changed in this diff Show More