Compare commits

..

91 Commits

Author SHA1 Message Date
Simon
f19007f424
Merge commit from fork
Some checks failed
Backend Tests / run (push) Has been cancelled
Frontend Workflow / run (push) Has been cancelled
Static Code Analysis / run (push) Has been cancelled
2025-04-18 09:45:41 +01:00
Robert Korulczyk
15112c2f40
Sanitize page in Tag (#4170) 2025-04-18 09:45:21 +01:00
flarum-bot
00ef0dd9d0 Bundled output for commit cae706a638944428cd248e8618e5e9a38a0c7347
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-04-18 08:40:38 +00:00
Sami Mazouz
cae706a638
feat: advanced admin registry extenders (#4209)
* feat: advanced admin registry extenders
* fix: admin extender execution order
2025-04-18 09:38:00 +01:00
flarum-bot
2be1932e54 Bundled output for commit 2339c23aae08b1d963c841b9fcb7012c65a60578
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-04-18 08:14:42 +00:00
Davide Iadeluca
2339c23aae
refactor(core): improve extensibility of AppearancePage (#4200) 2025-04-18 09:12:10 +01:00
StyleCI Bot
2b08c30a22
Apply fixes from StyleCI
Some checks failed
Backend Tests / run (push) Has been cancelled
Static Code Analysis / run (push) Has been cancelled
Frontend Workflow / run (push) Has been cancelled
2025-04-11 09:07:58 +00:00
Sami Mazouz
fa88731fe1
chore: increase composer job timeout 2025-04-11 10:07:17 +01:00
Daniël Klabbers
65d8c16580
fix: issue with smtp non-tls connections (#4203)
Some checks failed
Static Code Analysis / run (push) Has been cancelled
Backend Tests / run (push) Has been cancelled
Frontend Workflow / run (push) Has been cancelled
2025-02-27 18:34:31 +01:00
flarum-bot
1a206ff658 Bundled output for commit 0ca99dcba5f116e499072b3fcb9c8564a80b5a5c
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-02-22 09:53:35 +00:00
Davide Iadeluca
0ca99dcba5
feat: improve extensibility of WelcomeHero (#4199) 2025-02-22 10:50:56 +01:00
Davide Iadeluca
12fc3aeec2
feat(tags): improve extensibility of TagHero (#4198) 2025-02-22 10:49:44 +01:00
flarum-bot
b07b310fdf Bundled output for commit e2221b5f748d1e27cad7216ca2e212fc78227083
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-02-21 08:48:16 +00:00
Davide Iadeluca
e2221b5f74
feat: allow extending PostPreview content (#4197) 2025-02-21 09:45:36 +01:00
flarum-bot
f54a5200cf Bundled output for commit 236a8e9e0ac84bc3de90009dbb7ca745bb2a1280
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-02-21 08:43:28 +00:00
Davide Iadeluca
236a8e9e0a
feat: improve extensibility of PostMeta component (#4196)
* refactor(core): create new method for selecting permalink

* refactor(core): improve extensibility of `PostMeta`

* chore: dummy commit
2025-02-21 09:40:44 +01:00
Davide Iadeluca
7feab89cca
fix(a11y): change starting position of aria-posinset (#4191) 2025-02-21 09:38:06 +01:00
flarum-bot
bbc4b6dd13 Bundled output for commit 561e22784a547c8aa92120e0972a9cc97ac21645
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-02-14 17:55:17 +00:00
Sami Mazouz
561e22784a
feat(messages): messages page extensible content 2025-02-14 18:49:57 +01:00
flarum-bot
469127ccf3 Bundled output for commit 80c116b3862d35c880ccfcf528a410792f1ae64a
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-02-14 10:04:16 +00:00
Sami Mazouz
80c116b386
fix(regression): broken styling 2025-02-14 10:59:28 +01:00
flarum-bot
b74c7f9746 Bundled output for commit b7bab2811d86a25f60f964e42be2b722578b52e0
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-02-14 09:49:16 +00:00
Davide Iadeluca
b7bab2811d
feat: actions dropdown in admin user list (#4188) 2025-02-14 10:46:40 +01:00
flarum-bot
3cbc7f4de1 Bundled output for commit 1b9ff2b6fa90a9c991b6e1d9ab5bd959802bd099
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-02-14 09:02:41 +00:00
Sami Mazouz
1b9ff2b6fa
fix(phpstan): incompatibility with recent updates 2025-02-14 09:59:54 +01:00
flarum-bot
d02a924bb8 Bundled output for commit 973f4f6f6ba8574b9d56674df94a02f060464ca4
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-02-14 08:35:40 +00:00
Sami Mazouz
973f4f6f6b
chore: render after first post items once 2025-02-14 09:32:53 +01:00
flarum-bot
9977d491cf Bundled output for commit 9758592daae3710c44aa0d754e2d4d2d666cc5d3
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-02-14 08:29:43 +00:00
Davide Iadeluca
9758592daa
feat: make it easier to add content after the first post (#4186) 2025-02-14 09:27:10 +01:00
flarum-bot
60feaa0184 Bundled output for commit 5557bf82d34cdf4dec9deb14b5fbd94ff2a01b83
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-02-14 08:27:00 +00:00
Sami Mazouz
5557bf82d3
chore: minor changes 2025-02-14 09:24:13 +01:00
flarum-bot
090fd4dea5 Bundled output for commit ecb23a64fcde5d77970c69b5654ca72eb3335f46
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-02-14 08:21:40 +00:00
Davide Iadeluca
ecb23a64fc
feat: reusable component for showing IP address (#4187) 2025-02-14 09:19:01 +01:00
flarum-bot
7ba768bf68 Bundled output for commit f13dc058667540ce14f58eb25eef151ba14f524b
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-02-11 19:28:15 +00:00
Davide Iadeluca
f13dc05866
fix: change condition when unread label is shown in Scrubber (#4185) 2025-02-11 20:25:26 +01:00
Davide Iadeluca
a34a5d4d62
fix: resolve a11y warnings in Admin Frontend (#4184) 2025-02-11 20:24:44 +01:00
Davide Iadeluca
79e969778e
fix: return empty object if selected mail driver is unavailable (#4183) 2025-02-11 20:23:50 +01:00
Davide Iadeluca
41d62b8c82
feat: improve extensibility of IndexPage (#4182) 2025-02-11 20:23:02 +01:00
Davide Iadeluca
5dc94bf4e8
feat: allow labels of PostStreamScrubber to be customized (#4181) 2025-02-11 20:21:18 +01:00
flarum-bot
fdaf09752c Bundled output for commit 975c2c936f5265be28e8d76e4057c30200c6b2b9
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-02-08 18:06:58 +00:00
Sami Mazouz
975c2c936f
feat(pm): delete own messages (#4180) 2025-02-08 19:04:20 +01:00
Sami Mazouz
ce5feca140
fix(em): skip incompatible extension updates (#4177) 2025-02-08 19:01:09 +01:00
Sami Mazouz
55cd0850c7
perf: optimize querying post index (#4178) 2025-02-08 18:35:44 +01:00
flarum-bot
6b31a47f05 Bundled output for commit db1e36d545aa0d483620b52ababd79ef1b2aa27c
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-02-08 17:33:04 +00:00
Sami Mazouz
db1e36d545
feat(pm): messages anchor link (#4175) 2025-02-08 18:30:35 +01:00
Davide Iadeluca
333bbb11e2
fix(webpack): chunk module path checking fails with dotted directories (#4179)
Resolves issues when the path contains unexpected periods
2025-02-08 17:50:12 +01:00
flarum-bot
863d6526df Bundled output for commit 97e56af2cd8e97e4ef10235d3e584d0def2afffc
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-31 12:28:10 +00:00
Sami Mazouz
97e56af2cd
fix: visual bugs 2025-01-31 13:25:26 +01:00
flarum-bot
21da7758af Bundled output for commit 89ff98444640b13ffbd79faf91f1363dafbc5d6b
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-31 12:00:13 +00:00
Sami Mazouz
89ff984446
fix: messages inconsistencies (#4174)
* fix: messages inconsistencies

* fix

* chore: message at the page

* fix: permission grid styling broken

* fix
2025-01-31 12:57:45 +01:00
flarum-bot
7136ad01d5 Bundled output for commit 875fd241b7da7b82083b43d123fc64ec8e1125c9
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-31 08:20:25 +00:00
Sami Mazouz
875fd241b7
fix: messages UI/UX improvement (#4173) 2025-01-31 09:17:51 +01:00
Davide Iadeluca
962d95746d
feat: make search debounce time extensible (#4172) 2025-01-31 09:17:21 +01:00
Sami Mazouz
0b995b96ef
fix: suspended_until serialized as date instead of datetime (#4169) 2025-01-31 09:15:46 +01:00
Sami Mazouz
76d8ea505e
fix: sendmail driver fails (#4168) 2025-01-31 09:12:41 +01:00
Sami Mazouz
cf7ef48906
fix: prevent users from seeing their own flags (#4167) 2025-01-31 09:12:12 +01:00
flarum-bot
670aa2e236 Bundled output for commit ac6133a768ef94c8ea8827569a5dfaf11f437a33
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-17 08:49:10 +00:00
Sami Mazouz
ac6133a768
chore: v2.0.0-beta.2 2025-01-17 09:44:27 +01:00
flarum-bot
48ec73f86e Bundled output for commit 00426c85e38efc91554af33644b088e72b3b3c1b
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-12 12:06:04 +00:00
Sami Mazouz
00426c85e3
chore: extensibility improvements 2025-01-12 13:02:07 +01:00
Sami Mazouz
33121ed1cc
fix: mistakenly removed code
Some checks failed
Backend Tests / run (push) Has been cancelled
Frontend Workflow / run (push) Has been cancelled
Static Code Analysis / run (push) Has been cancelled
2025-01-11 10:09:46 +01:00
flarum-bot
b2f6b4cf88 Bundled output for commit 1cd644d27feb4eeea5cbaedd009a3af2643af396
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-10 18:51:42 +00:00
Sami Mazouz
1cd644d27f
fix: conditional renders 0 2025-01-10 19:48:56 +01:00
Sami Mazouz
68faca4d5f
chore: flarum-webpack-config 3.0.1 2025-01-10 16:55:00 +01:00
flarum-bot
603140a0b1 Bundled output for commit 36d079560776cba31c11e2abea0686180f132713
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-10 15:51:01 +00:00
Sami Mazouz
36d0795607
fix: custom styles from 1.x can crash the app (#4159) 2025-01-10 16:48:24 +01:00
Sami Mazouz
a7d584f8e1
fix(webpack): produces incorrect ext namespace 2025-01-10 16:47:50 +01:00
Sami Mazouz
958dec5944
fix: invisible dropdown text when header is colored
Some checks are pending
Backend Tests / run (push) Waiting to run
Frontend Workflow / run (push) Waiting to run
Static Code Analysis / run (push) Waiting to run
2025-01-10 09:58:07 +01:00
flarum-bot
55aa8a7723 Bundled output for commit e3943eebba4a9b8120d1c22ef5ca2280dda52fe8
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-05 11:02:16 +00:00
Sami Mazouz
e3943eebba
fix: discussion page renders before loading is finished (#4158) 2025-01-05 11:59:45 +01:00
Sami Mazouz
101ff98822
fix: select input cuts off (#4157)
Some checks failed
Backend Tests / run (push) Has been cancelled
Frontend Workflow / run (push) Has been cancelled
Static Code Analysis / run (push) Has been cancelled
2025-01-05 10:36:26 +01:00
flarum-bot
6a9ce6f8e8 Bundled output for commit 23666660917628699ab2d33f877fdba938b08a24
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-05 09:34:10 +00:00
Sami Mazouz
2366666091
fix: discussion posts not always properly loaded (#4156) 2025-01-05 10:31:42 +01:00
flarum-bot
8c9a772635 Bundled output for commit 9d611d1ee2aad142461ed1bd1d17a6455fb14b73
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-04 10:08:20 +00:00
Sami Mazouz
9d611d1ee2
fix(em): incorrect extension compatibility check (#4155) 2025-01-04 11:05:38 +01:00
Sami Mazouz
e73ffee9e7
fix: fixed side nav missing top spacing (#4147) 2025-01-04 10:32:25 +01:00
flarum-bot
1b18f57353 Bundled output for commit 9ce03c6ec8cf44107c593db1bd3a33941e33541e
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-03 16:20:34 +00:00
Sami Mazouz
9ce03c6ec8
fix: use correct human time format key (#4154) 2025-01-03 17:18:02 +01:00
Sami Mazouz
1bdfb66f6c
fix: tag selection icon alignment (#4153) 2025-01-03 17:06:44 +01:00
Sami Mazouz
a3461a8020
fix: bad modal alert text alignment (#4152) 2025-01-03 16:29:53 +01:00
flarum-bot
da8ebe439b Bundled output for commit 5d26485c04099ab207648736fc3bf21318eef733
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-03 14:19:07 +00:00
Sami Mazouz
5d26485c04
fix: code split fails with common module (#4151) 2025-01-03 15:16:37 +01:00
flarum-bot
38e1b49cc1 Bundled output for commit 0c40e19cdf6a313454c986c67e15363b6363f36b
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-03 13:20:33 +00:00
Sami Mazouz
0c40e19cdf
fix: unexpected subscription breaks rendering (#4150) 2025-01-03 14:17:45 +01:00
flarum-bot
6d1e90c969 Bundled output for commit 3294941226da644f52fd8baa313b8a872e65b6a2
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-03 12:56:32 +00:00
Sami Mazouz
3294941226
fix: composer no longer autofocusing (#4149) 2025-01-03 13:53:59 +01:00
flarum-bot
87fa4a32dd Bundled output for commit e08a987842bb4330fa563007393bcba06038f585
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-01-03 10:44:03 +00:00
Sami Mazouz
e08a987842
fix: lazy module import always returns default module (#4148) 2025-01-03 11:41:28 +01:00
Sami Mazouz
9fe17b3c24
perf(mentions): only access related mentions if loaded
Some checks failed
Backend Tests / run (push) Has been cancelled
Frontend Workflow / run (push) Has been cancelled
Static Code Analysis / run (push) Has been cancelled
2024-12-13 13:06:36 +01:00
Sami Mazouz
4dce4d40a3
fix(regression): phpstan errors 2024-12-13 13:06:08 +01:00
Sami Mazouz
a81d13e26c
fix: beta.1 early bugs
Some checks are pending
Backend Tests / run (push) Waiting to run
Frontend Workflow / run (push) Waiting to run
Static Code Analysis / run (push) Waiting to run
2024-12-12 21:27:57 +01:00
237 changed files with 2102 additions and 659 deletions

View File

@ -18,7 +18,7 @@ trim_trailing_whitespace = false
[*.{php,xml,json}]
indent_size = 4
[{tsconfig.json,prettierrc.json}]
[{tsconfig.json,prettierrc.json,package.json}]
indent_size = 2
[*.neon]

View File

@ -1,5 +1,29 @@
# Changelog
## [v2.0.0-beta.2](https://github.com/flarum/framework/compare/v2.0.0-beta.1...v2.0.0-beta.2)
### Fixed
- (em) incorrect extension compatibility check [#4155]
- (webpack) produces incorrect ext namespace (a7d584f8e1ec650035dafd660a70586d1d0d6bb9)
- bad modal alert text alignment [#4152]
- beta.1 early bugs (a81d13e26c1c2191859493de2ad45a515ad07b90)
- code split fails with common module [#4151]
- composer no longer autofocusing [#4149]
- conditional renders 0 (1cd644d27feb4eeea5cbaedd009a3af2643af396)
- custom styles from 1.x can crash the app [#4159]
- discussion page renders before loading is finished [#4158]
- discussion posts not always properly loaded [#4156]
- fixed side nav missing top spacing [#4147]
- invisible dropdown text when header is colored (958dec594486cbc14cf8f922db324a8ffc0245e3)
- lazy module import always returns default module [#4148]
- mistakenly removed code (33121ed1cc260bf967f0b8c4d10ab5099410bac0)
- select input cuts off [#4157]
- tag selection icon alignment [#4153]
- unexpected subscription breaks rendering [#4150]
- use correct human time format key [#4154]
### Changed
- (mentions) only access related mentions if loaded (9fe17b3c24c5b9236e419a00c1230b2994b8c009)
- extensibility improvements (00426c85e38efc91554af33644b088e72b3b3c1b)
## [v2.0.0-beta.1](https://github.com/flarum/framework/compare/v1.8.9...v2.0.0-beta.1)
### Changed
- php 8.4 [#4103]

View File

@ -171,7 +171,7 @@
"mockery/mockery": "^1.5",
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^1.10.0",
"larastan/larastan": "2.9.12",
"larastan/larastan": "2.9.14",
"symfony/var-dumper": "^7.0",
"flarum/testing-tests": "*@dev"
},

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1",
"flarum/core": "^2.0.0-beta.2",
"flarum/approval": "^2.0"
},
"autoload": {

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1",
"flarum/core": "^2.0.0-beta.2",
"flarum/flags": "^2.0"
},
"autoload": {

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"autoload": {
"psr-4": {

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"autoload": {
"psr-4": {

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"extra": {
"branch-alias": {

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"autoload": {
"psr-4": {

View File

@ -37,10 +37,8 @@ class ScopeFlagVisibility
if ($actor->hasPermission('discussion.viewFlags')) {
$query->orWhereDoesntHave('post.discussion.tags');
}
}
if (! $actor->hasPermission('discussion.viewFlags')) {
$query->orWhere('flags.user_id', $actor->id);
} elseif (! $actor->hasPermission('discussion.viewFlags')) {
$query->whereRaw('1 = 0');
}
});
}

View File

@ -96,7 +96,7 @@ class ListTest extends TestCase
}
#[Test]
public function regular_user_sees_own_flags_of_visible_posts()
public function regular_user_does_not_see_own_flags_of_visible_posts()
{
$response = $this->send(
$this->request('GET', '/api/flags', [
@ -109,7 +109,7 @@ class ListTest extends TestCase
$data = json_decode($response->getBody()->getContents(), true)['data'];
$ids = Arr::pluck($data, 'id');
$this->assertEqualsCanonicalizing(['2', '4'], $ids);
$this->assertEqualsCanonicalizing([], $ids);
}
#[Test]

View File

@ -122,7 +122,7 @@ class ListWithTagsTest extends TestCase
}
#[Test]
public function regular_user_sees_own_flags()
public function regular_user_does_not_see_own_flags()
{
$response = $this->send(
$this->request('GET', '/api/flags', [
@ -135,7 +135,7 @@ class ListWithTagsTest extends TestCase
$data = json_decode($response->getBody()->getContents(), true)['data'];
$ids = Arr::pluck($data, 'id');
$this->assertEqualsCanonicalizing(['2', '4'], $ids);
$this->assertEqualsCanonicalizing([], $ids);
}
#[Test]

View File

@ -144,7 +144,7 @@ class IncludeFlagsVisibilityTest extends TestCase
'user_with_general_permission_sees_where_unrestricted_tag' => [2, [6, 7, 8]],
'user_with_tag1_permission_sees_tag1_flags' => [3, [1, 2, 3, 4, 5]],
'normal_user_sees_none' => [4, []],
'normal_user_sees_own' => [5, [2, 7, 4, 8]],
'normal_user_does_not_see_own' => [5, []],
];
}
}

View File

@ -7,7 +7,7 @@
],
"license": "MIT",
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"extra": {
"branch-alias": {

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"autoload": {
"psr-4": {

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"autoload": {
"psr-4": {

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"extra": {
"branch-alias": {

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"autoload": {
"psr-4": {

View File

@ -25,9 +25,13 @@ class FormatGroupMentions
public function __invoke(Renderer $renderer, mixed $context, string $xml): string
{
return Utils::replaceAttributes($xml, 'GROUPMENTION', function ($attributes) use ($context) {
$group = ($context instanceof AbstractModel && $context->isRelation('mentionsGroups'))
? $context->mentionsGroups->find($attributes['id']) // @phpstan-ignore-line
: Group::find($attributes['id']);
/** @var Group|null $group */
$group = match (true) {
$context instanceof AbstractModel && $context->isRelation('mentionsGroups') => $context->relationLoaded('mentionsGroups')
? $context->mentionsGroups->find($attributes['id']) // @phpstan-ignore-line
: $context->mentionsGroups()->find($attributes['id']), // @phpstan-ignore-line
default => Group::query()->find($attributes['id']),
};
if ($group) {
$attributes['groupname'] = $group->name_plural;

View File

@ -31,9 +31,13 @@ class FormatPostMentions
public function __invoke(Renderer $renderer, mixed $context, string $xml): string
{
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($context) {
$post = ($context instanceof AbstractModel && $context->isRelation('mentionsPosts'))
? $context->mentionsPosts->find($attributes['id']) // @phpstan-ignore-line
: Post::find($attributes['id']);
/** @var Post|null $post */
$post = match (true) {
$context instanceof AbstractModel && $context->isRelation('mentionsPosts') => $context->relationLoaded('mentionsPosts')
? $context->mentionsPosts->find($attributes['id']) // @phpstan-ignore-line
: $context->mentionsPosts()->find($attributes['id']), // @phpstan-ignore-line
default => Post::query()->find($attributes['id']),
};
if ($post && $post->user) {
$attributes['displayname'] = $post->user->display_name;

View File

@ -21,9 +21,12 @@ class FormatTagMentions
{
return Utils::replaceAttributes($xml, 'TAGMENTION', function ($attributes) use ($context) {
/** @var Tag|null $tag */
$tag = ($context instanceof AbstractModel && $context->isRelation('mentionsTags'))
? $context->mentionsTags->find($attributes['id']) // @phpstan-ignore-line
: Tag::query()->find($attributes['id']);
$tag = match (true) {
$context instanceof AbstractModel && $context->isRelation('mentionsTags') => $context->relationLoaded('mentionsTags')
? $context->mentionsTags->find($attributes['id']) // @phpstan-ignore-line
: $context->mentionsTags()->find($attributes['id']), // @phpstan-ignore-line
default => Tag::query()->find($attributes['id']),
};
if ($tag) {
$attributes['deleted'] = false;

View File

@ -27,9 +27,13 @@ class FormatUserMentions
public function __invoke(Renderer $renderer, mixed $context, string $xml): string
{
return Utils::replaceAttributes($xml, 'USERMENTION', function ($attributes) use ($context) {
$user = ($context instanceof AbstractModel && $context->isRelation('mentionsUsers'))
? $context->mentionsUsers->find($attributes['id']) // @phpstan-ignore-line
: User::find($attributes['id']);
/** @var User|null $user */
$user = match (true) {
$context instanceof AbstractModel && $context->isRelation('mentionsUsers') => $context->relationLoaded('mentionsUsers')
? $context->mentionsUsers->find($attributes['id']) // @phpstan-ignore-line
: $context->mentionsUsers()->find($attributes['id']), // @phpstan-ignore-line
default => User::query()->find($attributes['id']),
};
$attributes['deleted'] = false;

View File

@ -34,9 +34,13 @@ class UnparsePostMentions
protected function updatePostMentionTags(mixed $context, string $xml): string
{
return Utils::replaceAttributes($xml, 'POSTMENTION', function ($attributes) use ($context) {
$post = ($context instanceof AbstractModel && $context->isRelation('mentionsPosts'))
? $context->mentionsPosts->find($attributes['id']) // @phpstan-ignore-line
: Post::find($attributes['id']);
/** @var Post|null $post */
$post = match (true) {
$context instanceof AbstractModel && $context->isRelation('mentionsPosts') => $context->relationLoaded('mentionsPosts')
? $context->mentionsPosts->find($attributes['id']) // @phpstan-ignore-line
: $context->mentionsPosts()->find($attributes['id']), // @phpstan-ignore-line
default => Post::query()->find($attributes['id']),
};
if ($post && $post->user) {
$attributes['displayname'] = $post->user->display_name;

View File

@ -29,9 +29,12 @@ class UnparseTagMentions
{
return Utils::replaceAttributes($xml, 'TAGMENTION', function (array $attributes) use ($context) {
/** @var Tag|null $tag */
$tag = ($context instanceof AbstractModel && $context->isRelation('mentionsTags'))
? $context->mentionsTags->find($attributes['id']) // @phpstan-ignore-line
: Tag::query()->find($attributes['id']);
$tag = match (true) {
$context instanceof AbstractModel && $context->isRelation('mentionsTags') => $context->relationLoaded('mentionsTags')
? $context->mentionsTags->find($attributes['id']) // @phpstan-ignore-line
: $context->mentionsTags()->find($attributes['id']), // @phpstan-ignore-line
default => Tag::query()->find($attributes['id']),
};
if ($tag) {
$attributes['tagname'] = $tag->name;

View File

@ -34,9 +34,13 @@ class UnparseUserMentions
protected function updateUserMentionTags(mixed $context, string $xml): string
{
return Utils::replaceAttributes($xml, 'USERMENTION', function ($attributes) use ($context) {
$user = ($context instanceof AbstractModel && $context->isRelation('mentionsUsers'))
? $context->mentionsUsers->find($attributes['id']) // @phpstan-ignore-line
: User::find($attributes['id']);
/** @var User|null $user */
$user = match (true) {
$context instanceof AbstractModel && $context->isRelation('mentionsUsers') => $context->relationLoaded('mentionsUsers')
? $context->mentionsUsers->find($attributes['id']) // @phpstan-ignore-line
: $context->mentionsUsers()->find($attributes['id']), // @phpstan-ignore-line
default => User::query()->find($attributes['id']),
};
$attributes['displayname'] = $user?->display_name ?? $this->translator->trans('core.lib.username.deleted_text');

View File

@ -7,7 +7,7 @@
"type": "flarum-extension",
"license": "MIT",
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"authors": [
{

View File

@ -24,7 +24,7 @@ return [
->css(__DIR__.'/less/forum.less')
->jsDirectory(__DIR__.'/js/dist/forum')
->route('/messages', 'messages')
->route('/messages/dialog/{id:\d+}', 'messages.dialog'),
->route('/messages/dialog/{id:\d+}[/{near:\d+}]', 'messages.dialog'),
(new Extend\Frontend('admin'))
->js(__DIR__.'/js/dist/admin.js')
@ -51,7 +51,9 @@ return [
(new Extend\ApiResource(Resource\UserResource::class))
->fields(fn () => [
Schema\Boolean::make('canSendAnyMessage')
->get(fn (object $model, Context $context) => $context->getActor()->can('sendAnyMessage')),
->get(fn (User $user, Context $context) => $user->can('sendAnyMessage')),
Schema\Boolean::make('canDeleteOwnMessages')
->visible(fn (User $user, Context $context) => $context->getActor()->is($user)),
Schema\Integer::make('messageCount')
->get(function (object $model, Context $context) {
return Dialog::whereVisibleTo($context->getActor())

View File

@ -3,7 +3,7 @@ import DialogListState from '../forum/states/DialogListState';
declare module 'flarum/forum/routes' {
export interface ForumRoutes {
dialog: (tag: Dialog) => string;
dialog: (dialog: Dialog, near?: number) => string;
}
}
@ -19,3 +19,9 @@ declare module 'flarum/forum/states/ComposerState' {
composingMessageTo(dialog: Dialog): boolean;
}
}
declare module 'flarum/common/models/User' {
export default interface User {
canSendAnyMessage(): boolean;
}
}

View File

@ -1,2 +1,2 @@
declare const _default: (import("flarum/common/extenders/Store").default | import("flarum/common/extenders/Admin").default)[];
declare const _default: (import("flarum/common/extenders/Store").default | import("flarum/common/extenders/Model").default | import("flarum/common/extenders/Admin").default)[];
export default _default;

View File

@ -1,2 +1,2 @@
declare const _default: import("flarum/common/extenders/Store").default[];
declare const _default: (import("flarum/common/extenders/Store").default | import("flarum/common/extenders/Model").default)[];
export default _default;

View File

@ -2,6 +2,7 @@ import Model from 'flarum/common/Model';
import type Dialog from './Dialog';
import type User from 'flarum/common/models/User';
export default class DialogMessage extends Model {
number(): number;
content(): string | null | undefined;
contentHtml(): string | null | undefined;
renderFailed(): boolean | undefined;
@ -9,4 +10,5 @@ export default class DialogMessage extends Model {
createdAt(): Date;
dialog(): false | Dialog;
user(): false | User;
canDelete(): boolean;
}

View File

@ -10,6 +10,7 @@ export default class DialogSection<CustomAttrs extends IDialogStreamAttrs = IDia
protected loading: boolean;
protected messages: MessageStreamState;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
requestParams(forgetNear?: boolean): any;
view(): JSX.Element;
actionItems(): ItemList<Mithril.Children>;
controlItems(): ItemList<Mithril.Children>;

View File

@ -3,8 +3,10 @@ import Mithril from 'mithril';
import AbstractPost, { type IAbstractPostAttrs } from 'flarum/forum/components/AbstractPost';
import type User from 'flarum/common/models/User';
import DialogMessage from '../../common/models/DialogMessage';
import type MessageStreamState from '../states/MessageStreamState';
export interface IMessageAttrs extends IAbstractPostAttrs {
message: DialogMessage;
state: MessageStreamState;
}
/**
* The `Post` component displays a single post. The basic post template just

View File

@ -7,6 +7,7 @@ export interface IMessagesPageAttrs extends IPageAttrs {
}
export default class MessagesPage<CustomAttrs extends IMessagesPageAttrs = IMessagesPageAttrs> extends Page<CustomAttrs> {
protected selectedDialog: Stream<Dialog | null>;
protected currentDialogId: string | null;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>): void;
dialogRequestParams(): {
include: string;
@ -15,6 +16,7 @@ export default class MessagesPage<CustomAttrs extends IMessagesPageAttrs = IMess
onupdate(vnode: Mithril.VnodeDOM<CustomAttrs, this>): void;
view(): JSX.Element;
hero(): Mithril.Children;
contentItems(): ItemList<Mithril.Children>;
/**
* Build an item list for the part of the toolbar which is concerned with how
* the results are displayed. By default this is just a select box to change
@ -23,7 +25,7 @@ export default class MessagesPage<CustomAttrs extends IMessagesPageAttrs = IMess
viewItems(): ItemList<Mithril.Children>;
/**
* Build an item list for the part of the toolbar which is about taking action
* on the results. By default this is just a "mark all as read" button.
* on the results. By default, this is just a "mark all as read" button.
*/
actionItems(): ItemList<Mithril.Children>;
}

View File

@ -1,2 +1,2 @@
declare const _default: (import("flarum/common/extenders/Store").default | import("flarum/common/extenders/Routes").default)[];
declare const _default: (import("flarum/common/extenders/Store").default | import("flarum/common/extenders/Model").default | import("flarum/common/extenders/Routes").default)[];
export default _default;

View File

@ -0,0 +1,17 @@
import ItemList from 'flarum/common/utils/ItemList';
import type Mithril from 'mithril';
import type DialogMessage from '../../common/models/DialogMessage';
import type Message from '../components/Message';
declare const MessageControls: {
controls(message: DialogMessage, context: Message<any>): ItemList<Mithril.Children>;
sections(): {
user: (message: DialogMessage, context: Message) => ItemList<Mithril.Children>;
moderation: (message: DialogMessage, context: Message) => ItemList<Mithril.Children>;
destructive: (message: DialogMessage, context: Message) => ItemList<Mithril.Children>;
};
userControls(message: DialogMessage, context: Message): ItemList<Mithril.Children>;
moderationControls(message: DialogMessage, context: Message): ItemList<Mithril.Children>;
destructiveControls(message: DialogMessage, context: Message): ItemList<Mithril.Children>;
deleteAction(message: DialogMessage, context: Message): Promise<void> | undefined;
};
export default MessageControls;

View File

@ -1,2 +1,2 @@
(()=>{var e={n:t=>{var r=t&&t.__esModule?()=>t.default:()=>t;return e.d(r,{a:r}),r},d:(t,r)=>{for(var a in r)e.o(r,a)&&!e.o(t,a)&&Object.defineProperty(t,a,{enumerable:!0,get:r[a]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};(()=>{"use strict";e.r(t),e.d(t,{extend:()=>h});const r=flarum.reg.get("core","admin/app");var a=e.n(r);const s=flarum.reg.get("core","common/extenders");var n=e.n(s);const l=flarum.reg.get("core","common/Model");var o=e.n(l);const i=flarum.reg.get("core","common/utils/computed");var u=e.n(i);const d=flarum.reg.get("core","common/utils/string");class c extends(o()){content(){return o().attribute("content").call(this)}contentHtml(){return o().attribute("contentHtml").call(this)}renderFailed(){return o().attribute("renderFailed").call(this)}contentPlain(){return u()("contentHtml",(e=>"string"==typeof e?(0,d.getPlainContent)(e):e)).call(this)}createdAt(){return o().attribute("createdAt",o().transformDate).call(this)}dialog(){return o().hasOne("dialog").call(this)}user(){return o().hasOne("user").call(this)}}flarum.reg.add("flarum-messages","common/models/DialogMessage",c);const m=flarum.reg.get("core","common/app");var g=e.n(m);class f extends(o()){title(){return o().attribute("title").call(this)}type(){return o().attribute("type").call(this)}lastMessageAt(){return o().attribute("lastMessageAt",o().transformDate).call(this)}createdAt(){return o().attribute("createdAt",o().transformDate).call(this)}users(){return o().hasMany("users").call(this)}firstMessage(){return o().hasOne("firstMessage").call(this)}lastMessage(){return o().hasOne("lastMessage").call(this)}unreadCount(){return o().attribute("unreadCount").call(this)}lastReadMessageId(){return o().attribute("lastReadMessageId").call(this)}lastReadAt(){return o().attribute("lastReadAt",o().transformDate).call(this)}recipient(){let e=this.users();return e?e.find((e=>e&&e.id()!==g().session.user.id())):null}}flarum.reg.add("flarum-messages","common/models/Dialog",f);const h=[(new(n().Store)).add("dialogs",f).add("dialog-messages",c),(new(n().Admin)).permission((()=>({icon:"fas fa-envelope-open-text",label:a().translator.trans("flarum-messages.admin.permissions.send_messages"),permission:"dialog.sendMessage",allowGuest:!1})),"start",98)];a().initializers.add("flarum-messages",(()=>{}))})(),module.exports=t})();
(()=>{var e={n:t=>{var a=t&&t.__esModule?()=>t.default:()=>t;return e.d(a,{a}),a},d:(t,a)=>{for(var s in a)e.o(a,s)&&!e.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:a[s]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};(()=>{"use strict";e.r(t),e.d(t,{extend:()=>w});const a=flarum.reg.get("core","admin/app");var s=e.n(a);const r=flarum.reg.get("core","common/extenders");var n=e.n(r);const l=flarum.reg.get("core","common/Model");var o=e.n(l);const i=flarum.reg.get("core","common/utils/computed");var u=e.n(i);const c=flarum.reg.get("core","common/utils/string");class d extends(o()){number(){return o().attribute("number").call(this)}content(){return o().attribute("content").call(this)}contentHtml(){return o().attribute("contentHtml").call(this)}renderFailed(){return o().attribute("renderFailed").call(this)}contentPlain(){return u()("contentHtml",(e=>"string"==typeof e?(0,c.getPlainContent)(e):e)).call(this)}createdAt(){return o().attribute("createdAt",o().transformDate).call(this)}dialog(){return o().hasOne("dialog").call(this)}user(){return o().hasOne("user").call(this)}canDelete(){return o().attribute("canDelete").call(this)}}flarum.reg.add("flarum-messages","common/models/DialogMessage",d);const g=flarum.reg.get("core","common/app");var f=e.n(g);class b extends(o()){title(){return o().attribute("title").call(this)}type(){return o().attribute("type").call(this)}lastMessageAt(){return o().attribute("lastMessageAt",o().transformDate).call(this)}createdAt(){return o().attribute("createdAt",o().transformDate).call(this)}users(){return o().hasMany("users").call(this)}firstMessage(){return o().hasOne("firstMessage").call(this)}lastMessage(){return o().hasOne("lastMessage").call(this)}unreadCount(){return o().attribute("unreadCount").call(this)}lastReadMessageId(){return o().attribute("lastReadMessageId").call(this)}lastReadAt(){return o().attribute("lastReadAt",o().transformDate).call(this)}recipient(){let e=this.users();return e?e.find((e=>e&&e.id()!==f().session.user.id())):null}}flarum.reg.add("flarum-messages","common/models/Dialog",b);const p=flarum.reg.get("core","common/models/User");var _=e.n(p);const h=[(new(n().Store)).add("dialogs",b).add("dialog-messages",d),new(n().Model)(_()).attribute("canSendAnyMessage").attribute("canDeleteOwnMessage")],y=flarum.reg.get("core","admin/components/SettingDropdown");var v=e.n(y);const w=[...h,(new(n().Admin)).permission((()=>({icon:"fas fa-envelope-open-text",label:s().translator.trans("flarum-messages.admin.permissions.send_messages_label"),permission:"dialog.sendMessage",allowGuest:!1})),"start",95).permission((()=>({icon:"far fa-trash-alt",label:s().translator.trans("flarum-messages.admin.permissions.delete_own_messages_label"),id:"flarum-messages.allow_delete_own_messages",setting:()=>(parseInt(s().data.settings["flarum-messages.allow_delete_own_messages"],10),m(v(),{default:"0",key:"flarum-messages.allow_delete_own_messages",options:[{value:"-1",label:s().translator.trans("core.admin.permissions_controls.allow_indefinitely_button")},{value:"10",label:s().translator.trans("core.admin.permissions_controls.allow_ten_minutes_button")},{value:"reply",label:s().translator.trans("core.admin.permissions_controls.allow_until_reply_button")},{value:"0",label:s().translator.trans("core.admin.permissions_controls.allow_never_button")}]}))})),"reply",80)];s().initializers.add("flarum-messages",(()=>{}))})(),module.exports=t})();
//# 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

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,21 +0,0 @@
import type Dialog from '../common/models/Dialog';
import DialogListState from '../forum/states/DialogListState';
declare module 'flarum/forum/routes' {
export interface ForumRoutes {
dialog: (tag: Dialog) => string;
}
}
declare module 'flarum/forum/ForumApplication' {
export default interface ForumApplication {
dialogs: DialogListState;
dropdownDialogs: DialogListState;
}
}
declare module 'flarum/forum/states/ComposerState' {
export default interface ComposerState {
composingMessageTo(dialog: Dialog): boolean;
}
}

View File

@ -1,18 +0,0 @@
import Extend from 'flarum/common/extenders';
import commonExtend from '../common/extend';
import app from 'flarum/admin/app';
export default [
...commonExtend,
new Extend.Admin().permission(
() => ({
icon: 'fas fa-envelope-open-text',
label: app.translator.trans('flarum-messages.admin.permissions.send_messages'),
permission: 'dialog.sendMessage',
allowGuest: false,
}),
'start',
98
),
];

View File

@ -0,0 +1,45 @@
import Extend from 'flarum/common/extenders';
import commonExtend from '../common/extend';
import app from 'flarum/admin/app';
import SettingDropdown from 'flarum/admin/components/SettingDropdown';
export default [
...commonExtend,
new Extend.Admin()
.permission(
() => ({
icon: 'fas fa-envelope-open-text',
label: app.translator.trans('flarum-messages.admin.permissions.send_messages_label'),
permission: 'dialog.sendMessage',
allowGuest: false,
}),
'start',
95
)
.permission(
() => ({
icon: 'far fa-trash-alt',
label: app.translator.trans('flarum-messages.admin.permissions.delete_own_messages_label'),
id: 'flarum-messages.allow_delete_own_messages',
setting: () => {
const minutes = parseInt(app.data.settings['flarum-messages.allow_delete_own_messages'], 10);
return (
<SettingDropdown
default={'0'}
key="flarum-messages.allow_delete_own_messages"
options={[
{ value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button') },
{ value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button') },
{ value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button') },
{ value: '0', label: app.translator.trans('core.admin.permissions_controls.allow_never_button') },
]}
/>
);
},
}),
'reply',
80
),
];

View File

@ -1,9 +1,14 @@
import DialogMessage from './models/DialogMessage';
import Dialog from './models/Dialog';
import Extend from 'flarum/common/extenders';
import User from 'flarum/common/models/User';
export default [
new Extend.Store()
.add('dialogs', Dialog) //
.add('dialog-messages', DialogMessage), //
new Extend.Model(User) //
.attribute<boolean>('canSendAnyMessage')
.attribute<boolean>('canDeleteOwnMessage'),
];

View File

@ -5,6 +5,9 @@ import type Dialog from './Dialog';
import type User from 'flarum/common/models/User';
export default class DialogMessage extends Model {
number() {
return Model.attribute<number>('number').call(this);
}
content() {
return Model.attribute<string | null | undefined>('content').call(this);
}
@ -33,4 +36,8 @@ export default class DialogMessage extends Model {
user() {
return Model.hasOne<User>('user').call(this);
}
canDelete() {
return Model.attribute<boolean>('canDelete').call(this);
}
}

View File

@ -24,14 +24,27 @@ export default class DialogSection<CustomAttrs extends IDialogStreamAttrs = IDia
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.messages = new MessageStreamState({
this.messages = new MessageStreamState(this.requestParams());
this.messages.refresh();
}
requestParams(forgetNear = false): any {
const params: any = {
filter: {
dialog: this.attrs.dialog.id(),
},
sort: '-createdAt',
});
sort: '-number',
};
this.messages.refresh();
const near = m.route.param('near');
if (near && !forgetNear) {
params.page = params.page || {};
params.page.near = parseInt(near);
}
return params;
}
view() {
@ -42,11 +55,14 @@ export default class DialogSection<CustomAttrs extends IDialogStreamAttrs = IDia
<div className="DialogSection-header">
<Avatar user={recipient} />
<div className="DialogSection-header-info">
{(recipient && (
<Link href={app.route.user(recipient!)}>
<h2>{username(recipient)}</h2>
</Link>
)) || <h2>{username(recipient)}</h2>}
<h2 className="DialogSection-header-info-title">
{(recipient && <Link href={app.route.user(recipient!)}>{username(recipient)}</Link>) || username(recipient)}
{recipient && recipient.canSendAnyMessage() ? null : (
<span className="DialogSection-header-info-helperText">
{app.translator.trans('flarum-messages.forum.dialog_section.cannot_reply_text')}
</span>
)}
</h2>
<div className="badges">{listItems(recipient?.badges().toArray() || [])}</div>
</div>
<div className="DialogSection-header-actions">{this.actionItems().toArray()}</div>
@ -59,6 +75,13 @@ export default class DialogSection<CustomAttrs extends IDialogStreamAttrs = IDia
actionItems() {
const items = new ItemList<Mithril.Children>();
items.add(
'back',
<Button className="Button Button--icon DialogSection-back" icon="fas fa-arrow-left" onclick={this.attrs.onback}>
{app.translator.trans('flarum-messages.forum.dialog_section.back_label')}
</Button>
);
items.add(
'details',
<Dropdown

View File

@ -9,9 +9,12 @@ import Comment from 'flarum/forum/components/Comment';
import PostUser from 'flarum/forum/components/PostUser';
import PostMeta from 'flarum/forum/components/PostMeta';
import classList from 'flarum/common/utils/classList';
import MessageControls from '../utils/MessageControls';
import type MessageStreamState from '../states/MessageStreamState';
export interface IMessageAttrs extends IAbstractPostAttrs {
message: DialogMessage;
state: MessageStreamState;
}
/**
@ -29,7 +32,7 @@ export default abstract class Message<CustomAttrs extends IMessageAttrs = IMessa
}
controls(): Mithril.Children[] {
return [];
return MessageControls.controls(this.attrs.message, this).toArray();
}
freshness(): Date {
@ -97,7 +100,7 @@ export default abstract class Message<CustomAttrs extends IMessageAttrs = IMessa
}
avatar(): Mithril.Children {
return this.attrs.message.user() ? <Avatar user={this.attrs.message.user()} /> : '';
return this.attrs.message.user() ? <Avatar user={this.attrs.message.user()} className="Post-avatar" /> : '';
}
headerItems() {
@ -105,7 +108,21 @@ export default abstract class Message<CustomAttrs extends IMessageAttrs = IMessa
const message = this.attrs.message;
items.add('user', <PostUser post={message} />, 100);
items.add('meta', <PostMeta post={message} />);
items.add(
'meta',
<PostMeta
post={message}
permalink={() => {
const dialog = message.dialog();
if (!dialog) {
return null;
}
return app.forum.attribute('baseOrigin') + app.route.dialog(dialog, message.number());
}}
/>
);
return items;
}

View File

@ -77,18 +77,20 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
content() {
const items: Mithril.Children[] = [];
const messages = this.attrs.state.getAllItems().sort((a, b) => a.createdAt().getTime() - b.createdAt().getTime());
const messages = Array.from(new Map(this.attrs.state.getAllItems().map((msg) => [msg.id(), msg])).values()).sort(
(a, b) => a.number() - b.number()
);
const ReplyPlaceholder = this.replyPlaceholderComponent();
const LoadingPost = this.loadingPostComponent();
if (messages[0].id() !== (this.attrs.dialog.data.relationships?.firstMessage.data as ModelIdentifier).id) {
items.push(
<div className="MessageStream-item" key="loadPrevious">
<div className="MessageStream-item" key="loadNext">
<Button
onclick={() => this.whileMaintainingScroll(() => this.attrs.state.loadNext())}
type="button"
className="Button Button--block MessageStream-loadPrev"
className="Button Button--block MessageStream-loadNext"
>
{app.translator.trans('flarum-messages.forum.messages_page.stream.load_previous_button')}
</Button>
@ -97,7 +99,7 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
if (LoadingPost) {
items.push(
<div className="MessageStream-item" key="loading-prev">
<div className="MessageStream-item" key="loading-next">
<LoadingPost />
</div>
);
@ -106,9 +108,31 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
messages.forEach((message, index) => items.push(this.messageItem(message, index)));
if (ReplyPlaceholder) {
if (messages[messages.length - 1].id() !== (this.attrs.dialog.data.relationships?.lastMessage.data as ModelIdentifier).id) {
if (LoadingPost) {
items.push(
<div className="MessageStream-item" key="loading-prev">
<LoadingPost />
</div>
);
}
items.push(
<div className="MessageStream-item" key="reply" /*data-index={this.attrs.state.count()}*/>
<div className="MessageStream-item" key="loadPrev">
<Button
onclick={() => this.whileMaintainingScroll(() => this.attrs.state.loadPrev())}
type="button"
className="Button Button--block MessageStream-loadPrev"
>
{app.translator.trans('flarum-messages.forum.messages_page.stream.load_next_button')}
</Button>
</div>
);
}
if (app.session.user!.canSendAnyMessage() && ReplyPlaceholder) {
items.push(
<div className="MessageStream-item" key="reply">
<ReplyPlaceholder
discussion={this.attrs.dialog}
onclick={() => {
@ -135,9 +159,9 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
messageItem(message: DialogMessage, index: number) {
return (
<div className="MessageStream-item" key={index} data-id={message.id()}>
<div className="MessageStream-item" key={index} data-id={message.id()} data-number={message.number()}>
{this.timeGap(message)}
<Message message={message} />
<Message message={message} state={this.attrs.state} />
</div>
);
}
@ -177,7 +201,7 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
return this.attrs.state.loadNext();
}
if (this.element.scrollTop + this.element.clientHeight === this.element.scrollHeight && this.attrs.state.hasPrev()) {
if (this.element.scrollTop + this.element.clientHeight >= this.element.scrollHeight && this.attrs.state.hasPrev()) {
return this.attrs.state.loadPrev();
}
@ -186,16 +210,34 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
}
scrollToBottom() {
this.element.scrollTop = this.element.scrollHeight;
const near = m.route.param('near');
if (near) {
const $message = this.element.querySelector(`.MessageStream-item[data-number="${near}"]`);
if ($message) {
this.element.scrollTop = $message.getBoundingClientRect().top - this.element.getBoundingClientRect().top;
$message.classList.add('flash');
// forget near
window.history.replaceState(null, '', app.route.dialog(this.attrs.dialog));
} else {
this.element.scrollTop = this.element.scrollHeight;
}
} else {
this.element.scrollTop = this.element.scrollHeight;
}
}
whileMaintainingScroll(callback: () => null | Promise<void>) {
const scrollTop = this.element.scrollTop;
const scrollHeight = this.element.scrollHeight;
const closerToBottomThanTop = scrollTop > (scrollHeight - this.element.clientHeight) / 2;
const result = callback();
if (result instanceof Promise) {
if (result instanceof Promise && !closerToBottomThanTop) {
result.then(() => {
requestAnimationFrame(() => {
this.element.scrollTop = this.element.scrollHeight - scrollHeight + scrollTop;

View File

@ -14,11 +14,13 @@ import listItems from 'flarum/common/helpers/listItems';
import ItemList from 'flarum/common/utils/ItemList';
import Dropdown from 'flarum/common/components/Dropdown';
import Button from 'flarum/common/components/Button';
import classList from 'flarum/common/utils/classList';
export interface IMessagesPageAttrs extends IPageAttrs {}
export default class MessagesPage<CustomAttrs extends IMessagesPageAttrs = IMessagesPageAttrs> extends Page<CustomAttrs> {
protected selectedDialog = Stream<Dialog | null>(null);
protected currentDialogId: string | null = null;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
@ -49,6 +51,7 @@ export default class MessagesPage<CustomAttrs extends IMessagesPageAttrs = IMess
protected async initDialog() {
const dialogId = m.route.param('id');
this.currentDialogId = dialogId;
const title = app.translator.trans('flarum-messages.forum.messages_page.title', {}, true);
@ -94,19 +97,12 @@ export default class MessagesPage<CustomAttrs extends IMessagesPageAttrs = IMess
) : !app.dialogs.hasItems() ? (
<InfoTile icon="far fa-envelope-open">{app.translator.trans('flarum-messages.forum.messages_page.empty_text')}</InfoTile>
) : (
<div className="MessagesPage-content">
<div className="MessagesPage-sidebar" key="sidebar">
<div className="IndexPage-toolbar" key="toolbar">
<ul className="IndexPage-toolbar-view">{listItems(this.viewItems().toArray())}</ul>
<ul className="IndexPage-toolbar-action">{listItems(this.actionItems().toArray())}</ul>
</div>
<DialogList key="list" state={app.dialogs} activeDialog={this.selectedDialog()} />
</div>
{this.selectedDialog() ? (
<DialogSection key="dialog" dialog={this.selectedDialog()} />
) : (
<LoadingIndicator key="loading" display="block" />
)}
<div
className={classList('MessagesPage-content', {
'MessagesPage-content--onDialog': this.currentDialogId,
})}
>
{this.contentItems().toArray()}
</div>
)}
</PageStructure>
@ -128,6 +124,40 @@ export default class MessagesPage<CustomAttrs extends IMessagesPageAttrs = IMess
);
}
contentItems() {
const items = new ItemList<Mithril.Children>();
items.add(
'sidebar',
<div className="MessagesPage-sidebar" key="sidebar">
<div className="IndexPage-toolbar" key="toolbar">
<ul className="IndexPage-toolbar-view">{listItems(this.viewItems().toArray())}</ul>
<ul className="IndexPage-toolbar-action">{listItems(this.actionItems().toArray())}</ul>
</div>
<DialogList key="list" state={app.dialogs} activeDialog={this.selectedDialog()} />
</div>,
100
);
items.add(
'dialog',
this.selectedDialog() ? (
<DialogSection
key="dialog"
dialog={this.selectedDialog()}
onback={() => {
this.currentDialogId = null;
}}
/>
) : (
<LoadingIndicator key="loading" display="block" />
),
80
);
return items;
}
/**
* Build an item list for the part of the toolbar which is concerned with how
* the results are displayed. By default this is just a select box to change
@ -168,7 +198,7 @@ export default class MessagesPage<CustomAttrs extends IMessagesPageAttrs = IMess
/**
* Build an item list for the part of the toolbar which is about taking action
* on the results. By default this is just a "mark all as read" button.
* on the results. By default, this is just a "mark all as read" button.
*/
actionItems() {
const items = new ItemList<Mithril.Children>();

View File

@ -14,8 +14,6 @@ export default class MessagesSidebar<CustomAttrs extends IMessagesSidebarAttrs =
items(): ItemList<Mithril.Children> {
const items = super.items();
const canSendAnyMessage = app.session.user!.attribute<boolean>('canSendAnyMessage');
items.remove('newDiscussion');
items.add(
@ -27,9 +25,11 @@ export default class MessagesSidebar<CustomAttrs extends IMessagesSidebarAttrs =
onclick={() => {
return this.newMessageAction();
}}
disabled={!canSendAnyMessage}
disabled={!app.session.user!.canSendAnyMessage()}
>
{app.translator.trans('flarum-messages.forum.messages_page.new_message_button')}
{app.session.user!.canSendAnyMessage()
? app.translator.trans('flarum-messages.forum.messages_page.send_message_button')
: app.translator.trans('flarum-messages.forum.messages_page.cannot_send_message_button')}
</Button>,
10
);

View File

@ -9,5 +9,6 @@ export default [
new Extend.Routes() //
.add('messages', '/messages', () => import('./components/MessagesPage'))
.add('dialog', '/messages/dialog/:id', () => import('./components/MessagesPage'))
.helper('dialog', (dialog: Dialog) => app.route('dialog', { id: dialog.id() })),
.add('dialog.message', '/messages/dialog/:id/:near', () => import('./components/MessagesPage'))
.helper('dialog', (dialog: Dialog, near?: number) => app.route(near ? 'dialog.message' : 'dialog', { id: dialog.id(), near: near })),
];

View File

@ -8,6 +8,7 @@ import Button from 'flarum/common/components/Button';
import type Dialog from '../common/models/Dialog';
import DialogsDropdown from './components/DialogsDropdown';
import DialogListState from './states/DialogListState';
import type User from 'flarum/common/models/User';
export { default as extend } from './extend';
@ -44,14 +45,14 @@ app.initializers.add('flarum-messages', () => {
});
extend(HeaderSecondary.prototype, 'items', function (items) {
if (app.session.user?.attribute<boolean>('canSendAnyMessage')) {
if (app.session.user?.canSendAnyMessage()) {
items.add('messages', <DialogsDropdown state={app.dropdownDialogs} />, 15);
}
});
// @ts-ignore
extend(UserControls, 'userControls', (items, user) => {
if (app.session.user?.attribute<boolean>('canSendAnyMessage')) {
extend(UserControls, 'userControls', (items, user: User) => {
if (app.session.user?.canSendAnyMessage()) {
items.add(
'sendMessage',
<Button
@ -66,6 +67,7 @@ app.initializers.add('flarum-messages', () => {
.then(() => app.composer.show());
});
}}
helperText={user.canSendAnyMessage() ? null : app.translator.trans('flarum-messages.forum.user_controls.cannot_reply_text')}
>
{app.translator.trans('flarum-messages.forum.user_controls.send_message_button')}
</Button>

View File

@ -1,5 +1,6 @@
import PaginatedListState, { PaginatedListParams } from 'flarum/common/states/PaginatedListState';
import DialogMessage from '../../common/models/DialogMessage';
import { ApiQueryParamsPlural } from 'flarum/common/Store';
export interface MessageStreamParams extends PaginatedListParams {
//

View File

@ -0,0 +1,67 @@
import ItemList from 'flarum/common/utils/ItemList';
import Separator from 'flarum/common/components/Separator';
import type Mithril from 'mithril';
import type DialogMessage from '../../common/models/DialogMessage';
import type Message from '../components/Message';
import Button from 'flarum/common/components/Button';
import app from 'flarum/forum/app';
import extractText from 'flarum/common/utils/extractText';
const MessageControls = {
controls(message: DialogMessage, context: Message<any>) {
const items = new ItemList<Mithril.Children>();
Object.entries(this.sections()).forEach(([section, method]) => {
const controls = method.call(this, message, context).toArray();
if (controls.length) {
controls.forEach((item) => items.add(item.itemName, item));
items.add(section + 'Separator', <Separator />);
}
});
return items;
},
sections() {
return {
user: this.userControls,
moderation: this.moderationControls,
destructive: this.destructiveControls,
};
},
userControls(message: DialogMessage, context: Message) {
return new ItemList<Mithril.Children>();
},
moderationControls(message: DialogMessage, context: Message) {
return new ItemList<Mithril.Children>();
},
destructiveControls(message: DialogMessage, context: Message) {
const items = new ItemList<Mithril.Children>();
if (message.canDelete()) {
items.add(
'delete',
<Button icon="far fa-trash-alt" onclick={() => this.deleteAction(message, context)}>
{app.translator.trans('flarum-messages.forum.message_controls.delete_button')}
</Button>
);
}
return items;
},
deleteAction(message: DialogMessage, context: Message) {
if (!confirm(extractText(app.translator.trans('flarum-messages.forum.message_controls.delete_confirmation')))) return;
return message.delete().then(() => {
context.attrs.state.remove(message);
m.redraw();
});
},
};
export default MessageControls;

View File

@ -1,17 +1,68 @@
.MessagesPage-sidebar {
flex-shrink: 0;
width: 280px;
.MessagesPage {
padding-bottom: 0;
}
.MessagesPage-content {
--messages-page-gap: 32px;
display: flex;
gap: 32px;
gap: var(--messages-page-gap);
.Avatar {
--size: 40px;
}
}
.MessagesPage-sidebar {
flex-shrink: 0;
width: 100%;
.MessagesPage-content--onDialog & {
// margin-inline-start: calc(~"0px - 100% - var(--messages-page-gap)");
display: none;
}
@media @tablet-up {
width: 280px;
.MessagesPage-content--onDialog & {
// margin-inline-start: 0;
display: block;
}
}
}
.DialogSection {
flex-grow: 1;
min-width: 0;
@media @tablet-up {
padding-inline-start: 32px;
}
&-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--control-bg);
a {
color: var(--text-color);
}
&-actions {
margin-inline-start: auto;
}
&-info {
display: flex;
align-items: center;
gap: 12px;
}
}
}
.MessageComposer-recipients {
display: flex;
align-items: center;
@ -145,34 +196,6 @@
}
}
.DialogSection {
flex-grow: 1;
padding-inline-start: 32px;
&-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--control-bg);
a {
color: var(--text-color);
}
&-actions {
margin-inline-start: auto;
}
&-info {
display: flex;
align-items: center;
gap: 12px;
}
}
}
.Message {
padding-right: 0;
@ -191,8 +214,41 @@
}
.MessageStream, .DialogList {
max-height: calc(100vh - var(--header-height) - 140px - 235px);
--additional-gap: 52px;
max-height: calc(100vh - var(--header-height) - 140px - var(--additional-gap));
overflow: auto;
@media @tablet-up {
--additional-gap: 235px;
}
}
.MessageStream .ReplyPlaceholder {
margin-bottom: 24px;
}
.DialogSection-header-actions {
display: flex;
gap: 6px;
}
.DialogSection-header-info-title {
display: flex;
flex-direction: column;
}
.DialogSection-header-info-helperText {
font-size: 0.8rem;
font-weight: normal;
color: var(--control-color);
}
.DialogSection-back {
display: flex;
@media @tablet-up {
display: none;
}
}
.DialogList-loadMore {

View File

@ -3,7 +3,8 @@ flarum-messages:
# Translations in this namespace are used by the admin interface.
admin:
permissions:
send_messages: Send private messages
send_messages_label: Send private messages
delete_own_messages_label: Delete own messages
# Translations in this namespace are used by the forum user interface.
forum:
@ -21,6 +22,8 @@ flarum-messages:
view_all: View all messages
dialog_section:
back_label: Go back
cannot_reply_text: This user cannot reply
controls:
details_button: Details
controls_toggle_label: Dialog control actions
@ -40,17 +43,22 @@ flarum-messages:
newest_button: Newest
oldest_button: Oldest
message_controls:
delete_button: Delete
delete_confirmation: Are you sure you want to delete this message? This action cannot be undone.
messages_page:
empty_text: You have no messages yet. When you send or receive messages, they
will appear here.
cannot_send_message_button: Can't Send a Message
empty_text: No new messages
hero:
title: Messages
subtitle: Your private conversations with other users
mark_all_as_read_tooltip: Mark all as read
new_message_button: Send a Message
refresh_tooltip: Refresh
send_message_button: Send a Message
stream:
load_previous_button: Load previous messages
load_next_button: Load next messages
start_of_the_conversation: Start of the conversation
time_lapsed_text: => core.forum.post_stream.time_lapsed_text
title: Messages
@ -63,6 +71,7 @@ flarum-messages:
user_controls:
send_message_button: Send a message
cannot_reply_text: This user cannot reply
notifications:
message_received_text: Message Received notification from {user}

View File

@ -0,0 +1,50 @@
<?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.
*/
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
return [
'up' => function (Builder $schema) {
$schema->table('dialog_messages', function (Blueprint $table) {
$table->unsignedBigInteger('number')->nullable()->after('content');
});
$numbers = [];
$schema->getConnection()
->table('dialogs')
->orderBy('id')
->each(function (object $dialog) use ($schema, &$numbers) {
$numbers[$dialog->id] = 0;
$schema->getConnection()
->table('dialog_messages')
->where('dialog_id', $dialog->id)
->orderBy('id')
->each(function (object $message) use ($schema, &$numbers) {
$schema->getConnection()
->table('dialog_messages')
->where('id', $message->id)
->update(['number' => ++$numbers[$message->dialog_id]]);
});
unset($numbers[$dialog->id]);
});
$schema->table('dialog_messages', function (Blueprint $table) {
$table->unsignedBigInteger('number')->nullable(false)->change();
});
},
'down' => function (Builder $schema) {
$schema->table('dialog_messages', function (Blueprint $table) {
$table->dropColumn('number');
});
}
];

View File

@ -9,14 +9,36 @@
namespace Flarum\Messages\Access;
use Carbon\Carbon;
use Flarum\Messages\DialogMessage;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\Access\AbstractPolicy;
use Flarum\User\User;
class DialogMessagePolicy extends AbstractPolicy
{
public function update(User $actor, DialogMessage $dialogMessage): bool
public function __construct(
protected SettingsRepositoryInterface $settings
) {
}
public function update(User $actor, DialogMessage $message): ?bool
{
return null;
}
public function delete(User $actor, DialogMessage $message): bool|null|string
{
if ($message->user_id === $actor->id) {
$allowHiding = $this->settings->get('flarum-messages.allow_delete_own_messages');
if ($allowHiding === '-1'
|| ($allowHiding === 'reply' && $message->number >= $message->dialog->lastMessage->number)
|| (is_numeric($allowHiding) && $message->created_at->diffInMinutes(new Carbon, true) < $allowHiding)) {
return $this->allow();
}
}
return false;
}
}

View File

@ -27,6 +27,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Tobyz\JsonApiServer\Context as OriginalContext;
use Tobyz\JsonApiServer\Exception\BadRequestException;
/**
* @extends Resource\AbstractDatabaseResource<DialogMessage>
@ -77,6 +78,11 @@ class DialogMessageResource extends Resource\AbstractDatabaseResource
return $actor->can('sendAnyMessage');
}
}),
Endpoint\Delete::make()
->authenticated()
->visible(function (DialogMessage $message, Context $context): bool {
return $context->getActor()->can('delete', $message);
}),
Endpoint\Index::make()
->authenticated()
->defaultInclude([
@ -86,6 +92,7 @@ class DialogMessageResource extends Resource\AbstractDatabaseResource
'mentionsGroups',
'mentionsTags',
])
->defaultSort('-number')
->eagerLoad(function () {
if ($this->extensions->isEnabled('flarum-mentions')) {
return ['mentionsUsers', 'mentionsPosts', 'mentionsGroups', 'mentionsTags'];
@ -93,6 +100,35 @@ class DialogMessageResource extends Resource\AbstractDatabaseResource
return [];
})
->extractOffset(function (Context $context, array $defaultExtracts): int {
$queryParams = $context->request->getQueryParams();
$near = intval(Arr::get($queryParams, 'page.near'));
if ($near > 1) {
$sort = $defaultExtracts['sort'];
$filter = $defaultExtracts['filter'];
$dialogId = $filter['dialog'] ?? null;
if (count($filter) > 1 || ! $dialogId || ($sort && $sort !== ['number' => 'desc'])) {
throw new BadRequestException(
'You can only use page[near] with filter[dialog] and the default sort order'
);
}
$limit = $defaultExtracts['limit'];
$index = DialogMessage::query()
->where('dialog_id', $dialogId)
->where('number', '>=', $near)
->orderBy('number', 'desc')
->whereVisibleTo($context->getActor())
->count();
return max(0, $index - $limit / 2);
}
return $defaultExtracts['offset'];
})
->paginate(),
];
}
@ -101,6 +137,7 @@ class DialogMessageResource extends Resource\AbstractDatabaseResource
{
return [
Schema\Number::make('number'),
Schema\Str::make('content')
->requiredOnCreate()
->writableOnCreate()
@ -134,6 +171,12 @@ class DialogMessageResource extends Resource\AbstractDatabaseResource
->items(1)
->set(fn () => null),
// Read-only.
Schema\Boolean::make('canDelete')
->get(function (DialogMessage $message, Context $context) {
return $context->getActor()->can('delete', $message);
}),
Schema\Relationship\ToOne::make('user')
->type('users')
->includable(),
@ -161,7 +204,7 @@ class DialogMessageResource extends Resource\AbstractDatabaseResource
public function sorts(): array
{
return [
SortColumn::make('createdAt'),
SortColumn::make('number'),
];
}

View File

@ -21,12 +21,14 @@ use Flarum\Tags\Tag;
use Flarum\User\User;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Query\Expression;
/**
* @property int $id
* @property int $dialog_id
* @property int|null $user_id
* @property string $content
* @property int|Expression $number
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property-read Dialog $dialog
@ -48,6 +50,28 @@ class DialogMessage extends AbstractModel implements Formattable
protected $guarded = [];
protected $casts = [
'dialog_id' => 'integer',
'user_id' => 'integer',
'number' => 'integer',
];
public static function boot()
{
parent::boot();
static::creating(function (self $message) {
$db = static::getConnectionResolver()->connection();
$message->number = new Expression('('.
$db->table('dialog_messages', 'dm')
->whereRaw($db->getTablePrefix().'dm.dialog_id = '.intval($message->dialog_id))
->selectRaw('COALESCE(MAX('.$db->getTablePrefix().'dm.number), 0) + 1')
->toSql()
.')');
});
}
public function dialog(): BelongsTo
{
return $this->belongsTo(Dialog::class);

View File

@ -39,12 +39,12 @@ class ListTest extends TestCase
['id' => 104, 'type' => 'direct'],
],
DialogMessage::class => [
['id' => 102, 'dialog_id' => 102, 'user_id' => 3, 'content' => 'Hello, Gale!'],
['id' => 103, 'dialog_id' => 102, 'user_id' => 4, 'content' => 'Hello, Astarion!'],
['id' => 104, 'dialog_id' => 103, 'user_id' => 3, 'content' => 'Hello, Karlach!'],
['id' => 105, 'dialog_id' => 103, 'user_id' => 5, 'content' => 'Hello, Astarion!'],
['id' => 106, 'dialog_id' => 104, 'user_id' => 4, 'content' => 'Hello, Karlach!'],
['id' => 107, 'dialog_id' => 104, 'user_id' => 5, 'content' => 'Hello, Gale!'],
['id' => 102, 'dialog_id' => 102, 'user_id' => 3, 'content' => 'Hello, Gale!', 'number' => 1],
['id' => 103, 'dialog_id' => 102, 'user_id' => 4, 'content' => 'Hello, Astarion!', 'number' => 2],
['id' => 104, 'dialog_id' => 103, 'user_id' => 3, 'content' => 'Hello, Karlach!', 'number' => 1],
['id' => 105, 'dialog_id' => 103, 'user_id' => 5, 'content' => 'Hello, Astarion!', 'number' => 2],
['id' => 106, 'dialog_id' => 104, 'user_id' => 4, 'content' => 'Hello, Karlach!', 'number' => 1],
['id' => 107, 'dialog_id' => 104, 'user_id' => 5, 'content' => 'Hello, Gale!', 'number' => 2],
],
'dialog_user' => [
['dialog_id' => 102, 'user_id' => 3, 'joined_at' => Carbon::now()],
@ -125,4 +125,49 @@ class ListTest extends TestCase
'Karlach can see messages in dialogs with Astarion and Gale' => [5, [104, 105, 106, 107]],
];
}
public function test_can_list_near_accessible_dialog_messages(): void
{
$messages = [];
for ($i = 1; $i <= 40; $i++) {
$messages[] = ['id' => 200 + $i, 'dialog_id' => 200, 'user_id' => $i % 2 === 0 ? 3 : 4, 'content' => '<t>Hello, Gale!</t>', 'number' => $i];
}
$this->prepareDatabase([
Dialog::class => [
['id' => 200, 'type' => 'direct'],
],
DialogMessage::class => $messages,
'dialog_user' => [
['dialog_id' => 200, 'user_id' => 3, 'joined_at' => Carbon::now()],
['dialog_id' => 200, 'user_id' => 4, 'joined_at' => Carbon::now()],
],
]);
$this->database()->table('dialogs')->where('id', '!=', 200)->delete();
$this->database()->table('dialog_messages')->where('dialog_id', '!=', 200)->delete();
$response = $this->send(
$this->request('GET', '/api/dialog-messages', [
'authenticatedAs' => 3,
])->withQueryParams([
'include' => 'dialog',
'page' => ['near' => 10],
'filter' => ['dialog' => 200],
]),
);
$json = $response->getBody()->getContents();
$prettyJson = json_encode($json, JSON_PRETTY_PRINT);
$this->assertEquals(200, $response->getStatusCode(), $prettyJson);
$this->assertJson($json);
$data = json_decode($json, true)['data'];
$prettyJson = json_encode(json_decode($json), JSON_PRETTY_PRINT);
$this->assertEquals(40, $this->database()->table('dialog_messages')->count());
$this->assertCount(19, $data, $prettyJson);
}
}

View File

@ -36,7 +36,7 @@ class CreateTest extends TestCase
['id' => 102, 'type' => 'direct'],
],
DialogMessage::class => [
['id' => 102, 'dialog_id' => 102, 'user_id' => 4, 'content' => 'Hello, Karlach!'],
['id' => 102, 'dialog_id' => 102, 'user_id' => 4, 'content' => 'Hello, Karlach!', 'number' => 1],
],
'dialog_user' => [
['dialog_id' => 102, 'user_id' => 4, 'joined_at' => Carbon::now()],

View File

@ -37,16 +37,16 @@ class UpdateTest extends TestCase
['id' => 102, 'type' => 'direct', 'last_message_id' => 111],
],
DialogMessage::class => [
['id' => 102, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>'],
['id' => 103, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>'],
['id' => 104, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>'],
['id' => 105, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>'],
['id' => 106, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>'],
['id' => 107, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>'],
['id' => 108, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>'],
['id' => 109, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>'],
['id' => 110, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>'],
['id' => 111, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>'],
['id' => 102, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>', 'number' => 1],
['id' => 103, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>', 'number' => 2],
['id' => 104, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>', 'number' => 3],
['id' => 105, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>', 'number' => 4],
['id' => 106, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>', 'number' => 5],
['id' => 107, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>', 'number' => 6],
['id' => 108, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>', 'number' => 7],
['id' => 109, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>', 'number' => 8],
['id' => 110, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>', 'number' => 9],
['id' => 111, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>', 'number' => 10],
],
'dialog_user' => [
['dialog_id' => 102, 'user_id' => 3, 'last_read_message_id' => 0, 'last_read_at' => null, 'joined_at' => Carbon::now()],

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"autoload": {
"psr-4": {

View File

@ -22,7 +22,7 @@
"source": "https://github.com/flarum/extension-manager"
},
"require": {
"flarum/core": "^2.0.0-beta.1",
"flarum/core": "^2.0.0-beta.2",
"composer/composer": "^2.7"
},
"require-dev": {

View File

@ -19,8 +19,8 @@ export default class ExternalExtension extends Model {
locale: () => string;
latestFlarumVersionSupported: () => string;
downloads: () => number;
isSupported: () => boolean;
readonly installed = false;
isSupported(): boolean;
isProductionReady(): boolean;
toLocalExtension(): Extension;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -22,21 +22,9 @@ export default class ExternalExtension extends Model {
locale = Model.attribute<string>('locale');
latestFlarumVersionSupported = Model.attribute<string>('latestFlarumVersionSupported');
downloads = Model.attribute<number>('downloads');
isSupported = Model.attribute<boolean>('isSupported');
readonly installed = false;
public isSupported(): boolean {
const currentVersion = app.data.settings.version;
const latestCompatibleVersion = this.latestFlarumVersionSupported();
// If stability is not the same, it's not compatible.
if (currentVersion.split('-')[1] !== latestCompatibleVersion.split('-')[1]) {
return false;
}
// Minor versions are compatible.
return currentVersion.split('.')[0] === latestCompatibleVersion.split('.')[0];
}
public isProductionReady(): boolean {
return isProductionReady(this.highestVersion());
}

View File

@ -9,6 +9,7 @@
namespace Flarum\ExtensionManager\Api\Resource;
use Composer\Semver\Semver;
use Flarum\Api\Endpoint;
use Flarum\Api\Resource\AbstractResource;
use Flarum\Api\Resource\Contracts\Countable;
@ -31,7 +32,7 @@ use Tobyz\JsonApiServer\Schema\CustomFilter;
class ExternalExtensionResource extends AbstractResource implements Listable, Paginatable, Countable
{
protected int|null $totalResults = null;
protected ?int $totalResults = null;
public function __construct(
protected Repository $cache,
@ -81,6 +82,11 @@ class ExternalExtensionResource extends AbstractResource implements Listable, Pa
Schema\Boolean::make('compatibleWithLatestFlarum')
->property('compatible_with_latest_flarum'),
Schema\Integer::make('downloads'),
Schema\Boolean::make('isSupported')
->get(function (Extension $extension) {
return Semver::satisfies(Application::VERSION, $extension->latest_flarum_version_supported);
}),
];
}

View File

@ -9,6 +9,7 @@
namespace Flarum\ExtensionManager\Command;
use Composer\Semver\Semver;
use Flarum\Extension\Extension;
use Flarum\Extension\ExtensionManager;
use Flarum\ExtensionManager\Composer\ComposerAdapter;
@ -16,16 +17,21 @@ use Flarum\ExtensionManager\Composer\ComposerJson;
use Flarum\ExtensionManager\Exception\ComposerCommandFailedException;
use Flarum\ExtensionManager\Settings\LastUpdateCheck;
use Flarum\ExtensionManager\Support\Util;
use Flarum\Foundation\Application;
use GuzzleHttp\Client;
use Illuminate\Support\Collection;
use Symfony\Component\Console\Input\ArrayInput;
class CheckForUpdatesHandler
{
protected array $meta = [];
public function __construct(
protected ComposerAdapter $composer,
protected LastUpdateCheck $lastUpdateCheck,
protected ExtensionManager $extensions,
protected ComposerJson $composerJson
protected ComposerJson $composerJson,
protected Client $http
) {
}
@ -97,6 +103,10 @@ class CheckForUpdatesHandler
$mainPackageUpdate['required-as'] = $composerJson['require'][$mainPackageUpdate['name']] ?? null;
if (! $this->compatibleWithCurrentFlarumVersion($mainPackageUpdate)) {
continue;
}
$updates->push($mainPackageUpdate);
}
@ -136,4 +146,49 @@ class CheckForUpdatesHandler
return $output->getContents();
}
private function compatibleWithCurrentFlarumVersion(array $mainPackageUpdate): bool
{
if (empty($mainPackageUpdate['latest-major']) || str_contains($mainPackageUpdate['latest-major'], 'dev-')) {
return true;
}
if (! empty($this->meta[$mainPackageUpdate['name']])) {
$json = $this->meta[$mainPackageUpdate['name']];
} else {
$response = $this->http->get("https://repo.packagist.org/p2/{$mainPackageUpdate['name']}.json");
$body = $response->getBody()->getContents();
if ($response->getStatusCode() > 299 || $response->getStatusCode() < 200) {
return true;
}
$json = json_decode($body, true);
$this->meta[$mainPackageUpdate['name']] = $json;
}
$packages = new Collection($json['packages'][$mainPackageUpdate['name']] ?? []);
if ($packages->isEmpty()) {
return true;
}
$package = $packages->firstWhere('version', $mainPackageUpdate['latest-major']);
if (! $package) {
return true;
}
$flarumVersion = Application::VERSION;
$require = $package['require']['flarum/core'] ?? null;
if (! $require || str_contains($require, 'dev-')) {
return true;
}
return Semver::satisfies($flarumVersion, $require);
}
}

View File

@ -20,6 +20,11 @@ use Throwable;
class ComposerCommandJob extends AbstractJob implements ShouldBeUnique
{
/**
* The number of seconds the job can run before timing out.
*/
public int $timeout = 60 * 3;
public function __construct(
protected AbstractActionCommand $command,
protected string $phpVersion

View File

@ -21,6 +21,9 @@ class Util
if (str_starts_with($currentVersion, 'v')) {
$currentVersion = substr($currentVersion, 1);
}
if (str_starts_with($latestVersion, 'v')) {
$latestVersion = substr($latestVersion, 1);
}
$currentVersion = explode('.', $currentVersion);
$latestVersion = explode('.', $latestVersion);

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1",
"flarum/core": "^2.0.0-beta.2",
"pusher/pusher-php-server": "^7.2"
},
"require-dev": {

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"autoload": {
"psr-4": {

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"autoload": {
"psr-4": {

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"autoload": {
"psr-4": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -51,7 +51,7 @@ export default class SubscriptionMenu<CustomAttrs extends ISubscriptionMenuAttrs
const discussion = this.attrs.discussion;
const subscription = discussion.subscription();
const buttonAttrs = this.possibleButtonAttrs[subscription];
const buttonAttrs = this.possibleButtonAttrs[subscription ?? 'null'] ?? this.possibleButtonAttrs.null;
const preferences = app.session.user!.preferences()!;
const notifyEmail = preferences['notify_newPost_email'];

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"autoload": {
"psr-4": {

View File

@ -26,7 +26,7 @@ class UserResourceFields
Schema\Str::make('suspendMessage')
->writable($canSuspend)
->visible(fn (User $user, Context $context) => $context->getActor()->id === $user->id || $canSuspend($user, $context)),
Schema\Date::make('suspendedUntil')
Schema\DateTime::make('suspendedUntil')
->writable($canSuspend)
->visible(fn (User $user, Context $context) => $context->getActor()->id === $user->id || $canSuspend($user, $context))
->nullable(),

View File

@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^2.0.0-beta.1"
"flarum/core": "^2.0.0-beta.2"
},
"autoload": {
"psr-4": {

View File

@ -1,5 +1,15 @@
export default class TagHero extends Component<import("flarum/common/Component").ComponentAttrs, undefined> {
constructor();
view(): JSX.Element;
/**
* @returns {ItemList<Mithril.Children>}
*/
viewItems(): ItemList<Mithril.Children>;
/**
* @returns {ItemList<Mithril.Children>}
*/
contentItems(): ItemList<Mithril.Children>;
}
import Component from "flarum/common/Component";
import ItemList from "flarum/common/utils/ItemList";
import Mithril from "mithril";

2
extensions/tags/js/dist/admin.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
extensions/tags/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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -78,14 +78,14 @@ export default function () {
'tag',
<Dropdown
className="Dropdown--restrictByTag"
buttonClassName="Button Button--text"
buttonClassName="Button Button--link"
label={app.translator.trans('flarum-tags.admin.permissions.restrict_by_tag_heading')}
icon="fas fa-plus"
caretIcon={null}
>
{tags.map((tag) => (
<Button icon={true} onclick={() => tag.save({ isRestricted: true })}>
{[tagIcon(tag, { className: 'Button-icon' }), ' ', tag.name()]}
<Button icon={tagIcon(tag, { className: 'Button-icon' })} onclick={() => tag.save({ isRestricted: true })}>
{tag.name()}
</Button>
))}
</Dropdown>

View File

@ -19,7 +19,12 @@ function tagItem(tag) {
<div className="TagListItem-info">
{tagIcon(tag)}
<span className="TagListItem-name">{tag.name()}</span>
<Button className="Button Button--link" icon="fas fa-pencil-alt" onclick={() => app.modal.show(EditTagModal, { model: tag })} />
<Button
className="Button Button--link"
icon="fas fa-pencil-alt"
aria-label={app.translator.trans('flarum-tags.admin.tags.edit_tag_label', { tag: tag.name() })}
onclick={() => app.modal.show(EditTagModal, { model: tag })}
/>
</div>
{!tag.isChild() && tag.position() !== null && (
<ol className="TagListItem-children TagList">

View File

@ -2,6 +2,8 @@ import Component from 'flarum/common/Component';
import textContrastClass from 'flarum/common/helpers/textContrastClass';
import tagIcon from '../../common/helpers/tagIcon';
import classList from 'flarum/common/utils/classList';
import ItemList from 'flarum/common/utils/ItemList';
import Mithril from 'mithril';
export default class TagHero extends Component {
view() {
@ -13,15 +15,39 @@ export default class TagHero extends Component {
className={classList('Hero', 'TagHero', { 'TagHero--colored': color, [textContrastClass(color)]: color })}
style={color ? { '--hero-bg': color } : undefined}
>
<div className="container">
<div className="containerNarrow">
<h1 className="Hero-title">
{tag.icon() && tagIcon(tag, {}, { useColor: false })} {tag.name()}
</h1>
<div className="Hero-subtitle">{tag.description()}</div>
</div>
</div>
<div className="container">{this.viewItems().toArray()}</div>
</header>
);
}
/**
* @returns {ItemList<Mithril.Children>}
*/
viewItems() {
const items = new ItemList();
items.add('content', <div className="containerNarrow">{this.contentItems().toArray()}</div>, 80);
return items;
}
/**
* @returns {ItemList<Mithril.Children>}
*/
contentItems() {
const items = new ItemList();
const tag = this.attrs.model;
items.add(
'tag-title',
<h1 className="Hero-title">
{tag.icon() && tagIcon(tag, {}, { useColor: false })} {tag.name()}
</h1>,
100
);
items.add('tag-subtitle', <div className="Hero-subtitle">{tag.description()}</div>, 90);
return items;
}
}

View File

@ -3,8 +3,6 @@
width: 16px;
height: 16px;
display: inline-block;
vertical-align: -3px;
margin-left: 1px;
background: var(--color, var(--control-bg));
&.untagged {

View File

@ -57,6 +57,8 @@
position: absolute;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background-color: transparent;
}
@ -76,6 +78,8 @@
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
display: flex;
align-items: center;
&.pinned:not(.child) {
padding-top: 10px;
@ -99,16 +103,9 @@
background: var(--control-bg);
}
.icon::before {
display: inline-block;
width: 16px;
text-align: center;
vertical-align: middle;
}
&.selected {
.SelectTagListItem-checkIcon {
display: inline-block;
display: inline-flex;
color: var(--muted-color);
font-size: 14px;
}
@ -123,10 +120,12 @@
}
.SelectTagListItem-icon {
position: relative;
display: inline-block;
vertical-align: top;
margin-top: 3px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 0;
height: 16px;
width: 16px;
}
.SelectTagListItem-name {
display: inline-block;

View File

@ -44,7 +44,7 @@
opacity: 1;
}
@media @tablet-up {
.IndexPage, .UserPage {
.IndexPage, .UserPage, .DiscussionList--floatingTags {
.DiscussionListItem-title {
margin-right: 155px;
}

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