mirror of
https://github.com/flarum/core.git
synced 2025-08-16 21:34:08 +02:00
Compare commits
313 Commits
as/search-
...
mithril-2-
Author | SHA1 | Date | |
---|---|---|---|
|
dd17fa3342 | ||
|
db8755d5d9 | ||
|
5c2a233b74 | ||
|
4dd0361ccf | ||
|
ccd2b0ee0b | ||
|
b954e79ec8 | ||
|
bdc64f98a5 | ||
|
f9e5aa4193 | ||
|
801220cc10 | ||
|
bd52aaaa61 | ||
|
4a5d12626b | ||
|
1e4a7f3282 | ||
|
cbcb0429d0 | ||
|
14b12f8ae8 | ||
|
37d7068569 | ||
|
6e1a9193ac | ||
|
ea6aea409d | ||
|
d572200cfb | ||
|
e10ebf0489 | ||
|
b251ff0469 | ||
|
4a0947db4b | ||
|
32767d6321 | ||
|
27017b7181 | ||
|
bdd30ceecc | ||
|
abc98645a0 | ||
|
5d538adc33 | ||
|
6f11567f91 | ||
|
537e2690e8 | ||
|
0e715a8c40 | ||
|
383a7e559f | ||
|
aeaa9a4b73 | ||
|
85f8fc52b7 | ||
|
174f3aba90 | ||
|
47ce93d2fd | ||
|
24935eacaf | ||
|
0f99e7c015 | ||
|
691ae85e50 | ||
|
fb83f8c59c | ||
|
a6e6c54972 | ||
|
6b5bdb5c41 | ||
|
d7ef260c54 | ||
|
571ed8d8e5 | ||
|
42ad490096 | ||
|
09bead3ba2 | ||
|
abb896d430 | ||
|
6e7c86ac50 | ||
|
2673dd2ee3 | ||
|
cbd9c8dd4f | ||
|
29d995de45 | ||
|
a5b2768836 | ||
|
398951f282 | ||
|
07f04c7ba7 | ||
|
c8d5ca51bb | ||
|
60dbd3f26c | ||
|
e7f6e37799 | ||
|
27ffeb204e | ||
|
884a5cf3b9 | ||
|
e895ca738d | ||
|
f41aec1043 | ||
|
4c73d76668 | ||
|
f67194484b | ||
|
c8f47d519d | ||
|
e9b267a33a | ||
|
554c72c6db | ||
|
fb2b0a1d3e | ||
|
2ac18a39ed | ||
|
6ed3cb56d4 | ||
|
038744f092 | ||
|
2edbd4508a | ||
|
a6d4658dff | ||
|
955c8121d3 | ||
|
fd2dcd38d6 | ||
|
520c7e7d0f | ||
|
5ec9c52b04 | ||
|
0341e64057 | ||
|
f1a480d3d7 | ||
|
aceac88013 | ||
|
8cc9e18990 | ||
|
94d3bea53e | ||
|
9e6cfcf05a | ||
|
b8abd2522e | ||
|
376a00f24f | ||
|
43bfaa7400 | ||
|
7ba8a7122b | ||
|
88bc291c86 | ||
|
e699ada1cc | ||
|
2186584878 | ||
|
7beeae6269 | ||
|
e0e3d6ecae | ||
|
f8bc58fd1a | ||
|
764f50f469 | ||
|
e8485db484 | ||
|
f017d7afbe | ||
|
44c1e91f05 | ||
|
4b05e0073a | ||
|
3764abee51 | ||
|
015cedb29d | ||
|
32e9fa1f0b | ||
|
2bdf0d7096 | ||
|
b889fa1bbf | ||
|
76c3494f9b | ||
|
6bcb76b914 | ||
|
63984b43f9 | ||
|
389bc59745 | ||
|
2eb28ea396 | ||
|
169b0fbd9b | ||
|
6b178b8204 | ||
|
1d6e985107 | ||
|
f1fc0fecb7 | ||
|
94d8f7e726 | ||
|
b0891c42da | ||
|
7d5bebb70a | ||
|
b78db0268a | ||
|
fcda092558 | ||
|
f6dd87f72f | ||
|
d8d43d95e0 | ||
|
51c7b17305 | ||
|
b592ceb199 | ||
|
a083876b5f | ||
|
b0cbe277c2 | ||
|
b1d948becc | ||
|
a48568b17a | ||
|
e5cebd85ed | ||
|
fbd5f6245b | ||
|
3ce63bc035 | ||
|
bac5e7c94c | ||
|
97b0e61f61 | ||
|
604989be72 | ||
|
f9b1dfe499 | ||
|
f611a44a08 | ||
|
911f1fd5c9 | ||
|
0087b956ef | ||
|
99b119f1fa | ||
|
6be37fd376 | ||
|
46741f63fe | ||
|
cec00c0dd6 | ||
|
5f2c9da2f5 | ||
|
92de05e911 | ||
|
68aa6e26da | ||
|
8b7fa012c7 | ||
|
61231debd3 | ||
|
e771ec90c4 | ||
|
ab85b49845 | ||
|
04e5d5884f | ||
|
ddc1141106 | ||
|
a48cc19814 | ||
|
6d18b700ec | ||
|
b2bc427b3f | ||
|
1f94ffc842 | ||
|
3d91268493 | ||
|
81fd986881 | ||
|
d1b0030292 | ||
|
2c395a781c | ||
|
21861f231b | ||
|
537f5e833e | ||
|
4d45aaa9ae | ||
|
529f8e5f32 | ||
|
991d90bf4a | ||
|
38fed603f8 | ||
|
9691a6ab92 | ||
|
e8e4b64d7d | ||
|
35d76515d3 | ||
|
5d34124a02 | ||
|
16a6f82e8f | ||
|
8a0c241a8e | ||
|
a376c0e596 | ||
|
5ccf9d420e | ||
|
742f89f660 | ||
|
916cf4b546 | ||
|
f127e67fd4 | ||
|
3e79c3e3ff | ||
|
0172dfd79c | ||
|
eb627544fa | ||
|
73c0a90da7 | ||
|
ca0f8f2d72 | ||
|
090df13e7f | ||
|
d1a1277f88 | ||
|
31b2ab1b2b | ||
|
2590073a50 | ||
|
4402dc81ac | ||
|
f93a255a2f | ||
|
ddb0a9f1ce | ||
|
f44caf1600 | ||
|
89b6847710 | ||
|
2fb885175a | ||
|
7d9db2f4ae | ||
|
5d073941c9 | ||
|
f664fa5be7 | ||
|
aa4b58d7aa | ||
|
3120eb6f63 | ||
|
aac54a1d28 | ||
|
0d0841d019 | ||
|
83d2dbd290 | ||
|
7e5b40c532 | ||
|
6c9971eeba | ||
|
37a690833a | ||
|
fa2301b5c1 | ||
|
edca7b93ec | ||
|
095dce9a3e | ||
|
479f655bb3 | ||
|
41d6e91318 | ||
|
87414995b6 | ||
|
2c93b5f801 | ||
|
7498f5e506 | ||
|
79f5291f04 | ||
|
ed3b923f58 | ||
|
1ce06611ce | ||
|
27bacd779b | ||
|
02acacfdcb | ||
|
23d95a7566 | ||
|
43164df79e | ||
|
46e704b27b | ||
|
e55867acb4 | ||
|
3596425bde | ||
|
b43452223f | ||
|
70697be8c0 | ||
|
c20ae678f5 | ||
|
30a61b8b42 | ||
|
824fe95346 | ||
|
8475d176e0 | ||
|
95f4dc771d | ||
|
68caf45f33 | ||
|
f72d118bec | ||
|
652d961907 | ||
|
71178245fc | ||
|
cfd1f01299 | ||
|
674f55e91d | ||
|
f897b58f29 | ||
|
5c49b71c02 | ||
|
b9ba5b63f1 | ||
|
6e88dfb2cb | ||
|
c899e11070 | ||
|
784b5cc03c | ||
|
c17d7cd23f | ||
|
4af34265cc | ||
|
1616d8f1c7 | ||
|
57b85f501a | ||
|
91609b8a71 | ||
|
9d8b466d57 | ||
|
02154d05e5 | ||
|
a454b185e9 | ||
|
84c5248872 | ||
|
2a784009fb | ||
|
070865f825 | ||
|
9615fd3e39 | ||
|
dde9c9c51b | ||
|
6547290472 | ||
|
74f6a3e6ce | ||
|
98740472a8 | ||
|
1e9825de4f | ||
|
d14ca79dfa | ||
|
5cd5f27769 | ||
|
d05b63eddd | ||
|
48c7354c08 | ||
|
0fe635d32c | ||
|
2c2a42030a | ||
|
37d2902cd1 | ||
|
66c2f6b76a | ||
|
4d68636544 | ||
|
73891b751b | ||
|
fe011bf285 | ||
|
524dde31d4 | ||
|
b69fc01ab3 | ||
|
20e69e6351 | ||
|
d097e7ed4b | ||
|
63e07b2044 | ||
|
60c3f23667 | ||
|
9793c10610 | ||
|
29af3e6d8b | ||
|
703c3442da | ||
|
e0d3a8c733 | ||
|
527748ff66 | ||
|
eb440bb9b6 | ||
|
d71f77d592 | ||
|
8b891abb2b | ||
|
5f0dcc71ba | ||
|
4fcafe3b2f | ||
|
29065e1ee9 | ||
|
2b39bd7a0d | ||
|
94fe3236d7 | ||
|
3b6e5a0caf | ||
|
9ac8f1543a | ||
|
439e3a5a9a | ||
|
883e1a9d6a | ||
|
2ac2edbbad | ||
|
18148141c3 | ||
|
10f4223028 | ||
|
e10220ae47 | ||
|
dcd14821c2 | ||
|
edeaa5855c | ||
|
cc91244f1a | ||
|
5faab8bdbd | ||
|
99d79e7571 | ||
|
5606eae0f1 | ||
|
62cb71d4e1 | ||
|
e28ba4acff | ||
|
5b3914535d | ||
|
44975bc606 | ||
|
f7931e8a30 | ||
|
3ba655b2f9 | ||
|
735ecab446 | ||
|
64d4eb8c4c | ||
|
e4f1a397d6 | ||
|
5a244dcfd2 | ||
|
b60309284c | ||
|
392fe98c02 | ||
|
873f489fec | ||
|
9d8374208f | ||
|
07551b4890 | ||
|
b2c147c147 | ||
|
5bf5bd36ee | ||
|
4435ff193a | ||
|
5c30f8fa67 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -11,5 +11,3 @@ phpunit.xml export-ignore
|
||||
tests export-ignore
|
||||
|
||||
js/dist/* -diff
|
||||
|
||||
* text=auto eol=lf
|
||||
|
97
CHANGELOG.md
97
CHANGELOG.md
@@ -1,102 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [0.1.0-beta.14](https://github.com/flarum/core/compare/v0.1.0-beta.13...v0.1.0-beta.14)
|
||||
|
||||
### Added
|
||||
|
||||
- Check dependencies before enabling / disabling extensions (https://github.com/flarum/core/pull/2188)
|
||||
- Set up temporary infrastructure for TypeScript in core (https://github.com/flarum/core/pull/2206)
|
||||
- Better UI for request error modals (https://github.com/flarum/core/pull/1929)
|
||||
- Display name extender, tests, frontend UI (https://github.com/flarum/core/pull/2174)
|
||||
- Scroll to post or show alert when editing a post from another page (https://github.com/flarum/core/pull/2108)
|
||||
- Feature to test email config by sending an email to the current user (https://github.com/flarum/core/pull/2023)
|
||||
- Allow searching users by group ID using the group gambit (https://github.com/flarum/core/pull/2192)
|
||||
- Use `liveHumanTimes` helper to update times without reload/rerender (https://github.com/flarum/core/pull/2208)
|
||||
- View extender, tests (https://github.com/flarum/core/pull/2134)
|
||||
- User extender to replace `PrepareUserGroups` (https://github.com/flarum/core/pull/2110)
|
||||
- Increase extensibility of skeleton PHP (https://github.com/flarum/core/pull/2308, https://github.com/flarum/core/pull/2318)
|
||||
- Pass a translator instance to `getEmailSubject` in `MailableInterface` (https://github.com/flarum/core/pull/2244)
|
||||
- Force LF line endings on windows (https://github.com/flarum/core/pull/2321)
|
||||
- Add a `Link` component for internal and external links (https://github.com/flarum/core/pull/2315)
|
||||
- `ConfirmDocumentUnload` component
|
||||
- Error handler middleware can now be manipulated by the middleware extender
|
||||
|
||||
### Changed
|
||||
|
||||
- Update to Mithril 2 (https://github.com/flarum/core/pull/2255)
|
||||
- Stop storing component instances (https://github.com/flarum/core/issues/1821, https://github.com/flarum/core/issues/2144)
|
||||
- Update to Laravel 6.x (https://github.com/flarum/core/issues/2055)
|
||||
- `Flarum\Foundation\Application` no longer implements `Illuminate\Contracts\Foundation\Application` (#2142)
|
||||
- `Flarum\Foundation\Application` no longer inherits `Illuminate\Container\Container` (#2142)
|
||||
- `paths` have been split off from `Flarum\Foundation\Application` into `Flarum\Foundation\Paths`, which can be injected where needed (#2142)
|
||||
- `Flarum\User\Gate` no longer implements `Illuminate\Contracts\Auth\Access\Gate` (https://github.com/flarum/core/pull/2181)
|
||||
- Improve Group Gambit performance (https://github.com/flarum/core/pull/2192)
|
||||
- Switch to `dayjs` from `momentjs` (https://github.com/flarum/core/pull/2219)
|
||||
- Don't create a `bio` column in `users` for new installations (https://github.com/flarum/core/pull/2215)
|
||||
- Start converting core JS to TypeScript (https://github.com/flarum/core/pull/2207)
|
||||
- Make Carbon an explicit dependency (https://github.com/flarum/core/commit/3b39c212e0fef7522e7d541a9214ff3817138d5d)
|
||||
- Use Symfony's translator interface instead of Laravel's (https://github.com/flarum/core/pull/2243)
|
||||
- Use newer versions of fontawesome (https://github.com/flarum/core/pull/2274)
|
||||
- Use URL generator instead of `app()->url()` where possible (https://github.com/flarum/core/pull/2302)
|
||||
- Move config from `config.php` into an injectable helper class (https://github.com/flarum/core/pull/2271)
|
||||
- Use reserved TLD for bogus and test urls (https://github.com/flarum/core/commit/6860b24b70bd04544dde90e537ce021a5fc5a689)
|
||||
- Replace `m.stream` with `flarum/utils/Stream` (https://github.com/flarum/core/pull/2316)
|
||||
- Replace `affixedSidebar` util with `AffixedSidebar` component
|
||||
- Replace `m.withAttr` with `flarum/utils/withAttr`
|
||||
- Scroll Listener is now passive, performance improvement (https://github.com/flarum/core/pull/2387)
|
||||
|
||||
### Fixed
|
||||
|
||||
- `generate:migration` command for extensions (https://github.com/flarum/core/commit/443949f7b9d7558dbc1e0994cb898cbac59bec87)
|
||||
- Container config for `UninstalledSite` (https://github.com/flarum/core/commit/ecdce44d555dd36a365fd472b2916e677ef173cf)
|
||||
- Tooltip glitch on page chang (https://github.com/flarum/core/issues/2118)
|
||||
- Using multiple extenders in tests (https://github.com/flarum/core/commit/c4f4f218bf4b175a30880b807f9ccb1a37a25330)
|
||||
- Header glitch when opening modals (https://github.com/flarum/core/pull/2131)
|
||||
- Ensure `SameSite` is explicitly set for cookies (https://github.com/flarum/core/pull/2159)
|
||||
- Ensure `Flarum\User\Event\AvatarChanged` event is properly dispatched (https://github.com/flarum/core/pull/2197)
|
||||
- Show correct error message on wrong password when changing email (https://github.com/flarum/core/pull/2171)
|
||||
- Discussion unreadCount could be higher than commentCount if posts deleted (https://github.com/flarum/core/pull/2195)
|
||||
- Don't show page title on the default route (https://github.com/flarum/core/pull/2047)
|
||||
- Add page title to `All Discussions` page when it isn't the default route (https://github.com/flarum/core/pull/2047)
|
||||
- Accept `'0'` as `false` for `flarum/components/Checkbox` (https://github.com/flarum/core/pull/2210)
|
||||
- Fix PostStreamScrubber background (https://github.com/flarum/core/pull/2222)
|
||||
- Test port on BaseUrl tests (https://github.com/flarum/core/pull/2226)
|
||||
- `UrlGenerator` can now generate urls with optional parameters (https://github.com/flarum/core/pull/2246)
|
||||
- Allow `less` to be compiled independently of Flarum (https://github.com/flarum/core/pull/2252)
|
||||
- Use correct number abbreviation (https://github.com/flarum/core/pull/2261)
|
||||
- Ensure avatar html uses alt tags for accessibility (https://github.com/flarum/core/pull/2269)
|
||||
- Escape regex when searching (https://github.com/flarum/core/pull/2273)
|
||||
- Remove unneeded semicolons inserted during JS compilation (https://github.com/flarum/core/pull/2280)
|
||||
- Don't require a username/password for SMTP (https://github.com/flarum/core/pull/2287)
|
||||
- Allow uppercase entries for SMTP encryption validation (https://github.com/flarum/core/pull/2289)
|
||||
- Ensure that the right number of posts is returned from list posts API (https://github.com/flarum/core/pull/2291)
|
||||
- Fix a variety of PostStream bugs (https://github.com/flarum/core/pull/2160, https://github.com/flarum/core/pull/2160)
|
||||
- Sliding discussion glitch on mobile (https://github.com/flarum/core/pull/2324)
|
||||
- Sliding discussion button in wrong place (https://github.com/flarum/core/pull/2330, https://github.com/flarum/core/pull/2383)
|
||||
- Sliding discussion glitch on mobile (https://github.com/flarum/core/pull/2381)
|
||||
- Fix PostStream for posts with top margins, and scrubber position when scrolling below posts (https://github.com/flarum/core/pull/2369)
|
||||
|
||||
### Removed
|
||||
|
||||
- `Flarum\Event\AbstractConfigureRoutes` event class
|
||||
- `Flarum\Event\ConfigureApiRoutes` event class
|
||||
- `Flarum\Event\ConfigureForumRoutes` event class
|
||||
- `Flarum\Console\Event\Configuring` event class
|
||||
- `Flarum\Event\ConfigureModelDates` event class
|
||||
- `Flarum\Event\ConfigureLocales` event class
|
||||
- `Flarum\Event\ConfigureModelDefaultAttributes` event class
|
||||
- `Flarum\Event\GetModelRelationship` event class
|
||||
- `Flarum\User\Event\BioChanged` event class
|
||||
- `Flarum\Database\MigrationServiceProvider` moved into `Flarum\Database\DatabaseServiceProvider`
|
||||
- Unused `admin/components/Widget` component (`admin/component/DashboardWidget` should be used instead)
|
||||
- Mandrill mail driver (https://github.com/flarum/core/commit/bca833d3f1c34d45d95bf905902368a2753b8908)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- `Flarum\User\Event\GetDisplayName` event class
|
||||
- Global path helpers, `Flarum\Foundation\Application` path methods (https://github.com/flarum/core/pull/2155)
|
||||
- `Flarum\User\AssertPermissionTrait` (https://github.com/flarum/core/pull/2044)
|
||||
|
||||
## [0.1.0-beta.13](https://github.com/flarum/core/compare/v0.1.0-beta.12...v0.1.0-beta.13)
|
||||
|
||||
### Added
|
||||
|
13
README.md
13
README.md
@@ -1,14 +1,12 @@
|
||||
<p align="center"><img src="https://flarum.org/assets/img/logo.png"></p>
|
||||
<p align="center"><img src="https://flarum.org/img/logo.png"></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/flarum/core/actions?query=workflow%3ATests"><img src="https://github.com/flarum/core/workflows/Tests/badge.svg" alt="PHP Tests"></a>
|
||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/packagist/dt/flarum/core" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/github/v/release/flarum/core?sort=semver" alt="Latest Version"></a>
|
||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/packagist/l/flarum/core" alt="License"></a>
|
||||
<a href="https://github.styleci.io/repos/28257573"><img src="https://github.styleci.io/repos/28257573/shield?style=flat" alt="StyleCI"></a>
|
||||
<a href="https://travis-ci.org/flarum/core"><img src="https://travis-ci.org/flarum/core.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/d/total.svg" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/v/stable.svg" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/license.svg" alt="License"></a>
|
||||
</p>
|
||||
|
||||
|
||||
## About Flarum
|
||||
|
||||
**[Flarum](https://flarum.org/) is a delightfully simple discussion platform for your website.** It's fast and easy to use, with all the features you need to run a successful community. It is designed to be:
|
||||
@@ -34,3 +32,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).
|
||||
|
||||
|
@@ -10,7 +10,7 @@
|
||||
"email": "franz@develophp.org"
|
||||
},
|
||||
{
|
||||
"name": "Daniël Klabbers",
|
||||
"name": "Daniel Klabbers",
|
||||
"email": "daniel@klabbers.email",
|
||||
"homepage": "https://luceos.com"
|
||||
},
|
||||
@@ -27,10 +27,6 @@
|
||||
{
|
||||
"name": "Matthew Kilgore",
|
||||
"email": "matthew@kilgore.dev"
|
||||
},
|
||||
{
|
||||
"name": "Alexander (Sasha) Skvortsov",
|
||||
"email": "askvortsov@flarum.org"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
@@ -76,11 +72,11 @@
|
||||
"psr/http-server-handler": "^1.0",
|
||||
"psr/http-server-middleware": "^1.0",
|
||||
"s9e/text-formatter": "^2.3.6",
|
||||
"symfony/config": "^4.3.4",
|
||||
"symfony/console": "^4.3.4",
|
||||
"symfony/event-dispatcher": "^4.3.4",
|
||||
"symfony/translation": "^4.3.4",
|
||||
"symfony/yaml": "^4.3.4",
|
||||
"symfony/config": "^3.3",
|
||||
"symfony/console": "^4.2",
|
||||
"symfony/event-dispatcher": "^4.3.2",
|
||||
"symfony/translation": "^3.3",
|
||||
"symfony/yaml": "^3.3",
|
||||
"tobscure/json-api": "^0.3.0",
|
||||
"wikimedia/less.php": "^3.0"
|
||||
},
|
||||
|
14
js/dist/admin.js
vendored
14
js/dist/admin.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/admin.js.map
vendored
2
js/dist/admin.js.map
vendored
File diff suppressed because one or more lines are too long
16
js/dist/forum.js
vendored
16
js/dist/forum.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/forum.js.map
vendored
2
js/dist/forum.js.map
vendored
File diff suppressed because one or more lines are too long
27
js/package-lock.json
generated
27
js/package-lock.json
generated
@@ -3556,9 +3556,9 @@
|
||||
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
|
||||
},
|
||||
"jquery": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
|
||||
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
|
||||
"integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
|
||||
},
|
||||
"jquery.hotkeys": {
|
||||
"version": "0.1.0",
|
||||
@@ -4546,6 +4546,11 @@
|
||||
"integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==",
|
||||
"dev": true
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
|
||||
"integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ=="
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
@@ -4895,29 +4900,21 @@
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz",
|
||||
"integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==",
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
|
||||
"integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
|
||||
"requires": {
|
||||
"cacache": "^12.0.2",
|
||||
"find-cache-dir": "^2.1.0",
|
||||
"is-wsl": "^1.1.0",
|
||||
"schema-utils": "^1.0.0",
|
||||
"serialize-javascript": "^4.0.0",
|
||||
"serialize-javascript": "^2.1.2",
|
||||
"source-map": "^0.6.1",
|
||||
"terser": "^4.1.2",
|
||||
"webpack-sources": "^1.4.0",
|
||||
"worker-farm": "^1.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"serialize-javascript": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
|
||||
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
|
||||
"requires": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
|
@@ -10,7 +10,7 @@
|
||||
"dayjs": "^1.8.28",
|
||||
"expose-loader": "^0.7.5",
|
||||
"flarum-webpack-config": "0.1.0-beta.10",
|
||||
"jquery": "^3.5.1",
|
||||
"jquery": "^3.4.1",
|
||||
"jquery.hotkeys": "^0.1.0",
|
||||
"lodash-es": "^4.17.14",
|
||||
"m.attrs.bidi": "github:tobscure/m.attrs.bidi",
|
||||
|
20
js/shims.d.ts
vendored
20
js/shims.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
// Mithril
|
||||
import Mithril from 'mithril';
|
||||
import * as Mithril from 'mithril';
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
// Other third-party libs
|
||||
import * as _dayjs from 'dayjs';
|
||||
@@ -8,6 +9,21 @@ import * as _$ from 'jquery';
|
||||
// Globals from flarum/core
|
||||
import Application from './src/common/Application';
|
||||
|
||||
/**
|
||||
* Helpers that flarum/core patches into Mithril
|
||||
*/
|
||||
interface m extends Mithril.Static {
|
||||
prop: typeof Stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export Mithril typings globally.
|
||||
*
|
||||
* This lets us use these typings without an extra import everywhere we use
|
||||
* Mithril in a TypeScript file.
|
||||
*/
|
||||
export as namespace Mithril;
|
||||
|
||||
/**
|
||||
* flarum/core exposes several extensions globally:
|
||||
*
|
||||
@@ -20,7 +36,7 @@ import Application from './src/common/Application';
|
||||
*/
|
||||
declare global {
|
||||
const $: typeof _$;
|
||||
const m: Mithril.Static;
|
||||
const m: m;
|
||||
const dayjs: typeof _dayjs;
|
||||
}
|
||||
|
||||
|
@@ -27,19 +27,20 @@ export default class AdminApplication extends Application {
|
||||
* @inheritdoc
|
||||
*/
|
||||
mount() {
|
||||
// Mithril does not render the home route on https://example.com/admin, so
|
||||
// we need to go to https://example.com/admin#/ explicitly.
|
||||
if (!document.location.hash) document.location.hash = '#/';
|
||||
|
||||
m.route.prefix = '#';
|
||||
super.mount();
|
||||
|
||||
m.mount(document.getElementById('app-navigation'), { view: () => Navigation.component({ className: 'App-backControl', drawer: true }) });
|
||||
m.mount(document.getElementById('header-navigation'), Navigation);
|
||||
m.mount(document.getElementById('header-primary'), HeaderPrimary);
|
||||
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
|
||||
m.mount(document.getElementById('admin-navigation'), AdminNav);
|
||||
|
||||
// Mithril does not render the home route on https://example.com/admin, so
|
||||
// we need to go to https://example.com/admin#/ explicitly.
|
||||
if (!document.location.hash) document.location.hash = '#/';
|
||||
|
||||
m.route.prefix = '#';
|
||||
|
||||
super.mount();
|
||||
|
||||
// If an extension has just been enabled, then we will run its settings
|
||||
// callback.
|
||||
const enabled = localStorage.getItem('enabledExtension');
|
||||
|
@@ -1,21 +1,21 @@
|
||||
import Page from '../../common/components/Page';
|
||||
import Button from '../../common/components/Button';
|
||||
import Switch from '../../common/components/Switch';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
import EditCustomCssModal from './EditCustomCssModal';
|
||||
import EditCustomHeaderModal from './EditCustomHeaderModal';
|
||||
import EditCustomFooterModal from './EditCustomFooterModal';
|
||||
import UploadImageButton from './UploadImageButton';
|
||||
import saveSettings from '../utils/saveSettings';
|
||||
import withAttr from '../../common/utils/withAttr';
|
||||
|
||||
export default class AppearancePage extends Page {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.primaryColor = Stream(app.data.settings.theme_primary_color);
|
||||
this.secondaryColor = Stream(app.data.settings.theme_secondary_color);
|
||||
this.darkMode = Stream(app.data.settings.theme_dark_mode);
|
||||
this.coloredHeader = Stream(app.data.settings.theme_colored_header);
|
||||
this.primaryColor = m.stream(app.data.settings.theme_primary_color);
|
||||
this.secondaryColor = m.stream(app.data.settings.theme_secondary_color);
|
||||
this.darkMode = m.stream(app.data.settings.theme_dark_mode);
|
||||
this.coloredHeader = m.stream(app.data.settings.theme_colored_header);
|
||||
}
|
||||
|
||||
view() {
|
||||
|
@@ -5,7 +5,6 @@ import Button from '../../common/components/Button';
|
||||
import saveSettings from '../utils/saveSettings';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import Switch from '../../common/components/Switch';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
import withAttr from '../../common/utils/withAttr';
|
||||
|
||||
export default class BasicsPage extends Page {
|
||||
@@ -27,7 +26,7 @@ export default class BasicsPage extends Page {
|
||||
this.values = {};
|
||||
|
||||
const settings = app.data.settings;
|
||||
this.fields.forEach((key) => (this.values[key] = Stream(settings[key])));
|
||||
this.fields.forEach((key) => (this.values[key] = m.stream(settings[key])));
|
||||
|
||||
this.localeOptions = {};
|
||||
const locales = app.data.locales;
|
||||
@@ -194,7 +193,9 @@ export default class BasicsPage extends Page {
|
||||
|
||||
saveSettings(settings)
|
||||
.then(() => {
|
||||
this.successAlert = app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.basics.saved_message'));
|
||||
this.successAlert = app.alerts.show(app.translator.trans('core.admin.basics.saved_message'), {
|
||||
type: 'success',
|
||||
});
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
|
@@ -4,7 +4,7 @@ import Badge from '../../common/components/Badge';
|
||||
import Group from '../../common/models/Group';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import Switch from '../../common/components/Switch';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
import withAttr from '../../common/utils/withAttr';
|
||||
|
||||
/**
|
||||
* The `EditGroupModal` component shows a modal dialog which allows the user
|
||||
@@ -16,11 +16,11 @@ export default class EditGroupModal extends Modal {
|
||||
|
||||
this.group = this.attrs.group || app.store.createRecord('groups');
|
||||
|
||||
this.nameSingular = Stream(this.group.nameSingular() || '');
|
||||
this.namePlural = Stream(this.group.namePlural() || '');
|
||||
this.icon = Stream(this.group.icon() || '');
|
||||
this.color = Stream(this.group.color() || '');
|
||||
this.isHidden = Stream(this.group.isHidden() || false);
|
||||
this.nameSingular = m.stream(this.group.nameSingular() || '');
|
||||
this.namePlural = m.stream(this.group.namePlural() || '');
|
||||
this.icon = m.stream(this.group.icon() || '');
|
||||
this.color = m.stream(this.group.color() || '');
|
||||
this.isHidden = m.stream(this.group.isHidden() || false);
|
||||
}
|
||||
|
||||
className() {
|
||||
|
@@ -123,7 +123,6 @@ export default class ExtensionsPage extends Page {
|
||||
url: app.forum.attribute('apiUrl') + '/extensions/' + id,
|
||||
method: 'PATCH',
|
||||
body: { enabled: !enabled },
|
||||
errorHandler: this.onerror.bind(this),
|
||||
})
|
||||
.then(() => {
|
||||
if (!enabled) localStorage.setItem('enabledExtension', id);
|
||||
@@ -132,27 +131,4 @@ export default class ExtensionsPage extends Page {
|
||||
|
||||
app.modal.show(LoadingModal);
|
||||
}
|
||||
|
||||
onerror(e) {
|
||||
// We need to give the modal animation time to start; if we close the modal too early,
|
||||
// it breaks the bootstrap modal library.
|
||||
// TODO: This workaround should be removed when we move away from bootstrap JS for modals.
|
||||
setTimeout(() => {
|
||||
app.modal.close();
|
||||
}, 300); // Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
|
||||
|
||||
if (e.status !== 409) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
const error = e.response.errors[0];
|
||||
|
||||
app.alerts.show(
|
||||
{ type: 'error' },
|
||||
app.translator.trans(`core.lib.error.${error.code}_message`, {
|
||||
extension: error.extension,
|
||||
extensions: error.extensions.join(', '),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import Alert from '../../common/components/Alert';
|
||||
import Select from '../../common/components/Select';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import saveSettings from '../utils/saveSettings';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
import withAttr from '../../common/utils/withAttr';
|
||||
|
||||
export default class MailPage extends Page {
|
||||
oninit(vnode) {
|
||||
@@ -25,7 +25,7 @@ export default class MailPage extends Page {
|
||||
this.status = { sending: false, errors: {} };
|
||||
|
||||
const settings = app.data.settings;
|
||||
this.fields.forEach((key) => (this.values[key] = Stream(settings[key])));
|
||||
this.fields.forEach((key) => (this.values[key] = m.stream(settings[key])));
|
||||
|
||||
app
|
||||
.request({
|
||||
@@ -40,7 +40,7 @@ export default class MailPage extends Page {
|
||||
for (const driver in this.driverFields) {
|
||||
for (const field in this.driverFields[driver]) {
|
||||
this.fields.push(field);
|
||||
this.values[field] = Stream(settings[field]);
|
||||
this.values[field] = m.stream(settings[field]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +194,9 @@ export default class MailPage extends Page {
|
||||
})
|
||||
.then((response) => {
|
||||
this.sendingTest = false;
|
||||
this.testEmailSuccessAlert = app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.email.send_test_mail_success'));
|
||||
this.testEmailSuccessAlert = app.alerts.show(app.translator.trans('core.admin.email.send_test_mail_success'), {
|
||||
type: 'success',
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.sendingTest = false;
|
||||
@@ -217,7 +219,9 @@ export default class MailPage extends Page {
|
||||
|
||||
saveSettings(settings)
|
||||
.then(() => {
|
||||
this.successAlert = app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.basics.saved_message'));
|
||||
this.successAlert = app.alerts.show(app.translator.trans('core.admin.basics.saved_message'), {
|
||||
type: 'success',
|
||||
});
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import Modal from '../../common/components/Modal';
|
||||
import Button from '../../common/components/Button';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
import saveSettings from '../utils/saveSettings';
|
||||
|
||||
export default class SettingsModal extends Modal {
|
||||
@@ -36,7 +35,7 @@ export default class SettingsModal extends Modal {
|
||||
}
|
||||
|
||||
setting(key, fallback = '') {
|
||||
this.settings[key] = this.settings[key] || Stream(app.data.settings[key] || fallback);
|
||||
this.settings[key] = this.settings[key] || m.stream(app.data.settings[key] || fallback);
|
||||
|
||||
return this.settings[key];
|
||||
}
|
||||
|
@@ -198,19 +198,13 @@ export default class Application {
|
||||
m.route(document.getElementById('content'), basePath + '/', mapRoutes(this.routes, basePath));
|
||||
|
||||
// Add a class to the body which indicates that the page has been scrolled
|
||||
// down. When this happens, we'll add classes to the header and app body
|
||||
// which will set the navbar's position to fixed. We don't want to always
|
||||
// have it fixed, as that could overlap with custom headers.
|
||||
const scrollListener = new ScrollListener((top) => {
|
||||
// down.
|
||||
new ScrollListener((top) => {
|
||||
const $app = $('#app');
|
||||
const offset = $app.offset().top;
|
||||
|
||||
$app.toggleClass('affix', top >= offset).toggleClass('scrolled', top > offset);
|
||||
$('.App-header').toggleClass('navbar-fixed-top', top >= offset);
|
||||
});
|
||||
|
||||
scrollListener.start();
|
||||
scrollListener.update();
|
||||
}).start();
|
||||
|
||||
$(() => {
|
||||
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
|
||||
@@ -278,7 +272,7 @@ export default class Application {
|
||||
/**
|
||||
* Make an AJAX request, handling any low-level errors that may occur.
|
||||
*
|
||||
* @see https://mithril.js.org/request.html
|
||||
* @see https://lhorie.github.io/mithril/mithril.request.html
|
||||
* @param {Object} options
|
||||
* @return {Promise}
|
||||
* @public
|
||||
@@ -410,7 +404,7 @@ export default class Application {
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
this.requestErrorAlert = this.alerts.show(error.alert, error.alert.content);
|
||||
this.requestErrorAlert = this.alerts.show(error.alert.content, error.alert);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import * as Mithril from 'mithril';
|
||||
|
||||
let deprecatedPropsWarned = false;
|
||||
let deprecatedInitPropsWarned = false;
|
||||
export type ComponentAttrs = {
|
||||
className?: string;
|
||||
|
||||
export interface ComponentAttrs extends Mithril.Attributes {}
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
/**
|
||||
* The `Component` class defines a user interface 'building block'. A component
|
||||
@@ -32,7 +33,7 @@ export interface ComponentAttrs extends Mithril.Attributes {}
|
||||
*
|
||||
* @see https://mithril.js.org/components.html
|
||||
*/
|
||||
export default abstract class Component<T extends ComponentAttrs = ComponentAttrs> implements Mithril.ClassComponent<T> {
|
||||
export default abstract class Component<T extends ComponentAttrs = any> implements Mithril.ClassComponent<T> {
|
||||
/**
|
||||
* The root DOM element for the component.
|
||||
*/
|
||||
@@ -131,38 +132,5 @@ export default abstract class Component<T extends ComponentAttrs = ComponentAttr
|
||||
*
|
||||
* This can be used to assign default values for missing, optional attrs.
|
||||
*/
|
||||
protected static initAttrs<T>(attrs: T): void {
|
||||
// Deprecated, part of Mithril 2 BC layer
|
||||
if ('initProps' in this && !deprecatedInitPropsWarned) {
|
||||
deprecatedInitPropsWarned = true;
|
||||
console.warn('initProps is deprecated, please use initAttrs instead.');
|
||||
(this as any).initProps(attrs);
|
||||
}
|
||||
}
|
||||
|
||||
// BEGIN DEPRECATED MITHRIL 2 BC LAYER
|
||||
|
||||
/**
|
||||
* The attributes passed into the component.
|
||||
*
|
||||
* @see https://mithril.js.org/components.html#passing-data-to-components
|
||||
*
|
||||
* @deprecated, use attrs instead.
|
||||
*/
|
||||
get props() {
|
||||
if (!deprecatedPropsWarned) {
|
||||
deprecatedPropsWarned = true;
|
||||
console.warn('this.props is deprecated, please use this.attrs instead.');
|
||||
}
|
||||
return this.attrs;
|
||||
}
|
||||
set props(props) {
|
||||
if (!deprecatedPropsWarned) {
|
||||
deprecatedPropsWarned = true;
|
||||
console.warn('this.props is deprecated, please use this.attrs instead.');
|
||||
}
|
||||
this.attrs = props;
|
||||
}
|
||||
|
||||
// END DEPRECATED MITHRIL 2 BC LAYER
|
||||
protected static initAttrs<T>(attrs: T): void {}
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ import * as Mithril from 'mithril';
|
||||
* This should only be used when necessary, and only with `m.render`. If you are unsure whether you need
|
||||
* this or `Component, you probably need `Component`.
|
||||
*/
|
||||
export default abstract class Fragment {
|
||||
export default abstract class Fragment implements Mithril.ClassComponent {
|
||||
/**
|
||||
* The root DOM element for the fragment.
|
||||
*/
|
||||
@@ -52,7 +52,7 @@ export default abstract class Fragment {
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
public render(): Mithril.Vnode<Mithril.Attributes, this> {
|
||||
public render(): Mithril.Vnode {
|
||||
const vdom = this.view();
|
||||
|
||||
vdom.attrs = vdom.attrs || {};
|
||||
@@ -61,14 +61,15 @@ export default abstract class Fragment {
|
||||
|
||||
vdom.attrs.oncreate = (vnode) => {
|
||||
this.element = vnode.dom;
|
||||
if (originalOnCreate) originalOnCreate.apply(this, [vnode]);
|
||||
if (this.oncreate) this.oncreate.apply(this, vnode);
|
||||
if (originalOnCreate) originalOnCreate.apply(this, vnode);
|
||||
};
|
||||
|
||||
return vdom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a view out of virtual elements.
|
||||
* @inheritdoc
|
||||
*/
|
||||
abstract view(): Mithril.Vnode<Mithril.Attributes, this>;
|
||||
abstract view();
|
||||
}
|
||||
|
@@ -83,7 +83,7 @@ export default class Store {
|
||||
*/
|
||||
find(type, id, query = {}, options = {}) {
|
||||
let params = query;
|
||||
let url = app.forum.attribute('apiUrl') + (query.search ? '/search/' : '/') + type;
|
||||
let url = app.forum.attribute('apiUrl') + '/' + type;
|
||||
|
||||
if (id instanceof Array) {
|
||||
url += '?filter[id]=' + id.join(',');
|
||||
|
@@ -12,14 +12,12 @@ import anchorScroll from './utils/anchorScroll';
|
||||
import RequestError from './utils/RequestError';
|
||||
import abbreviateNumber from './utils/abbreviateNumber';
|
||||
import * as string from './utils/string';
|
||||
import Stream from './utils/Stream';
|
||||
import SubtreeRetainer from './utils/SubtreeRetainer';
|
||||
import setRouteWithForcedRefresh from './utils/setRouteWithForcedRefresh';
|
||||
import extract from './utils/extract';
|
||||
import ScrollListener from './utils/ScrollListener';
|
||||
import stringToColor from './utils/stringToColor';
|
||||
import subclassOf from './utils/subclassOf';
|
||||
import SuperTextarea from './utils/SuperTextarea';
|
||||
import patchMithril from './utils/patchMithril';
|
||||
import classList from './utils/classList';
|
||||
import extractText from './utils/extractText';
|
||||
@@ -48,7 +46,6 @@ import FieldSet from './components/FieldSet';
|
||||
import Select from './components/Select';
|
||||
import Navigation from './components/Navigation';
|
||||
import Alert from './components/Alert';
|
||||
import Link from './components/Link';
|
||||
import LinkButton from './components/LinkButton';
|
||||
import Checkbox from './components/Checkbox';
|
||||
import SelectDropdown from './components/SelectDropdown';
|
||||
@@ -68,7 +65,6 @@ import username from './helpers/username';
|
||||
import userOnline from './helpers/userOnline';
|
||||
import listItems from './helpers/listItems';
|
||||
import Fragment from './Fragment';
|
||||
import DefaultResolver from './resolvers/DefaultResolver';
|
||||
|
||||
export default {
|
||||
extend: extend,
|
||||
@@ -89,9 +85,7 @@ export default {
|
||||
'utils/extract': extract,
|
||||
'utils/ScrollListener': ScrollListener,
|
||||
'utils/stringToColor': stringToColor,
|
||||
'utils/Stream': Stream,
|
||||
'utils/subclassOf': subclassOf,
|
||||
'utils/SuperTextarea': SuperTextarea,
|
||||
'utils/setRouteWithForcedRefresh': setRouteWithForcedRefresh,
|
||||
'utils/patchMithril': patchMithril,
|
||||
'utils/classList': classList,
|
||||
@@ -122,7 +116,6 @@ export default {
|
||||
'components/Select': Select,
|
||||
'components/Navigation': Navigation,
|
||||
'components/Alert': Alert,
|
||||
'components/Link': Link,
|
||||
'components/LinkButton': LinkButton,
|
||||
'components/Checkbox': Checkbox,
|
||||
'components/SelectDropdown': SelectDropdown,
|
||||
@@ -141,5 +134,4 @@ export default {
|
||||
'helpers/username': username,
|
||||
'helpers/userOnline': userOnline,
|
||||
'helpers/listItems': listItems,
|
||||
'resolvers/DefaultResolver': DefaultResolver,
|
||||
};
|
||||
|
@@ -1,33 +1,31 @@
|
||||
import Component, { ComponentAttrs } from '../Component';
|
||||
import Component from '../Component';
|
||||
import Button from './Button';
|
||||
import listItems from '../helpers/listItems';
|
||||
import extract from '../utils/extract';
|
||||
import Mithril from 'mithril';
|
||||
|
||||
export interface AlertAttrs extends ComponentAttrs {
|
||||
/** The type of alert this is. Will be used to give the alert a class name of `Alert--{type}`. */
|
||||
type?: string;
|
||||
/** An array of controls to show in the alert. */
|
||||
controls?: Mithril.Children;
|
||||
/** Whether or not the alert can be dismissed. */
|
||||
dismissible?: boolean;
|
||||
/** A callback to run when the alert is dismissed */
|
||||
ondismiss?: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `Alert` component represents an alert box, which contains a message,
|
||||
* some controls, and may be dismissible.
|
||||
*
|
||||
* ### Attrs
|
||||
*
|
||||
* - `type` The type of alert this is. Will be used to give the alert a class
|
||||
* name of `Alert--{type}`.
|
||||
* - `controls` An array of controls to show in the alert.
|
||||
* - `dismissible` Whether or not the alert can be dismissed.
|
||||
* - `ondismiss` A callback to run when the alert is dismissed.
|
||||
*
|
||||
* All other attrs will be assigned as attributes on the DOM element.
|
||||
*/
|
||||
export default class Alert<T extends AlertAttrs = AlertAttrs> extends Component<T> {
|
||||
view(vnode: Mithril.Vnode) {
|
||||
export default class Alert extends Component {
|
||||
view(vnode) {
|
||||
const attrs = Object.assign({}, this.attrs);
|
||||
|
||||
const type = extract(attrs, 'type');
|
||||
attrs.className = 'Alert Alert--' + type + ' ' + (attrs.className || '');
|
||||
|
||||
const content = extract(attrs, 'content') || vnode.children;
|
||||
const controls = (extract(attrs, 'controls') || []) as Mithril.ChildArray;
|
||||
const controls = extract(attrs, 'controls') || [];
|
||||
|
||||
// If the alert is meant to be dismissible (which is the case by default),
|
||||
// then we will create a dismiss button to append as the final control in
|
@@ -1,47 +0,0 @@
|
||||
import Component from '../Component';
|
||||
import extract from '../utils/extract';
|
||||
|
||||
/**
|
||||
* The link component enables both internal and external links.
|
||||
* It will return a regular HTML link for any links to external sites,
|
||||
* and it will use Mithril's m.route.Link for any internal links.
|
||||
*
|
||||
* Links will default to internal; the 'external' attr must be set to
|
||||
* `true` for the link to be external.
|
||||
*/
|
||||
export default class Link extends Component {
|
||||
view(vnode) {
|
||||
let { options = {}, ...attrs } = vnode.attrs;
|
||||
|
||||
attrs.href = attrs.href || '';
|
||||
|
||||
// For some reason, m.route.Link does not like vnode.text, so if present, we
|
||||
// need to convert it to text vnodes and store it in children.
|
||||
const children = vnode.children || { tag: '#', children: vnode.text };
|
||||
|
||||
if (attrs.external) {
|
||||
return <a {...attrs}>{children}</a>;
|
||||
}
|
||||
|
||||
// If the href URL of the link is the same as the current page path
|
||||
// we will not add a new entry to the browser history.
|
||||
// This allows us to still refresh the Page component
|
||||
// without adding endless history entries.
|
||||
if (attrs.href === m.route.get()) {
|
||||
if (!('replace' in options)) options.replace = true;
|
||||
}
|
||||
|
||||
// Mithril 2 does not completely rerender the page if a route change leads to the same route
|
||||
// (or the same component handling a different route).
|
||||
// Here, the `force` parameter will use Mithril's key system to force a full rerender
|
||||
// see https://mithril.js.org/route.html#key-parameter
|
||||
if (extract(attrs, 'force')) {
|
||||
if (!('state' in options)) options.state = {};
|
||||
if (!('key' in options.state)) options.state.key = Date.now();
|
||||
}
|
||||
|
||||
attrs.options = options;
|
||||
|
||||
return <m.route.Link {...attrs}>{children}</m.route.Link>;
|
||||
}
|
||||
}
|
@@ -1,5 +1,4 @@
|
||||
import Button from './Button';
|
||||
import Link from './Link';
|
||||
|
||||
/**
|
||||
* The `LinkButton` component defines a `Button` which links to a route.
|
||||
@@ -12,20 +11,18 @@ import Link from './Link';
|
||||
* active.
|
||||
* - `href` The URL to link to. If the current URL `m.route()` matches this,
|
||||
* the `active` prop will automatically be set to true.
|
||||
* - `force` Whether the page should be fully rerendered. Defaults to `true`.
|
||||
*/
|
||||
export default class LinkButton extends Button {
|
||||
static initAttrs(attrs) {
|
||||
super.initAttrs(attrs);
|
||||
|
||||
attrs.active = this.isActive(attrs);
|
||||
if (attrs.force === undefined) attrs.force = true;
|
||||
}
|
||||
|
||||
view(vnode) {
|
||||
const vdom = super.view(vnode);
|
||||
|
||||
vdom.tag = Link;
|
||||
vdom.tag = m.route.Link;
|
||||
vdom.attrs.active = String(vdom.attrs.active);
|
||||
|
||||
return vdom;
|
||||
|
@@ -24,20 +24,11 @@ export default class Modal extends Component {
|
||||
oncreate(vnode) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
this.attrs.animateShow(() => this.onready());
|
||||
this.attrs.onshow(() => this.onready());
|
||||
}
|
||||
|
||||
onbeforeremove() {
|
||||
// If the global modal state currently contains a modal,
|
||||
// we've just opened up a new one, and accordingly,
|
||||
// we don't need to show a hide animation.
|
||||
if (!this.attrs.state.modal) {
|
||||
this.attrs.animateHide();
|
||||
// Here, we ensure that the animation has time to complete.
|
||||
// See https://mithril.js.org/lifecycle-methods.html#onbeforeremove
|
||||
// Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
|
||||
return new Promise((resolve) => setTimeout(resolve, 300));
|
||||
}
|
||||
onremove() {
|
||||
this.attrs.onhide();
|
||||
}
|
||||
|
||||
view() {
|
||||
@@ -112,11 +103,13 @@ export default class Modal extends Component {
|
||||
this.$('form').find('input, select, textarea').first().focus().select();
|
||||
}
|
||||
|
||||
onhide() {}
|
||||
|
||||
/**
|
||||
* Hide the modal.
|
||||
*/
|
||||
hide() {
|
||||
this.attrs.state.close();
|
||||
this.attrs.onhide();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -11,14 +11,7 @@ export default class ModalManager extends Component {
|
||||
|
||||
return (
|
||||
<div className="ModalManager modal fade">
|
||||
{modal
|
||||
? modal.componentClass.component({
|
||||
...modal.attrs,
|
||||
animateShow: this.animateShow.bind(this),
|
||||
animateHide: this.animateHide.bind(this),
|
||||
state: this.attrs.state,
|
||||
})
|
||||
: ''}
|
||||
{modal ? modal.componentClass.component({ ...modal.attrs, onshow: this.animateShow.bind(this), onhide: this.animateHide.bind(this) }) : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -35,14 +28,6 @@ export default class ModalManager extends Component {
|
||||
animateShow(readyCallback) {
|
||||
const dismissible = !!this.attrs.state.modal.componentClass.isDismissible;
|
||||
|
||||
// If we are opening this modal while another modal is already open,
|
||||
// the shown event will not run, because the modal is already open.
|
||||
// So, we need to manually trigger the readyCallback.
|
||||
if (this.$().hasClass('in')) {
|
||||
readyCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
this.$()
|
||||
.one('shown.bs.modal', readyCallback)
|
||||
.modal({
|
||||
|
@@ -11,7 +11,9 @@ export default class Page extends Component {
|
||||
super.oninit(vnode);
|
||||
|
||||
app.previous = app.current;
|
||||
app.current = new PageState(this.constructor, { routeName: this.attrs.routeName });
|
||||
app.current = new PageState(this.constructor);
|
||||
|
||||
this.onNewRoute();
|
||||
|
||||
app.drawer.hide();
|
||||
app.modal.close();
|
||||
@@ -22,13 +24,16 @@ export default class Page extends Component {
|
||||
* @type {String}
|
||||
*/
|
||||
this.bodyClass = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we should scroll to the top of the page when its rendered.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.scrollTopOnCreate = true;
|
||||
/**
|
||||
* A collections of actions to run when the route changes.
|
||||
* This is extracted here, and not hardcoded in oninit, as oninit is not called
|
||||
* when a different route is handled by the same component, but we still need to
|
||||
* adjust the current route name.
|
||||
*/
|
||||
onNewRoute() {
|
||||
app.current.set('routeName', this.attrs.routeName);
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
@@ -37,10 +42,6 @@ export default class Page extends Component {
|
||||
if (this.bodyClass) {
|
||||
$('#app').addClass(this.bodyClass);
|
||||
}
|
||||
|
||||
if (this.scrollTopOnCreate) {
|
||||
$(window).scrollTop(0);
|
||||
}
|
||||
}
|
||||
|
||||
onremove() {
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import dayjs from 'dayjs';
|
||||
import * as Mithril from 'mithril';
|
||||
|
||||
/**
|
||||
* The `fullTime` helper displays a formatted time string wrapped in a <time>
|
||||
* tag.
|
||||
*
|
||||
* @param {Date} time
|
||||
* @return {Object}
|
||||
*/
|
||||
export default function fullTime(time: Date): Mithril.Vnode {
|
||||
export default function fullTime(time) {
|
||||
const d = dayjs(time);
|
||||
|
||||
const datetime = d.format();
|
@@ -1,13 +1,14 @@
|
||||
import dayjs from 'dayjs';
|
||||
import * as Mithril from 'mithril';
|
||||
import humanTimeUtil from '../utils/humanTime';
|
||||
|
||||
/**
|
||||
* The `humanTime` helper displays a time in a human-friendly time-ago format
|
||||
* (e.g. '12 days ago'), wrapped in a <time> tag with other information about
|
||||
* the time.
|
||||
*
|
||||
* @param {Date} time
|
||||
* @return {Object}
|
||||
*/
|
||||
export default function humanTime(time: Date): Mithril.Vnode {
|
||||
export default function humanTime(time) {
|
||||
const d = dayjs(time);
|
||||
|
||||
const datetime = d.format();
|
12
js/src/common/helpers/icon.js
Normal file
12
js/src/common/helpers/icon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* The `icon` helper displays an icon.
|
||||
*
|
||||
* @param {String} fontClass The full icon class, prefix and the icon’s name.
|
||||
* @param {Object} attrs Any other attributes to apply.
|
||||
* @return {Object}
|
||||
*/
|
||||
export default function icon(fontClass, attrs = {}) {
|
||||
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || '');
|
||||
|
||||
return <i {...attrs} />;
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
import * as Mithril from 'mithril';
|
||||
|
||||
/**
|
||||
* The `icon` helper displays an icon.
|
||||
*
|
||||
* @param fontClass The full icon class, prefix and the icon’s name.
|
||||
* @param attrs Any other attributes to apply.
|
||||
*/
|
||||
export default function icon(fontClass: string, attrs: Mithril.Attributes = {}): Mithril.Vnode {
|
||||
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || '');
|
||||
|
||||
return <i {...attrs} />;
|
||||
}
|
@@ -51,6 +51,8 @@ export default function listItems(items) {
|
||||
</li>
|
||||
);
|
||||
|
||||
node.state = node.state || {};
|
||||
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
@@ -1,41 +0,0 @@
|
||||
import Mithril from 'mithril';
|
||||
|
||||
/**
|
||||
* Generates a route resolver for a given component.
|
||||
* In addition to regular route resolver functionality:
|
||||
* - It provide the current route name as an attr
|
||||
* - It sets a key on the component so a rerender will be triggered on route change.
|
||||
*/
|
||||
export default class DefaultResolver {
|
||||
component: Mithril.Component;
|
||||
routeName: string;
|
||||
|
||||
constructor(component, routeName) {
|
||||
this.component = component;
|
||||
this.routeName = routeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a route change results in a changed key, a full page
|
||||
* rerender occurs. This method can be overriden in subclasses
|
||||
* to prevent rerenders on some route changes.
|
||||
*/
|
||||
makeKey() {
|
||||
return this.routeName + JSON.stringify(m.route.param());
|
||||
}
|
||||
|
||||
makeAttrs(vnode) {
|
||||
return {
|
||||
...vnode.attrs,
|
||||
routeName: this.routeName,
|
||||
};
|
||||
}
|
||||
|
||||
onmatch(args, requestedPath, route) {
|
||||
return this.component;
|
||||
}
|
||||
|
||||
render(vnode) {
|
||||
return [{ ...vnode, attrs: this.makeAttrs(vnode), key: this.makeKey() }];
|
||||
}
|
||||
}
|
50
js/src/common/states/AlertManagerState.js
Normal file
50
js/src/common/states/AlertManagerState.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import Alert from '../components/Alert';
|
||||
|
||||
export default class AlertManagerState {
|
||||
constructor() {
|
||||
this.activeAlerts = {};
|
||||
this.alertId = 0;
|
||||
}
|
||||
|
||||
getActiveAlerts() {
|
||||
return this.activeAlerts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an Alert in the alerts area.
|
||||
*/
|
||||
show(children, attrs, componentClass = Alert) {
|
||||
// Breaking Change Compliance Warning, Remove in Beta 15.
|
||||
// This is applied to the first argument (attrs) because previously, the alert was passed as the first argument.
|
||||
if (attrs === Alert || attrs instanceof Alert) {
|
||||
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
|
||||
console.error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
|
||||
throw new Error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
|
||||
}
|
||||
// End Change Compliance Warning, Remove in Beta 15
|
||||
this.activeAlerts[++this.alertId] = { children, attrs, componentClass };
|
||||
m.redraw();
|
||||
|
||||
return this.alertId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss an alert.
|
||||
*/
|
||||
dismiss(key) {
|
||||
if (!key || !(key in this.activeAlerts)) return;
|
||||
|
||||
delete this.activeAlerts[key];
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all alerts.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
clear() {
|
||||
this.activeAlerts = {};
|
||||
m.redraw();
|
||||
}
|
||||
}
|
@@ -1,80 +0,0 @@
|
||||
import Mithril from 'mithril';
|
||||
import Alert, { AlertAttrs } from '../components/Alert';
|
||||
|
||||
/**
|
||||
* Returned by `AlertManagerState.show`. Used to dismiss alerts.
|
||||
*/
|
||||
export type AlertIdentifier = number;
|
||||
|
||||
export interface AlertState {
|
||||
componentClass: typeof Alert;
|
||||
attrs: AlertAttrs;
|
||||
children: Mithril.Children;
|
||||
}
|
||||
|
||||
export default class AlertManagerState {
|
||||
protected activeAlerts: { [id: number]: AlertState } = {};
|
||||
protected alertId = 0;
|
||||
|
||||
getActiveAlerts() {
|
||||
return this.activeAlerts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an Alert in the alerts area.
|
||||
*
|
||||
* @returns The alert's ID, which can be used to dismiss the alert.
|
||||
*/
|
||||
show(children: Mithril.Children): AlertIdentifier;
|
||||
show(attrs: AlertAttrs, children: Mithril.Children): AlertIdentifier;
|
||||
show(componentClass: Alert, attrs: AlertAttrs, children: Mithril.Children): AlertIdentifier;
|
||||
|
||||
show(arg1: any, arg2?: any, arg3?: any) {
|
||||
// Assigns variables as per the above signatures
|
||||
let componentClass = Alert;
|
||||
let attrs: AlertAttrs = {};
|
||||
let children: Mithril.Children;
|
||||
|
||||
if (arguments.length == 1) {
|
||||
children = arg1 as Mithril.Children;
|
||||
} else if (arguments.length == 2) {
|
||||
attrs = arg1 as AlertAttrs;
|
||||
children = arg2 as Mithril.Children;
|
||||
} else if (arguments.length == 3) {
|
||||
componentClass = arg1 as typeof Alert;
|
||||
attrs = arg2 as AlertAttrs;
|
||||
children = arg3;
|
||||
}
|
||||
|
||||
// Breaking Change Compliance Warning, Remove in Beta 15.
|
||||
// This is applied to the first argument (attrs) because previously, the alert was passed as the first argument.
|
||||
if (attrs === Alert || attrs instanceof Alert) {
|
||||
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
|
||||
console.error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
|
||||
throw new Error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
|
||||
}
|
||||
// End Change Compliance Warning, Remove in Beta 15
|
||||
this.activeAlerts[++this.alertId] = { children, attrs, componentClass };
|
||||
m.redraw();
|
||||
|
||||
return this.alertId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss an alert.
|
||||
*/
|
||||
dismiss(key: AlertIdentifier): void {
|
||||
if (!key || !(key in this.activeAlerts)) return;
|
||||
|
||||
delete this.activeAlerts[key];
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all alerts.
|
||||
*/
|
||||
clear(): void {
|
||||
this.activeAlerts = {};
|
||||
m.redraw();
|
||||
}
|
||||
}
|
@@ -58,7 +58,7 @@ export default class ScrollListener {
|
||||
*/
|
||||
start() {
|
||||
if (!this.active) {
|
||||
window.addEventListener('scroll', (this.active = this.loop.bind(this)), { passive: true });
|
||||
window.addEventListener('scroll', (this.active = this.loop.bind(this)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,3 +0,0 @@
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
export default Stream;
|
@@ -28,9 +28,6 @@ export default class SubtreeRetainer {
|
||||
constructor(...callbacks) {
|
||||
this.callbacks = callbacks;
|
||||
this.data = {};
|
||||
// Build the initial data, so it is available when calling
|
||||
// needsRebuild from the onbeforeupdate hook.
|
||||
this.needsRebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,8 +60,6 @@ export default class SubtreeRetainer {
|
||||
*/
|
||||
check(...callbacks) {
|
||||
this.callbacks = this.callbacks.concat(callbacks);
|
||||
// Update the data cache when new checks are added.
|
||||
this.needsRebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -22,8 +22,6 @@ export default class SuperTextarea {
|
||||
*/
|
||||
setValue(value) {
|
||||
this.$.val(value).trigger('input');
|
||||
|
||||
this.el.dispatchEvent(new CustomEvent('input', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +49,8 @@ export default class SuperTextarea {
|
||||
*/
|
||||
insertAtCursor(text) {
|
||||
this.insertAt(this.el.selectionStart, text);
|
||||
|
||||
this.el.dispatchEvent(new CustomEvent('input', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,6 +1,3 @@
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/plugin/relativeTime';
|
||||
|
||||
/**
|
||||
* The `humanTime` utility converts a date to a localized, human-readable time-
|
||||
* ago string.
|
||||
|
@@ -1,9 +1,6 @@
|
||||
import DefaultResolver from '../resolvers/DefaultResolver';
|
||||
|
||||
/**
|
||||
* The `mapRoutes` utility converts a map of named application routes into a
|
||||
* format that can be understood by Mithril, and wraps them in route resolvers
|
||||
* to provide each route with the current route name.
|
||||
* format that can be understood by Mithril.
|
||||
*
|
||||
* @see https://mithril.js.org/route.html#signature
|
||||
* @param {Object} routes
|
||||
@@ -13,17 +10,14 @@ import DefaultResolver from '../resolvers/DefaultResolver';
|
||||
export default function mapRoutes(routes, basePath = '') {
|
||||
const map = {};
|
||||
|
||||
for (const routeName in routes) {
|
||||
const route = routes[routeName];
|
||||
for (const key in routes) {
|
||||
const route = routes[key];
|
||||
|
||||
if ('resolver' in route) {
|
||||
map[basePath + route.path] = route.resolver;
|
||||
} else if ('component' in route) {
|
||||
const resolverClass = 'resolverClass' in route ? route.resolverClass : DefaultResolver;
|
||||
map[basePath + route.path] = new resolverClass(route.component, routeName);
|
||||
} else {
|
||||
throw new Error(`Either a resolver or a component must be provided for the route [${routeName}]`);
|
||||
}
|
||||
map[basePath + route.path] = {
|
||||
render() {
|
||||
return m(route.component, { routeName: key });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return map;
|
||||
|
@@ -1,12 +1,39 @@
|
||||
import withAttr from './withAttr';
|
||||
import Stream from './Stream';
|
||||
|
||||
let deprecatedMPropWarned = false;
|
||||
let deprecatedMWithAttrWarned = false;
|
||||
import Stream from 'mithril/stream';
|
||||
import extract from './extract';
|
||||
|
||||
export default function patchMithril(global) {
|
||||
const defaultMithril = global.m;
|
||||
|
||||
/**
|
||||
* If the href URL of the link is the same as the current page path
|
||||
* we will not add a new entry to the browser history.
|
||||
*
|
||||
* This allows us to still refresh the Page component
|
||||
* without adding endless history entries.
|
||||
*
|
||||
* We also add the `force` attribute that adds a custom state key
|
||||
* for when you want to force a complete refresh of the Page
|
||||
*/
|
||||
const defaultLinkView = defaultMithril.route.Link.view;
|
||||
const modifiedLink = {
|
||||
view: function (vnode) {
|
||||
let { href, options = {} } = vnode.attrs;
|
||||
|
||||
if (href === m.route.get()) {
|
||||
if (!('replace' in options)) options.replace = true;
|
||||
}
|
||||
|
||||
if (extract(vnode.attrs, 'force')) {
|
||||
if (!('state' in options)) options.state = {};
|
||||
if (!('key' in options.state)) options.state.key = Date.now();
|
||||
}
|
||||
|
||||
vnode.attrs.options = options;
|
||||
|
||||
return defaultLinkView(vnode);
|
||||
},
|
||||
};
|
||||
|
||||
const modifiedMithril = function (comp, ...args) {
|
||||
const node = defaultMithril.apply(this, arguments);
|
||||
|
||||
@@ -17,28 +44,29 @@ export default function patchMithril(global) {
|
||||
modifiedMithril.bidi(node, node.attrs.bidi);
|
||||
}
|
||||
|
||||
// Allows us to use a "route" attr on links, which will automatically convert the link to one which
|
||||
// supports linking to other pages in the SPA without refreshing the document.
|
||||
if (node.attrs.route) {
|
||||
node.attrs.href = node.attrs.route;
|
||||
node.tag = modifiedLink;
|
||||
|
||||
// For some reason, m.route.Link does not like vnode.text, so if present, we
|
||||
// need to convert it to text vnodes and store it in children.
|
||||
if (node.text) {
|
||||
node.children = { tag: '#', children: node.text };
|
||||
}
|
||||
|
||||
delete node.attrs.route;
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
Object.keys(defaultMithril).forEach((key) => (modifiedMithril[key] = defaultMithril[key]));
|
||||
|
||||
// BEGIN DEPRECATED MITHRIL 2 BC LAYER
|
||||
modifiedMithril.prop = function (...args) {
|
||||
if (!deprecatedMPropWarned) {
|
||||
deprecatedMPropWarned = true;
|
||||
console.warn('m.prop() is deprecated, please use the Stream util (flarum/utils/Streams) instead.');
|
||||
}
|
||||
return Stream.bind(this)(...args);
|
||||
};
|
||||
modifiedMithril.stream = Stream;
|
||||
|
||||
modifiedMithril.withAttr = function (...args) {
|
||||
if (!deprecatedMWithAttrWarned) {
|
||||
deprecatedMWithAttrWarned = true;
|
||||
console.warn("m.withAttr() is deprecated, please use flarum's withAttr util (flarum/utils/withAttr) instead.");
|
||||
}
|
||||
return withAttr.bind(this)(...args);
|
||||
};
|
||||
// END DEPRECATED MITHRIL 2 BC LAYER
|
||||
modifiedMithril.route.Link = modifiedLink;
|
||||
|
||||
global.m = modifiedMithril;
|
||||
}
|
||||
|
@@ -115,19 +115,17 @@ export default class ForumApplication extends Application {
|
||||
this.routes[defaultAction].path = '/';
|
||||
this.history.push(defaultAction, this.translator.trans('core.forum.header.back_to_index_tooltip'), '/');
|
||||
|
||||
this.pane = new Pane(document.getElementById('app'));
|
||||
|
||||
m.route.prefix = '';
|
||||
super.mount(this.forum.attribute('basePath'));
|
||||
|
||||
// We mount navigation and header components after the page, so components
|
||||
// like the back button can access the updated state when rendering.
|
||||
m.mount(document.getElementById('app-navigation'), { view: () => Navigation.component({ className: 'App-backControl', drawer: true }) });
|
||||
m.mount(document.getElementById('header-navigation'), Navigation);
|
||||
m.mount(document.getElementById('header-primary'), HeaderPrimary);
|
||||
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
|
||||
m.mount(document.getElementById('composer'), { view: () => Composer.component({ state: this.composer }) });
|
||||
|
||||
this.pane = new Pane(document.getElementById('app'));
|
||||
|
||||
m.route.prefix = '';
|
||||
super.mount(this.forum.attribute('basePath'));
|
||||
|
||||
alertEmailConfirmation(this);
|
||||
|
||||
// Route the home link back home when clicked. We do not want it to register
|
||||
|
@@ -71,7 +71,6 @@ import Search from './components/Search';
|
||||
import DiscussionListItem from './components/DiscussionListItem';
|
||||
import LoadingPost from './components/LoadingPost';
|
||||
import PostsUserPage from './components/PostsUserPage';
|
||||
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
|
||||
import routes from './routes';
|
||||
import ForumApplication from './ForumApplication';
|
||||
|
||||
@@ -147,7 +146,6 @@ export default Object.assign(compat, {
|
||||
'components/DiscussionListItem': DiscussionListItem,
|
||||
'components/LoadingPost': LoadingPost,
|
||||
'components/PostsUserPage': PostsUserPage,
|
||||
'resolvers/DiscussionPageResolver': DiscussionPageResolver,
|
||||
routes: routes,
|
||||
ForumApplication: ForumApplication,
|
||||
});
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import Modal from '../../common/components/Modal';
|
||||
import Button from '../../common/components/Button';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
|
||||
/**
|
||||
* The `ChangeEmailModal` component shows a modal dialog which allows the user
|
||||
@@ -22,14 +21,14 @@ export default class ChangeEmailModal extends Modal {
|
||||
*
|
||||
* @type {function}
|
||||
*/
|
||||
this.email = Stream(app.session.user.email());
|
||||
this.email = m.stream(app.session.user.email());
|
||||
|
||||
/**
|
||||
* The value of the password input.
|
||||
*
|
||||
* @type {function}
|
||||
*/
|
||||
this.password = Stream('');
|
||||
this.password = m.stream('');
|
||||
}
|
||||
|
||||
className() {
|
||||
|
@@ -56,7 +56,9 @@ export default class CommentPost extends Post {
|
||||
]);
|
||||
}
|
||||
|
||||
refreshContent() {
|
||||
onupdate(vnode) {
|
||||
super.onupdate();
|
||||
|
||||
const contentHtml = this.isEditing() ? '' : this.attrs.post.contentHtml();
|
||||
|
||||
// If the post content has changed since the last render, we'll run through
|
||||
@@ -64,28 +66,13 @@ export default class CommentPost extends Post {
|
||||
// necessary because TextFormatter outputs them for e.g. syntax highlighting.
|
||||
if (this.contentHtml !== contentHtml) {
|
||||
this.$('.Post-body script').each(function () {
|
||||
const script = document.createElement('script');
|
||||
script.textContent = this.textContent;
|
||||
Array.from(this.attributes).forEach((attr) => script.setAttribute(attr.name, attr.value));
|
||||
this.parentNode.replaceChild(script, this);
|
||||
eval.call(window, $(this).text());
|
||||
});
|
||||
}
|
||||
|
||||
this.contentHtml = contentHtml;
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
this.refreshContent();
|
||||
}
|
||||
|
||||
onupdate(vnode) {
|
||||
super.onupdate(vnode);
|
||||
|
||||
this.refreshContent();
|
||||
}
|
||||
|
||||
isEditing() {
|
||||
return app.composer.bodyMatches(EditPostComposer, { post: this.attrs.post });
|
||||
}
|
||||
|
@@ -82,7 +82,7 @@ export default class Composer extends Component {
|
||||
});
|
||||
|
||||
// When the escape key is pressed on any inputs, close the composer.
|
||||
this.$().on('keydown', ':input', 'esc', () => this.state.close());
|
||||
this.$().on('keydown', ':input', 'esc', () => this.close());
|
||||
|
||||
this.handlers = {};
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import ComposerBody from './ComposerBody';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
|
||||
/**
|
||||
* The `DiscussionComposer` component displays the composer content for starting
|
||||
@@ -27,7 +26,7 @@ export default class DiscussionComposer extends ComposerBody {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.composer.fields.title = this.composer.fields.title || Stream('');
|
||||
this.composer.fields.title = this.composer.fields.title || m.stream('');
|
||||
|
||||
/**
|
||||
* The value of the title input.
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import Component from '../../common/Component';
|
||||
import Link from '../../common/components/Link';
|
||||
import avatar from '../../common/helpers/avatar';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import highlight from '../../common/helpers/highlight';
|
||||
@@ -51,7 +50,6 @@ export default class DiscussionListItem extends Component {
|
||||
'DiscussionListItem',
|
||||
this.active() ? 'active' : '',
|
||||
this.attrs.discussion.isHidden() ? 'DiscussionListItem--hidden' : '',
|
||||
'ontouchstart' in window ? 'Slidable' : '',
|
||||
]),
|
||||
};
|
||||
}
|
||||
@@ -91,16 +89,16 @@ export default class DiscussionListItem extends Component {
|
||||
)
|
||||
: ''}
|
||||
|
||||
<span
|
||||
<a
|
||||
className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')}
|
||||
onclick={this.markAsRead.bind(this)}
|
||||
>
|
||||
{icon('fas fa-check')}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '') + (isRead ? ' read' : '')}>
|
||||
<Link
|
||||
href={user ? app.route.user(user) : '#'}
|
||||
<a
|
||||
route={user ? app.route.user(user) : '#'}
|
||||
className="DiscussionListItem-author"
|
||||
title={extractText(
|
||||
app.translator.trans('core.forum.discussion_list.started_text', { user: user, ago: humanTime(discussion.createdAt()) })
|
||||
@@ -110,14 +108,14 @@ export default class DiscussionListItem extends Component {
|
||||
}}
|
||||
>
|
||||
{avatar(user, { title: '' })}
|
||||
</Link>
|
||||
</a>
|
||||
|
||||
<ul className="DiscussionListItem-badges badges">{listItems(discussion.badges().toArray())}</ul>
|
||||
|
||||
<Link href={app.route.discussion(discussion, jumpTo)} className="DiscussionListItem-main">
|
||||
<a route={app.route.discussion(discussion, jumpTo)} className="DiscussionListItem-main">
|
||||
<h3 className="DiscussionListItem-title">{highlight(discussion.title(), this.highlightRegExp)}</h3>
|
||||
<ul className="DiscussionListItem-info">{listItems(this.infoItems().toArray())}</ul>
|
||||
</Link>
|
||||
</a>
|
||||
|
||||
<span
|
||||
className="DiscussionListItem-count"
|
||||
@@ -138,7 +136,7 @@ export default class DiscussionListItem extends Component {
|
||||
// This allows the user to drag the row to either side of the screen to
|
||||
// reveal controls.
|
||||
if ('ontouchstart' in window) {
|
||||
const slidableInstance = slidable(this.$());
|
||||
const slidableInstance = slidable(this.$().addClass('Slidable'));
|
||||
|
||||
this.$('.DiscussionListItem-controls').on('hidden.bs.dropdown', () => slidableInstance.reset());
|
||||
}
|
||||
|
@@ -47,10 +47,11 @@ export default class DiscussionPage extends Page {
|
||||
app.history.push('discussion');
|
||||
|
||||
this.bodyClass = 'App--discussion';
|
||||
|
||||
this.prevRoute = m.route.get();
|
||||
}
|
||||
|
||||
onremove() {
|
||||
super.onremove();
|
||||
// If we are indeed navigating away from this discussion, then disable the
|
||||
// discussion list pane. Also, if we're composing a reply to this
|
||||
// discussion, minimize the composer – unless it's empty, in which case
|
||||
@@ -82,6 +83,7 @@ export default class DiscussionPage extends Page {
|
||||
{PostStream.component({
|
||||
discussion,
|
||||
stream: this.stream,
|
||||
targetPost: this.stream.targetPost,
|
||||
onPositionChange: this.positionChanged.bind(this),
|
||||
})}
|
||||
</div>
|
||||
@@ -93,6 +95,33 @@ export default class DiscussionPage extends Page {
|
||||
);
|
||||
}
|
||||
|
||||
onbeforeupdate(vnode) {
|
||||
super.onbeforeupdate(vnode);
|
||||
|
||||
if (m.route.get() !== this.prevRoute) {
|
||||
this.prevRoute = m.route.get();
|
||||
|
||||
// If we have routed to the same discussion as we were viewing previously,
|
||||
// cancel the unloading of this controller and instead prompt the post
|
||||
// stream to jump to the new 'near' param.
|
||||
if (this.discussion) {
|
||||
const idParam = m.route.param('id');
|
||||
|
||||
if (idParam && idParam.split('-')[0] === this.discussion.id()) {
|
||||
const near = m.route.param('near') || '1';
|
||||
|
||||
if (near !== String(this.near)) {
|
||||
this.stream.goToNumber(near);
|
||||
}
|
||||
|
||||
this.near = near;
|
||||
} else {
|
||||
this.oninit(vnode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the discussion from the API or use the preloaded one.
|
||||
*/
|
||||
@@ -131,8 +160,10 @@ export default class DiscussionPage extends Page {
|
||||
* @param {Discussion} discussion
|
||||
*/
|
||||
show(discussion) {
|
||||
this.discussion = discussion;
|
||||
|
||||
app.history.push('discussion', discussion.title());
|
||||
app.setTitle(discussion.title());
|
||||
app.setTitle(this.discussion.title());
|
||||
app.setTitleCount(0);
|
||||
|
||||
// When the API responds with a discussion, it will also include a number of
|
||||
@@ -155,7 +186,7 @@ export default class DiscussionPage extends Page {
|
||||
record.relationships.discussion.data.id === discussionId
|
||||
)
|
||||
.map((record) => app.store.getById('posts', record.id))
|
||||
.sort((a, b) => a.createdAt() - b.createdAt())
|
||||
.sort((a, b) => a.id() - b.id())
|
||||
.slice(0, 20);
|
||||
}
|
||||
|
||||
@@ -163,12 +194,10 @@ export default class DiscussionPage extends Page {
|
||||
// posts we want to display. Tell the stream to scroll down and highlight
|
||||
// the specific post that was routed to.
|
||||
this.stream = new PostStreamState(discussion, includedPosts);
|
||||
this.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true).then(() => {
|
||||
this.discussion = discussion;
|
||||
this.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true);
|
||||
|
||||
app.current.set('discussion', discussion);
|
||||
app.current.set('stream', this.stream);
|
||||
});
|
||||
app.current.set('discussion', discussion);
|
||||
app.current.set('stream', this.stream);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,7 +246,10 @@ export default class DiscussionPage extends Page {
|
||||
// replace it into the window's history and our own history stack.
|
||||
const url = app.route.discussion(discussion, (this.near = startNumber));
|
||||
|
||||
this.prevRoute = url;
|
||||
m.route.set(url, null, { replace: true });
|
||||
window.history.replaceState(null, document.title, url);
|
||||
|
||||
app.history.push('discussion', discussion.title());
|
||||
|
||||
// If the user hasn't read past here before, then we'll update their read
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import highlight from '../../common/helpers/highlight';
|
||||
import LinkButton from '../../common/components/LinkButton';
|
||||
import Link from '../../common/components/Link';
|
||||
|
||||
/**
|
||||
* The `DiscussionsSearchSource` finds and displays discussion search results in
|
||||
@@ -24,7 +23,7 @@ export default class DiscussionsSearchSource {
|
||||
include: 'mostRelevantPost',
|
||||
};
|
||||
|
||||
return app.store.find('discussions', params, { search: query }).then((results) => (this.results[query] = results));
|
||||
return app.store.find('discussions', params).then((results) => (this.results[query] = results));
|
||||
}
|
||||
|
||||
view(query) {
|
||||
@@ -48,10 +47,10 @@ export default class DiscussionsSearchSource {
|
||||
|
||||
return (
|
||||
<li className="DiscussionSearchResult" data-index={'discussions' + discussion.id()}>
|
||||
<Link href={app.route.discussion(discussion, mostRelevantPost && mostRelevantPost.number())}>
|
||||
<a route={app.route.discussion(discussion, mostRelevantPost && mostRelevantPost.number())}>
|
||||
<div className="DiscussionSearchResult-title">{highlight(discussion.title(), query)}</div>
|
||||
{mostRelevantPost ? <div className="DiscussionSearchResult-excerpt">{highlight(mostRelevantPost.contentPlain(), query, 100)}</div> : ''}
|
||||
</Link>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}),
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import ComposerBody from './ComposerBody';
|
||||
import Button from '../../common/components/Button';
|
||||
import Link from '../../common/components/Link';
|
||||
import icon from '../../common/helpers/icon';
|
||||
|
||||
function minimizeComposerIfFullScreen(e) {
|
||||
@@ -40,9 +39,9 @@ export default class EditPostComposer extends ComposerBody {
|
||||
'title',
|
||||
<h3>
|
||||
{icon('fas fa-pencil-alt')}{' '}
|
||||
<Link href={app.route.discussion(post.discussion(), post.number())} onclick={minimizeComposerIfFullScreen}>
|
||||
<a route={app.route.discussion(post.discussion(), post.number())} onclick={minimizeComposerIfFullScreen}>
|
||||
{app.translator.trans('core.forum.composer_edit.post_link', { number: post.number(), discussion: post.discussion().title() })}
|
||||
</Link>
|
||||
</a>
|
||||
</h3>
|
||||
);
|
||||
|
||||
@@ -96,13 +95,10 @@ export default class EditPostComposer extends ComposerBody {
|
||||
},
|
||||
app.translator.trans('core.forum.composer_edit.view_button')
|
||||
);
|
||||
alert = app.alerts.show(
|
||||
{
|
||||
type: 'success',
|
||||
controls: [viewButton],
|
||||
},
|
||||
app.translator.trans('core.forum.composer_edit.edited_message')
|
||||
);
|
||||
alert = app.alerts.show(app.translator.trans('core.forum.composer_edit.edited_message'), {
|
||||
type: 'success',
|
||||
controls: [viewButton],
|
||||
});
|
||||
}
|
||||
|
||||
this.composer.hide();
|
||||
|
@@ -4,7 +4,6 @@ import GroupBadge from '../../common/components/GroupBadge';
|
||||
import Group from '../../common/models/Group';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
|
||||
/**
|
||||
* The `EditUserModal` component displays a modal dialog with a login form.
|
||||
@@ -15,17 +14,17 @@ export default class EditUserModal extends Modal {
|
||||
|
||||
const user = this.attrs.user;
|
||||
|
||||
this.username = Stream(user.username() || '');
|
||||
this.email = Stream(user.email() || '');
|
||||
this.isEmailConfirmed = Stream(user.isEmailConfirmed() || false);
|
||||
this.setPassword = Stream(false);
|
||||
this.password = Stream(user.password() || '');
|
||||
this.username = m.stream(user.username() || '');
|
||||
this.email = m.stream(user.email() || '');
|
||||
this.isEmailConfirmed = m.stream(user.isEmailConfirmed() || false);
|
||||
this.setPassword = m.stream(false);
|
||||
this.password = m.stream(user.password() || '');
|
||||
this.groups = {};
|
||||
|
||||
app.store
|
||||
.all('groups')
|
||||
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
||||
.forEach((group) => (this.groups[group.id()] = Stream(user.groups().indexOf(group) !== -1)));
|
||||
.forEach((group) => (this.groups[group.id()] = m.stream(user.groups().indexOf(group) !== -1)));
|
||||
}
|
||||
|
||||
className() {
|
||||
|
@@ -2,7 +2,6 @@ import Post from './Post';
|
||||
import { ucfirst } from '../../common/utils/string';
|
||||
import usernameHelper from '../../common/helpers/username';
|
||||
import icon from '../../common/helpers/icon';
|
||||
import Link from '../../common/components/Link';
|
||||
|
||||
/**
|
||||
* The `EventPost` component displays a post which indicating a discussion
|
||||
@@ -30,9 +29,9 @@ export default class EventPost extends Post {
|
||||
const data = Object.assign(this.descriptionData(), {
|
||||
user,
|
||||
username: user ? (
|
||||
<Link className="EventPost-user" href={app.route.user(user)}>
|
||||
<a className="EventPost-user" route={app.route.user(user)}>
|
||||
{username}
|
||||
</Link>
|
||||
</a>
|
||||
) : (
|
||||
username
|
||||
),
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import Modal from '../../common/components/Modal';
|
||||
import Button from '../../common/components/Button';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
|
||||
/**
|
||||
* The `ForgotPasswordModal` component displays a modal which allows the user to
|
||||
@@ -20,7 +19,7 @@ export default class ForgotPasswordModal extends Modal {
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.email = Stream(this.attrs.email || '');
|
||||
this.email = m.stream(this.attrs.email || '');
|
||||
|
||||
/**
|
||||
* Whether or not the password reset email was sent successfully.
|
||||
|
@@ -42,7 +42,26 @@ export default class IndexPage extends Page {
|
||||
app.history.push('index', app.translator.trans('core.forum.header.back_to_index_tooltip'));
|
||||
|
||||
this.bodyClass = 'App--index';
|
||||
this.scrollTopOnCreate = false;
|
||||
|
||||
this.currentPath = m.route.get();
|
||||
}
|
||||
|
||||
onbeforeupdate(vnode) {
|
||||
super.onbeforeupdate(vnode);
|
||||
|
||||
const curPath = m.route.get();
|
||||
|
||||
if (this.currentPath !== curPath) {
|
||||
this.onNewRoute();
|
||||
|
||||
app.discussions.clear();
|
||||
|
||||
app.discussions.refreshParams(app.search.params());
|
||||
|
||||
this.currentPath = curPath;
|
||||
|
||||
this.setTitle();
|
||||
}
|
||||
}
|
||||
|
||||
view() {
|
||||
@@ -86,22 +105,18 @@ export default class IndexPage extends Page {
|
||||
|
||||
$('#app').css('min-height', $(window).height() + heroHeight);
|
||||
|
||||
// Let browser handle scrolling on page reload.
|
||||
if (app.previous.type == null) return;
|
||||
|
||||
// When on mobile, only retain scroll if we're coming from a discussion page.
|
||||
// Otherwise, we've just changed the filter, so we should go to the top of the page.
|
||||
if (app.screen() == 'desktop' || app.screen() == 'desktop-hd' || this.lastDiscussion) {
|
||||
$(window).scrollTop(scrollTop - oldHeroHeight + heroHeight);
|
||||
} else {
|
||||
$(window).scrollTop(0);
|
||||
}
|
||||
// Scroll to the remembered position. We do this after a short delay so that
|
||||
// it happens after the browser has done its own "back button" scrolling,
|
||||
// which isn't right. https://github.com/flarum/core/issues/835
|
||||
const scroll = () => $(window).scrollTop(scrollTop - oldHeroHeight + heroHeight);
|
||||
scroll();
|
||||
setTimeout(scroll, 1);
|
||||
|
||||
// If we've just returned from a discussion page, then the constructor will
|
||||
// have set the `lastDiscussion` property. If this is the case, we want to
|
||||
// scroll down to that discussion so that it's in view.
|
||||
if (this.lastDiscussion) {
|
||||
const $discussion = this.$(`li[data-id="${this.lastDiscussion.id()}"] .DiscussionListItem`);
|
||||
const $discussion = this.$(`.DiscussionListItem[data-id="${this.lastDiscussion.id()}"]`);
|
||||
|
||||
if ($discussion.length) {
|
||||
const indexTop = $('#header').outerHeight();
|
||||
@@ -116,16 +131,14 @@ export default class IndexPage extends Page {
|
||||
}
|
||||
}
|
||||
|
||||
onbeforeremove() {
|
||||
// Save the scroll position so we can restore it when we return to the
|
||||
// discussion list.
|
||||
app.cache.scrollTop = $(window).scrollTop();
|
||||
}
|
||||
|
||||
onremove() {
|
||||
super.onremove();
|
||||
|
||||
$('#app').css('min-height', '');
|
||||
|
||||
// Save the scroll position so we can restore it when we return to the
|
||||
// discussion list.
|
||||
app.cache.scrollTop = $(window).scrollTop();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -5,7 +5,6 @@ import Button from '../../common/components/Button';
|
||||
import LogInButtons from './LogInButtons';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
|
||||
/**
|
||||
* The `LogInModal` component displays a modal dialog with a login form.
|
||||
@@ -24,21 +23,21 @@ export default class LogInModal extends Modal {
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.identification = Stream(this.attrs.identification || '');
|
||||
this.identification = m.stream(this.attrs.identification || '');
|
||||
|
||||
/**
|
||||
* The value of the password input.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.password = Stream(this.attrs.password || '');
|
||||
this.password = m.stream(this.attrs.password || '');
|
||||
|
||||
/**
|
||||
* The value of the remember me input.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.remember = Stream(!!this.attrs.remember);
|
||||
this.remember = m.stream(!!this.attrs.remember);
|
||||
}
|
||||
|
||||
className() {
|
||||
|
@@ -3,7 +3,6 @@ import avatar from '../../common/helpers/avatar';
|
||||
import icon from '../../common/helpers/icon';
|
||||
import humanTime from '../../common/helpers/humanTime';
|
||||
import Button from '../../common/components/Button';
|
||||
import Link from '../../common/components/Link';
|
||||
|
||||
/**
|
||||
* The `Notification` component abstract displays a single notification.
|
||||
@@ -20,11 +19,13 @@ export default class Notification extends Component {
|
||||
const notification = this.attrs.notification;
|
||||
const href = this.href();
|
||||
|
||||
const linkAttrs = {};
|
||||
linkAttrs[href.indexOf('://') === -1 ? 'route' : 'href'] = href;
|
||||
|
||||
return (
|
||||
<Link
|
||||
<a
|
||||
className={'Notification Notification--' + notification.contentType() + ' ' + (!notification.isRead() ? 'unread' : '')}
|
||||
href={href}
|
||||
external={href.includes('://')}
|
||||
{...linkAttrs}
|
||||
onclick={this.markAsRead.bind(this)}
|
||||
>
|
||||
{!notification.isRead() &&
|
||||
@@ -44,7 +45,7 @@ export default class Notification extends Component {
|
||||
<span className="Notification-content">{this.content()}</span>
|
||||
{humanTime(notification.createdAt())}
|
||||
<div className="Notification-excerpt">{this.excerpt()}</div>
|
||||
</Link>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import Component from '../../common/Component';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import Button from '../../common/components/Button';
|
||||
import Link from '../../common/components/Link';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import Discussion from '../../common/models/Discussion';
|
||||
|
||||
@@ -64,10 +63,10 @@ export default class NotificationList extends Component {
|
||||
return (
|
||||
<div className="NotificationGroup">
|
||||
{group.discussion ? (
|
||||
<Link className="NotificationGroup-header" href={app.route.discussion(group.discussion)}>
|
||||
<a className="NotificationGroup-header" route={app.route.discussion(group.discussion)}>
|
||||
{badges && badges.length ? <ul className="NotificationGroup-badges badges">{listItems(badges)}</ul> : ''}
|
||||
{group.discussion.title()}
|
||||
</Link>
|
||||
</a>
|
||||
) : (
|
||||
<div className="NotificationGroup-header">{app.forum.attribute('title')}</div>
|
||||
)}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import Component from '../../common/Component';
|
||||
import Link from '../../common/components/Link';
|
||||
import avatar from '../../common/helpers/avatar';
|
||||
import username from '../../common/helpers/username';
|
||||
import highlight from '../../common/helpers/highlight';
|
||||
@@ -19,12 +18,12 @@ export default class PostPreview extends Component {
|
||||
const excerpt = highlight(post.contentPlain(), this.attrs.highlight, 300);
|
||||
|
||||
return (
|
||||
<Link className="PostPreview" href={app.route.post(post)} onclick={this.attrs.onclick}>
|
||||
<a className="PostPreview" route={app.route.post(post)} onclick={this.attrs.onclick}>
|
||||
<span className="PostPreview-content">
|
||||
{avatar(user)}
|
||||
{username(user)} <span className="PostPreview-excerpt">{excerpt}</span>
|
||||
</span>
|
||||
</Link>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -26,19 +26,17 @@ export default class PostStream extends Component {
|
||||
}
|
||||
|
||||
view() {
|
||||
function fadeIn(element, isInitialized, context) {
|
||||
if (!context.fadedIn) $(element).hide().fadeIn();
|
||||
context.fadedIn = true;
|
||||
}
|
||||
|
||||
let lastTime;
|
||||
|
||||
const viewingEnd = this.stream.viewingEnd();
|
||||
const posts = this.stream.posts();
|
||||
const postIds = this.discussion.postIds();
|
||||
|
||||
const postFadeIn = (vnode) => {
|
||||
$(vnode.dom).addClass('fadeIn');
|
||||
// 500 is the duration of the fadeIn CSS animation + 100ms,
|
||||
// so the animation has time to complete
|
||||
setTimeout(() => $(vnode.dom).removeClass('fadeIn'), 500);
|
||||
};
|
||||
|
||||
const items = posts.map((post, i) => {
|
||||
let content;
|
||||
const attrs = { 'data-index': this.stream.visibleStart + i };
|
||||
@@ -49,13 +47,13 @@ export default class PostStream extends Component {
|
||||
content = PostComponent ? PostComponent.component({ post }) : '';
|
||||
|
||||
attrs.key = 'post' + post.id();
|
||||
attrs.oncreate = postFadeIn;
|
||||
attrs.config = fadeIn;
|
||||
attrs['data-time'] = time.toISOString();
|
||||
attrs['data-number'] = post.number();
|
||||
attrs['data-id'] = post.id();
|
||||
attrs['data-type'] = post.contentType();
|
||||
|
||||
// If the post before this one was more than 4 days ago, we will
|
||||
// If the post before this one was more than 4 hours ago, we will
|
||||
// display a 'time gap' indicating how long it has been in between
|
||||
// the posts.
|
||||
const dt = time - lastTime;
|
||||
@@ -97,7 +95,7 @@ export default class PostStream extends Component {
|
||||
// is not already doing so, then show a 'write a reply' placeholder.
|
||||
if (viewingEnd && (!app.session.user || this.discussion.canReply())) {
|
||||
items.push(
|
||||
<div className="PostStream-item" key="reply" data-index={this.stream.count()} oncreate={postFadeIn}>
|
||||
<div className="PostStream-item" key="reply">
|
||||
{ReplyPlaceholder.component({ discussion: this.discussion })}
|
||||
</div>
|
||||
);
|
||||
@@ -129,16 +127,24 @@ export default class PostStream extends Component {
|
||||
* Start scrolling, if appropriate, to a newly-targeted post.
|
||||
*/
|
||||
triggerScroll() {
|
||||
if (!this.stream.needsScroll) return;
|
||||
if (!this.attrs.targetPost) return;
|
||||
|
||||
const target = this.stream.targetPost;
|
||||
this.stream.needsScroll = false;
|
||||
const oldTarget = this.prevTarget;
|
||||
const newTarget = this.attrs.targetPost;
|
||||
|
||||
if ('number' in target) {
|
||||
this.scrollToNumber(target.number, this.stream.animateScroll);
|
||||
} else if ('index' in target) {
|
||||
this.scrollToIndex(target.index, this.stream.animateScroll, target.reply);
|
||||
if (oldTarget) {
|
||||
if ('number' in oldTarget && oldTarget.number === newTarget.number) return;
|
||||
if ('index' in oldTarget && oldTarget.index === newTarget.index) return;
|
||||
}
|
||||
|
||||
if ('number' in newTarget) {
|
||||
this.scrollToNumber(newTarget.number, this.stream.noAnimationScroll);
|
||||
} else if ('index' in newTarget) {
|
||||
const backwards = newTarget.index === this.stream.count() - 1;
|
||||
this.scrollToIndex(newTarget.index, this.stream.noAnimationScroll, backwards);
|
||||
}
|
||||
|
||||
this.prevTarget = newTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,9 +194,9 @@ export default class PostStream extends Component {
|
||||
// seen if the browser were scrolled right up to the top of the page,
|
||||
// and the viewport had a height of 0.
|
||||
const $items = this.$('.PostStream-item[data-index]');
|
||||
let index = $items.first().data('index') || 0;
|
||||
let visible = 0;
|
||||
let period = '';
|
||||
let indexFromViewPort = null;
|
||||
|
||||
// Now loop through each of the items in the discussion. An 'item' is
|
||||
// either a single post or a 'gap' of one or more posts that haven't
|
||||
@@ -216,10 +222,8 @@ export default class PostStream extends Component {
|
||||
const visibleBottom = Math.min(height, viewportTop + viewportHeight - top);
|
||||
const visiblePost = visibleBottom - visibleTop;
|
||||
|
||||
// We take the index of the first item that passed the previous checks.
|
||||
// It is the item that is first visible in the viewport.
|
||||
if (indexFromViewPort === null) {
|
||||
indexFromViewPort = parseFloat($this.data('index')) + visibleTop / height;
|
||||
if (top <= viewportTop) {
|
||||
index = parseFloat($this.data('index')) + visibleTop / height;
|
||||
}
|
||||
|
||||
if (visiblePost > 0) {
|
||||
@@ -232,10 +236,7 @@ export default class PostStream extends Component {
|
||||
if (time) period = time;
|
||||
});
|
||||
|
||||
// If indexFromViewPort is null, it means no posts are visible in the
|
||||
// viewport. This can happen, when drafting a long reply post. In that case
|
||||
// set the index to the last post.
|
||||
this.stream.index = indexFromViewPort !== null ? indexFromViewPort + 1 : this.stream.count();
|
||||
this.stream.index = index + 1;
|
||||
this.stream.visible = visible;
|
||||
if (period) this.stream.description = dayjs(period).format('MMMM YYYY');
|
||||
}
|
||||
@@ -308,17 +309,18 @@ export default class PostStream extends Component {
|
||||
*
|
||||
* @param {Integer} index
|
||||
* @param {Boolean} animate
|
||||
* @param {Boolean} reply Whether or not to scroll to the reply placeholder.
|
||||
* @param {Boolean} bottom Whether or not to scroll to the bottom of the post
|
||||
* at the given index, instead of the top of it.
|
||||
* @return {jQuery.Deferred}
|
||||
*/
|
||||
scrollToIndex(index, animate, reply) {
|
||||
const $item = reply ? $('.PostStream-item:last-child') : this.$(`.PostStream-item[data-index=${index}]`);
|
||||
scrollToIndex(index, animate, bottom) {
|
||||
const $item = this.$(`.PostStream-item[data-index=${index}]`);
|
||||
|
||||
this.scrollToItem($item, animate, true, reply);
|
||||
|
||||
if (reply) {
|
||||
this.flashItem($item);
|
||||
}
|
||||
return this.scrollToItem($item, animate, true, bottom).then(() => {
|
||||
if (index == this.stream.count() - 1) {
|
||||
this.flashItem(this.$('.PostStream-item:last-child'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -328,12 +330,12 @@ export default class PostStream extends Component {
|
||||
* @param {Boolean} animate
|
||||
* @param {Boolean} force Whether or not to force scrolling to the item, even
|
||||
* if it is already in the viewport.
|
||||
* @param {Boolean} reply Whether or not to scroll to the reply placeholder.
|
||||
* @param {Boolean} bottom Whether or not to scroll to the bottom of the post
|
||||
* at the given index, instead of the top of it.
|
||||
* @return {jQuery.Deferred}
|
||||
*/
|
||||
scrollToItem($item, animate, force, reply) {
|
||||
scrollToItem($item, animate, force, bottom) {
|
||||
const $container = $('html, body').stop(true);
|
||||
const index = $item.data('index');
|
||||
|
||||
if ($item.length) {
|
||||
const itemTop = $item.offset().top - this.getMarginTop();
|
||||
@@ -342,10 +344,10 @@ export default class PostStream extends Component {
|
||||
const scrollBottom = scrollTop + $(window).height();
|
||||
|
||||
// If the item is already in the viewport, we may not need to scroll.
|
||||
// If we're scrolling to the reply placeholder, we'll make sure its
|
||||
// If we're scrolling to the bottom of an item, then we'll make sure the
|
||||
// bottom will line up with the top of the composer.
|
||||
if (force || itemTop < scrollTop || itemBottom > scrollBottom) {
|
||||
const top = reply ? itemBottom - $(window).height() + app.composer.computedHeight() : $item.is(':first-child') ? 0 : itemTop;
|
||||
const top = bottom ? itemBottom - $(window).height() + app.composer.computedHeight() : $item.is(':first-child') ? 0 : itemTop;
|
||||
|
||||
if (!animate) {
|
||||
$container.scrollTop(top);
|
||||
@@ -355,43 +357,12 @@ export default class PostStream extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const updateScrubberHeight = () => {
|
||||
// We manually set the index because we want to display the index of the
|
||||
// exact post we've scrolled to, not just that of the first post within viewport.
|
||||
this.updateScrubber();
|
||||
if (index !== undefined) this.stream.index = index + 1;
|
||||
};
|
||||
|
||||
// If we don't update this before the scroll, the scrubber will start
|
||||
// at the top, and animate down, which can be confusing
|
||||
updateScrubberHeight();
|
||||
this.stream.forceUpdateScrubber = true;
|
||||
|
||||
return Promise.all([$container.promise(), this.stream.loadPromise]).then(() => {
|
||||
this.updateScrubber();
|
||||
const index = $item.data('index');
|
||||
m.redraw.sync();
|
||||
|
||||
// Rendering post contents will probably throw off our position.
|
||||
// To counter this, we'll scroll either:
|
||||
// - To the reply placeholder (aligned with composer top)
|
||||
// - To the top of the page if we're on the first post
|
||||
// - To the top of a post (if that post exists)
|
||||
// If the post does not currently exist, it's probably
|
||||
// outside of the range we loaded in, so we won't adjust anything,
|
||||
// as it will soon be rendered by the "load more" system.
|
||||
let itemOffset;
|
||||
if (reply) {
|
||||
const $placeholder = $('.PostStream-item:last-child');
|
||||
$(window).scrollTop($placeholder.offset().top + $placeholder.height() - $(window).height() + app.composer.computedHeight());
|
||||
} else if (index === 0) {
|
||||
$(window).scrollTop(0);
|
||||
} else if ((itemOffset = $(`.PostStream-item[data-index=${index}]`).offset())) {
|
||||
$(window).scrollTop(itemOffset.top - this.getMarginTop());
|
||||
}
|
||||
|
||||
// We want to adjust this again after posts have been loaded in
|
||||
// and position adjusted so that the scrubber's height is accurate.
|
||||
updateScrubberHeight();
|
||||
|
||||
const scroll = index == 0 ? 0 : $(`.PostStream-item[data-index=${$item.data('index')}]`).offset().top - this.getMarginTop();
|
||||
$(window).scrollTop(scroll);
|
||||
this.calculatePosition();
|
||||
this.stream.paused = false;
|
||||
});
|
||||
@@ -403,11 +374,6 @@ export default class PostStream extends Component {
|
||||
* @param {jQuery} $item
|
||||
*/
|
||||
flashItem($item) {
|
||||
// This might execute before the fadeIn class has been removed in PostStreamItem's
|
||||
// oncreate, so we remove it just to be safe and avoid a double animation.
|
||||
$item.removeClass('fadeIn');
|
||||
$item.addClass('flash').on('animationend webkitAnimationEnd', (e) => {
|
||||
$item.removeClass('flash');
|
||||
});
|
||||
$item.addClass('flash').one('animationend webkitAnimationEnd', () => $item.removeClass('flash'));
|
||||
}
|
||||
}
|
||||
|
@@ -90,10 +90,7 @@ export default class PostStreamScrubber extends Component {
|
||||
}
|
||||
|
||||
onupdate() {
|
||||
if (this.stream.forceUpdateScrubber) {
|
||||
this.stream.forceUpdateScrubber = false;
|
||||
this.stream.loadPromise.then(() => this.updateScrubberValues({ animate: true, forceHeightChange: true }));
|
||||
}
|
||||
this.stream.loadPromise.then(() => this.updateScrubberValues({ animate: true, forceHeightChange: true }));
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
@@ -140,7 +137,7 @@ export default class PostStreamScrubber extends Component {
|
||||
|
||||
setTimeout(() => this.scrollListener.start());
|
||||
|
||||
this.stream.loadPromise.then(() => this.updateScrubberValues({ animate: false, forceHeightChange: true }));
|
||||
this.updateScrubberValues({ animate: true, forceHeightChange: true });
|
||||
}
|
||||
|
||||
onremove() {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import Component from '../../common/Component';
|
||||
import Link from '../../common/components/Link';
|
||||
import UserCard from './UserCard';
|
||||
import avatar from '../../common/helpers/avatar';
|
||||
import username from '../../common/helpers/username';
|
||||
@@ -41,11 +40,11 @@ export default class PostUser extends Component {
|
||||
return (
|
||||
<div className="PostUser">
|
||||
<h3>
|
||||
<Link href={app.route.user(user)}>
|
||||
<a route={app.route.user(user)}>
|
||||
{avatar(user, { className: 'PostUser-avatar' })}
|
||||
{userOnline(user)}
|
||||
{username(user)}
|
||||
</Link>
|
||||
</a>
|
||||
</h3>
|
||||
<ul className="PostUser-badges badges">{listItems(user.badges().toArray())}</ul>
|
||||
{card}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import UserPage from './UserPage';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import Button from '../../common/components/Button';
|
||||
import Link from '../../common/components/Link';
|
||||
import Placeholder from '../../common/components/Placeholder';
|
||||
import CommentPost from './CommentPost';
|
||||
|
||||
@@ -74,7 +73,7 @@ export default class PostsUserPage extends UserPage {
|
||||
<li>
|
||||
<div className="PostsUserPage-discussion">
|
||||
{app.translator.trans('core.forum.user.in_discussion_text', {
|
||||
discussion: <Link href={app.route.post(post)}>{post.discussion().title()}</Link>,
|
||||
discussion: <a route={app.route.post(post)}>{post.discussion().title()}</a>,
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import Modal from '../../common/components/Modal';
|
||||
import Button from '../../common/components/Button';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
|
||||
/**
|
||||
* The 'RenameDiscussionModal' displays a modal dialog with an input to rename a discussion
|
||||
@@ -11,7 +10,7 @@ export default class RenameDiscussionModal extends Modal {
|
||||
|
||||
this.discussion = this.attrs.discussion;
|
||||
this.currentTitle = this.attrs.currentTitle;
|
||||
this.newTitle = Stream(this.currentTitle);
|
||||
this.newTitle = m.stream(this.currentTitle);
|
||||
}
|
||||
|
||||
className() {
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import ComposerBody from './ComposerBody';
|
||||
import Button from '../../common/components/Button';
|
||||
import Link from '../../common/components/Link';
|
||||
import icon from '../../common/helpers/icon';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
|
||||
@@ -37,9 +36,9 @@ export default class ReplyComposer extends ComposerBody {
|
||||
'title',
|
||||
<h3>
|
||||
{icon('fas fa-reply')}{' '}
|
||||
<Link href={app.route.discussion(discussion)} onclick={minimizeComposerIfFullScreen}>
|
||||
<a route={app.route.discussion(discussion)} onclick={minimizeComposerIfFullScreen}>
|
||||
{discussion.title()}
|
||||
</Link>
|
||||
</a>
|
||||
</h3>
|
||||
);
|
||||
|
||||
@@ -99,13 +98,10 @@ export default class ReplyComposer extends ComposerBody {
|
||||
},
|
||||
app.translator.trans('core.forum.composer_reply.view_button')
|
||||
);
|
||||
alert = app.alerts.show(
|
||||
{
|
||||
type: 'success',
|
||||
controls: [viewButton],
|
||||
},
|
||||
app.translator.trans('core.forum.composer_reply.posted_message')
|
||||
);
|
||||
alert = app.alerts.show(app.translator.trans('core.forum.composer_reply.posted_message'), {
|
||||
type: 'success',
|
||||
controls: [viewButton],
|
||||
});
|
||||
}
|
||||
|
||||
this.composer.hide();
|
||||
|
@@ -33,7 +33,7 @@ export default class ReplyPlaceholder extends Component {
|
||||
}
|
||||
|
||||
const reply = () => {
|
||||
DiscussionControls.replyAction.call(this.attrs.discussion, true).catch(() => {});
|
||||
DiscussionControls.replyAction.call(this.attrs.discussion, true);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@@ -4,7 +4,6 @@ import Button from '../../common/components/Button';
|
||||
import LogInButtons from './LogInButtons';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
|
||||
/**
|
||||
* The `SignUpModal` component displays a modal dialog with a singup form.
|
||||
@@ -25,21 +24,21 @@ export default class SignUpModal extends Modal {
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.username = Stream(this.attrs.username || '');
|
||||
this.username = m.stream(this.attrs.username || '');
|
||||
|
||||
/**
|
||||
* The value of the email input.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.email = Stream(this.attrs.email || '');
|
||||
this.email = m.stream(this.attrs.email || '');
|
||||
|
||||
/**
|
||||
* The value of the password input.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.password = Stream(this.attrs.password || '');
|
||||
this.password = m.stream(this.attrs.password || '');
|
||||
}
|
||||
|
||||
className() {
|
||||
@@ -175,7 +174,7 @@ export default class SignUpModal extends Modal {
|
||||
* Get the data that should be submitted in the sign-up request.
|
||||
*
|
||||
* @return {Object}
|
||||
* @protected
|
||||
* @public
|
||||
*/
|
||||
submitData() {
|
||||
const data = {
|
||||
|
@@ -6,7 +6,6 @@ import avatar from '../../common/helpers/avatar';
|
||||
import username from '../../common/helpers/username';
|
||||
import icon from '../../common/helpers/icon';
|
||||
import Dropdown from '../../common/components/Dropdown';
|
||||
import Link from '../../common/components/Link';
|
||||
import AvatarEditor from './AvatarEditor';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
|
||||
@@ -51,10 +50,10 @@ export default class UserCard extends Component {
|
||||
{this.attrs.editable ? (
|
||||
[AvatarEditor.component({ user, className: 'UserCard-avatar' }), username(user)]
|
||||
) : (
|
||||
<Link href={app.route.user(user)}>
|
||||
<a route={app.route.user(user)}>
|
||||
<div className="UserCard-avatar">{avatar(user)}</div>
|
||||
{username(user)}
|
||||
</Link>
|
||||
</a>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
|
@@ -27,6 +27,17 @@ export default class UserPage extends Page {
|
||||
this.user = null;
|
||||
|
||||
this.bodyClass = 'App--user';
|
||||
|
||||
this.prevUsername = m.route.param('username');
|
||||
}
|
||||
|
||||
onbeforeupdate() {
|
||||
const currUsername = m.route.param('username');
|
||||
if (currUsername !== this.prevUsername) {
|
||||
this.prevUsername = currUsername;
|
||||
|
||||
this.loadUser(currUsername);
|
||||
}
|
||||
}
|
||||
|
||||
view() {
|
||||
@@ -135,7 +146,7 @@ export default class UserPage extends Page {
|
||||
|
||||
items.add(
|
||||
'posts',
|
||||
<LinkButton href={app.route('user.posts', { username: user.username() })} icon="far fa-comment">
|
||||
<LinkButton href={app.route('user.posts', { username: user.username() })} force icon="far fa-comment">
|
||||
{app.translator.trans('core.forum.user.posts_link')} <span className="Button-badge">{user.commentCount()}</span>
|
||||
</LinkButton>,
|
||||
100
|
||||
@@ -143,7 +154,7 @@ export default class UserPage extends Page {
|
||||
|
||||
items.add(
|
||||
'discussions',
|
||||
<LinkButton href={app.route('user.discussions', { username: user.username() })} icon="fas fa-bars">
|
||||
<LinkButton href={app.route('user.discussions', { username: user.username() })} force icon="fas fa-bars">
|
||||
{app.translator.trans('core.forum.user.discussions_link')} <span className="Button-badge">{user.discussionCount()}</span>
|
||||
</LinkButton>,
|
||||
90
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import highlight from '../../common/helpers/highlight';
|
||||
import avatar from '../../common/helpers/avatar';
|
||||
import username from '../../common/helpers/username';
|
||||
import Link from '../../common/components/Link';
|
||||
|
||||
/**
|
||||
* The `UsersSearchSource` finds and displays user search results in the search
|
||||
@@ -16,14 +15,10 @@ export default class UsersSearchResults {
|
||||
|
||||
search(query) {
|
||||
return app.store
|
||||
.find(
|
||||
'users',
|
||||
{
|
||||
filter: { q: query },
|
||||
page: { limit: 5 },
|
||||
},
|
||||
{ search: query }
|
||||
)
|
||||
.find('users', {
|
||||
filter: { q: query },
|
||||
page: { limit: 5 },
|
||||
})
|
||||
.then((results) => {
|
||||
this.results[query] = results;
|
||||
m.redraw();
|
||||
@@ -53,10 +48,10 @@ export default class UsersSearchResults {
|
||||
|
||||
return (
|
||||
<li className="UserSearchResult" data-index={'users' + user.id()}>
|
||||
<Link href={app.route.user(user)}>
|
||||
<a route={app.route.user(user)}>
|
||||
{avatar(user)}
|
||||
{{ ...name, text: undefined, children }}
|
||||
</Link>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}),
|
||||
|
@@ -1,49 +0,0 @@
|
||||
import DefaultResolver from '../../common/resolvers/DefaultResolver';
|
||||
import DiscussionPage from '../components/DiscussionPage';
|
||||
|
||||
/**
|
||||
* This isn't exported as it is a temporary measure.
|
||||
* A more robust system will be implemented alongside UTF-8 support in beta 15.
|
||||
*/
|
||||
function getDiscussionIdFromSlug(slug: string | undefined) {
|
||||
if (!slug) return;
|
||||
return slug.split('-')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom route resolver for DiscussionPage that generates the same key to all posts
|
||||
* on the same discussion. It triggers a scroll when going from one post to another
|
||||
* in the same discussion.
|
||||
*/
|
||||
export default class DiscussionPageResolver extends DefaultResolver {
|
||||
static scrollToPostNumber: string | null = null;
|
||||
|
||||
makeKey() {
|
||||
const params = { ...m.route.param() };
|
||||
if ('near' in params) {
|
||||
delete params.near;
|
||||
}
|
||||
params.id = getDiscussionIdFromSlug(params.id);
|
||||
return this.routeName.replace('.near', '') + JSON.stringify(params);
|
||||
}
|
||||
|
||||
onmatch(args, requestedPath, route) {
|
||||
if (app.current.matches(DiscussionPage) && getDiscussionIdFromSlug(args.id) === getDiscussionIdFromSlug(m.route.param('id'))) {
|
||||
// By default, the first post number of any discussion is 1
|
||||
DiscussionPageResolver.scrollToPostNumber = args.near || '1';
|
||||
}
|
||||
|
||||
return super.onmatch(args, requestedPath, route);
|
||||
}
|
||||
|
||||
render(vnode) {
|
||||
if (DiscussionPageResolver.scrollToPostNumber !== null) {
|
||||
const number = DiscussionPageResolver.scrollToPostNumber;
|
||||
// Scroll after a timeout to avoid clashes with the render.
|
||||
setTimeout(() => app.current.get('stream').goToNumber(number));
|
||||
DiscussionPageResolver.scrollToPostNumber = null;
|
||||
}
|
||||
|
||||
return super.render(vnode);
|
||||
}
|
||||
}
|
@@ -4,7 +4,6 @@ import PostsUserPage from './components/PostsUserPage';
|
||||
import DiscussionsUserPage from './components/DiscussionsUserPage';
|
||||
import SettingsPage from './components/SettingsPage';
|
||||
import NotificationsPage from './components/NotificationsPage';
|
||||
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
|
||||
|
||||
/**
|
||||
* The `routes` initializer defines the forum app's routes.
|
||||
@@ -15,8 +14,8 @@ export default function (app) {
|
||||
app.routes = {
|
||||
index: { path: '/all', component: IndexPage },
|
||||
|
||||
discussion: { path: '/d/:id', component: DiscussionPage, resolverClass: DiscussionPageResolver },
|
||||
'discussion.near': { path: '/d/:id/:near', component: DiscussionPage, resolverClass: DiscussionPageResolver },
|
||||
discussion: { path: '/d/:id', component: DiscussionPage },
|
||||
'discussion.near': { path: '/d/:id/:near', component: DiscussionPage },
|
||||
|
||||
user: { path: '/u/:username', component: PostsUserPage },
|
||||
'user.posts': { path: '/u/:username', component: PostsUserPage },
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import subclassOf from '../../common/utils/subclassOf';
|
||||
import Stream from '../../common/utils/Stream';
|
||||
import ReplyComposer from '../components/ReplyComposer';
|
||||
|
||||
class ComposerState {
|
||||
@@ -75,7 +74,7 @@ class ComposerState {
|
||||
this.onExit = null;
|
||||
|
||||
this.fields = {
|
||||
content: Stream(''),
|
||||
content: m.stream(''),
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -77,23 +77,17 @@ export default class DiscussionListState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear and reload the discussion list. Passing the option `deferClear: true`
|
||||
* will clear discussions only after new data has been received.
|
||||
* This can be used to refresh discussions without loading animations.
|
||||
* Clear and reload the discussion list.
|
||||
*/
|
||||
refresh({ deferClear = false } = {}) {
|
||||
refresh({ clear = true } = {}) {
|
||||
this.loading = true;
|
||||
|
||||
if (!deferClear) {
|
||||
if (clear) {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
return this.loadResults().then(
|
||||
(results) => {
|
||||
// This ensures that any changes made while waiting on this request
|
||||
// are ignored. Otherwise, we could get duplicate discussions.
|
||||
// We don't use `this.clear()` to avoid an unnecessary redraw.
|
||||
this.discussions = [];
|
||||
this.parseResults(results);
|
||||
},
|
||||
() => {
|
||||
@@ -119,7 +113,7 @@ export default class DiscussionListState {
|
||||
params.page = { offset };
|
||||
params.include = params.include.join(',');
|
||||
|
||||
return this.app.store.find('discussions', params, { search: params.filter.q });
|
||||
return this.app.store.find('discussions', params);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -2,8 +2,9 @@ import setRouteWithForcedRefresh from '../../common/utils/setRouteWithForcedRefr
|
||||
import SearchState from './SearchState';
|
||||
|
||||
export default class GlobalSearchState extends SearchState {
|
||||
constructor(cachedSearches = []) {
|
||||
constructor(cachedSearches = [], searchRoute = 'index') {
|
||||
super(cachedSearches);
|
||||
this.searchRoute = searchRoute;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
@@ -90,6 +91,6 @@ export default class GlobalSearchState extends SearchState {
|
||||
const params = this.params();
|
||||
delete params.q;
|
||||
|
||||
setRouteWithForcedRefresh(app.route(app.current.get('routeName'), params));
|
||||
setRouteWithForcedRefresh(app.route(this.searchRoute, params));
|
||||
}
|
||||
}
|
||||
|
@@ -37,18 +37,6 @@ class PostStreamState {
|
||||
*/
|
||||
this.description = '';
|
||||
|
||||
/**
|
||||
* When the page is scrolled, goToIndex is called, or the page is loaded,
|
||||
* various listeners result in the scrubber being updated with a new
|
||||
* position and values. However, if goToNumber is called, the scrubber
|
||||
* will not be updated. Accordingly, we add logic to the scrubber's
|
||||
* onupdate to update itself, but only when needed, as indicated by this
|
||||
* property.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.forceUpdateScrubber = false;
|
||||
|
||||
this.show(includedPosts);
|
||||
}
|
||||
|
||||
@@ -96,18 +84,15 @@ class PostStreamState {
|
||||
// If we want to go to the reply preview, then we will go to the end of the
|
||||
// discussion and then scroll to the very bottom of the page.
|
||||
if (number === 'reply') {
|
||||
const resultPromise = this.goToLast();
|
||||
this.targetPost.reply = true;
|
||||
return resultPromise;
|
||||
return this.goToLast();
|
||||
}
|
||||
|
||||
this.paused = true;
|
||||
|
||||
this.loadPromise = this.loadNearNumber(number);
|
||||
|
||||
this.needsScroll = true;
|
||||
this.targetPost = { number };
|
||||
this.animateScroll = !noAnimation;
|
||||
this.noAnimationScroll = noAnimation;
|
||||
this.number = number;
|
||||
|
||||
// In this case, the redraw is only called after the response has been loaded
|
||||
@@ -130,9 +115,8 @@ class PostStreamState {
|
||||
|
||||
this.loadPromise = this.loadNearIndex(index);
|
||||
|
||||
this.needsScroll = true;
|
||||
this.targetPost = { index };
|
||||
this.animateScroll = !noAnimation;
|
||||
this.noAnimationScroll = noAnimation;
|
||||
this.index = index;
|
||||
|
||||
m.redraw();
|
||||
@@ -282,13 +266,7 @@ class PostStreamState {
|
||||
}
|
||||
});
|
||||
|
||||
if (loadIds.length) {
|
||||
return app.store.find('posts', loadIds).then((newPosts) => {
|
||||
return loaded.concat(newPosts).sort((a, b) => a.createdAt() - b.createdAt());
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(loaded);
|
||||
return loadIds.length ? app.store.find('posts', loadIds) : Promise.resolve(loaded);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,12 +332,7 @@ class PostStreamState {
|
||||
* @return {boolean}
|
||||
*/
|
||||
viewingEnd() {
|
||||
// In some cases, such as if we've stickied a post, an event post
|
||||
// may have been added / removed. This means that `this.visibleEnd`
|
||||
// and`this.count()` will be out of sync by 1 post, but we are still
|
||||
// "viewing the end" of the post stream, so we should still reload
|
||||
// all posts up until the last one.
|
||||
return Math.abs(this.count() - this.visibleEnd) <= 1;
|
||||
return this.visibleEnd === this.count();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -61,9 +61,7 @@ export default {
|
||||
onclick: () => {
|
||||
// If the user is not logged in, the promise rejects, and a login modal shows up.
|
||||
// Since that's already handled, we dont need to show an error message in the console.
|
||||
return this.replyAction
|
||||
.bind(discussion)(true, false)
|
||||
.catch(() => {});
|
||||
return this.replyAction(discussion, true, false).catch(() => {});
|
||||
},
|
||||
},
|
||||
app.translator.trans(
|
||||
|
@@ -129,7 +129,9 @@ export default {
|
||||
error: 'core.forum.user_controls.delete_error_message',
|
||||
}[type];
|
||||
|
||||
app.alerts.show({ type }, app.translator.trans(message, { username, email }));
|
||||
app.alerts.show(app.translator.trans(message, { username, email }), {
|
||||
type,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@@ -52,18 +52,13 @@ export default function alertEmailConfirmation(app) {
|
||||
}
|
||||
}
|
||||
|
||||
class ContainedAlert extends Alert {
|
||||
view(vnode) {
|
||||
const vdom = super.view(vnode);
|
||||
return { ...vdom, children: [<div className="container">{vdom.children}</div>] };
|
||||
}
|
||||
}
|
||||
|
||||
m.mount($('<div/>').insertBefore('#content')[0], {
|
||||
view: () => (
|
||||
<ContainedAlert dismissible={false} controls={[<ResendButton />]}>
|
||||
{app.translator.trans('core.forum.user_email_confirmation.alert_message', { email: <strong>{user.email()}</strong> })}
|
||||
</ContainedAlert>
|
||||
<Alert dismissible={false} controls={[<ResendButton />]}>
|
||||
<div className="container">
|
||||
{app.translator.trans('core.forum.user_email_confirmation.alert_message', { email: <strong>{user.email()}</strong> })}
|
||||
</div>
|
||||
</Alert>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@@ -236,16 +236,12 @@
|
||||
.App-header {
|
||||
padding: 8px;
|
||||
height: @header-height;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: @zindex-header;
|
||||
|
||||
.affix & {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
& when (@config-colored-header = true) {
|
||||
.light-contents(@header-color, @header-control-bg, @header-control-color);
|
||||
}
|
||||
|
@@ -105,10 +105,6 @@
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.off.Checkbox--switch .Checkbox-display {
|
||||
background: @muted-more-color;
|
||||
}
|
||||
}
|
||||
.Modal-footer {
|
||||
border: 0;
|
||||
|
@@ -271,8 +271,6 @@
|
||||
}
|
||||
}
|
||||
.Post-footer {
|
||||
display: inline-block;
|
||||
height: 0;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
@@ -290,6 +288,7 @@
|
||||
margin-top: -5px;
|
||||
float: right;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.transition(opacity 0.2s);
|
||||
|
||||
|
@@ -6,7 +6,18 @@
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeIn {
|
||||
0% {opacity: 0}
|
||||
100% {opacity: 1}
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
0% {opacity: 0}
|
||||
100% {opacity: 1}
|
||||
}
|
||||
.PostStream-item {
|
||||
.animation(fadeIn 0.6s ease-in-out);
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid @control-bg;
|
||||
|
||||
@@ -93,16 +104,3 @@
|
||||
.animation(pulsate 0.2s ease-in-out);
|
||||
.animation-iteration-count(1);
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeIn {
|
||||
0% {opacity: 0}
|
||||
100% {opacity: 1}
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
0% {opacity: 0}
|
||||
100% {opacity: 1}
|
||||
}
|
||||
.fadeIn {
|
||||
.animation(fadeIn 0.4s ease-in-out);
|
||||
.animation-iteration-count(1);
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: 0;
|
||||
color: #fff !important;
|
||||
@@ -36,15 +36,12 @@
|
||||
.Slidable-underneath--left {
|
||||
text-align: left;
|
||||
}
|
||||
.Slidable-underneath--right {
|
||||
left: unset;
|
||||
}
|
||||
.Slidable-content {
|
||||
.transition(~"box-shadow 0.2s, border-radius 0.2s");
|
||||
|
||||
.sliding& {
|
||||
position: relative;
|
||||
background: @control-bg;
|
||||
background: #fff;
|
||||
z-index: 2;
|
||||
border-radius: 2px;
|
||||
.box-shadow(0 2px 6px @shadow-color);
|
||||
|
@@ -9,8 +9,6 @@
|
||||
|
||||
namespace Flarum\Admin;
|
||||
|
||||
use Flarum\Extension\Event\Disabled;
|
||||
use Flarum\Extension\Event\Enabled;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Foundation\ErrorHandling\Registry;
|
||||
use Flarum\Foundation\ErrorHandling\Reporter;
|
||||
@@ -54,25 +52,20 @@ class AdminServiceProvider extends AbstractServiceProvider
|
||||
HttpMiddleware\StartSession::class,
|
||||
HttpMiddleware\RememberFromCookie::class,
|
||||
HttpMiddleware\AuthenticateWithSession::class,
|
||||
HttpMiddleware\SetLocale::class,
|
||||
'flarum.admin.route_resolver',
|
||||
HttpMiddleware\CheckCsrfToken::class,
|
||||
Middleware\RequireAdministrateAbility::class
|
||||
HttpMiddleware\SetLocale::class,
|
||||
Middleware\RequireAdministrateAbility::class,
|
||||
];
|
||||
});
|
||||
|
||||
$this->app->bind('flarum.admin.error_handler', function () {
|
||||
return new HttpMiddleware\HandleErrors(
|
||||
$this->app->make(Registry::class),
|
||||
$this->app['flarum.config']->inDebugMode() ? $this->app->make(WhoopsFormatter::class) : $this->app->make(ViewFormatter::class),
|
||||
$this->app['flarum']->inDebugMode() ? $this->app->make(WhoopsFormatter::class) : $this->app->make(ViewFormatter::class),
|
||||
$this->app->tagged(Reporter::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('flarum.admin.route_resolver', function () {
|
||||
return new HttpMiddleware\ResolveRoute($this->app->make('flarum.admin.routes'));
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.admin.handler', function () {
|
||||
$pipe = new MiddlewarePipe;
|
||||
|
||||
@@ -80,7 +73,7 @@ class AdminServiceProvider extends AbstractServiceProvider
|
||||
$pipe->pipe($this->app->make($middleware));
|
||||
}
|
||||
|
||||
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
|
||||
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.admin.routes')));
|
||||
|
||||
return $pipe;
|
||||
});
|
||||
@@ -123,7 +116,7 @@ class AdminServiceProvider extends AbstractServiceProvider
|
||||
$events = $this->app->make('events');
|
||||
|
||||
$events->listen(
|
||||
[Enabled::class, Disabled::class, ClearingCache::class],
|
||||
ClearingCache::class,
|
||||
function () {
|
||||
$recompile = new RecompileFrontendAssets(
|
||||
$this->app->make('flarum.assets.admin'),
|
||||
|
@@ -51,24 +51,19 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
HttpMiddleware\RememberFromCookie::class,
|
||||
HttpMiddleware\AuthenticateWithSession::class,
|
||||
HttpMiddleware\AuthenticateWithHeader::class,
|
||||
HttpMiddleware\CheckCsrfToken::class,
|
||||
HttpMiddleware\SetLocale::class,
|
||||
'flarum.api.route_resolver',
|
||||
HttpMiddleware\CheckCsrfToken::class
|
||||
];
|
||||
});
|
||||
|
||||
$this->app->bind('flarum.api.error_handler', function () {
|
||||
return new HttpMiddleware\HandleErrors(
|
||||
$this->app->make(Registry::class),
|
||||
new JsonApiFormatter($this->app['flarum.config']->inDebugMode()),
|
||||
new JsonApiFormatter($this->app['flarum']->inDebugMode()),
|
||||
$this->app->tagged(Reporter::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('flarum.api.route_resolver', function () {
|
||||
return new HttpMiddleware\ResolveRoute($this->app->make('flarum.api.routes'));
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.api.handler', function () {
|
||||
$pipe = new MiddlewarePipe;
|
||||
|
||||
@@ -76,16 +71,10 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
$pipe->pipe($this->app->make($middleware));
|
||||
}
|
||||
|
||||
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
|
||||
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.api.routes')));
|
||||
|
||||
return $pipe;
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.api.notification_serializers', function () {
|
||||
return [
|
||||
'discussionRenamed' => BasicDiscussionSerializer::class
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,7 +82,7 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->setNotificationSerializers();
|
||||
$this->registerNotificationSerializers();
|
||||
|
||||
AbstractSerializeController::setContainer($this->app);
|
||||
AbstractSerializeController::setEventDispatcher($events = $this->app->make('events'));
|
||||
@@ -105,12 +94,13 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
/**
|
||||
* Register notification serializers.
|
||||
*/
|
||||
protected function setNotificationSerializers()
|
||||
protected function registerNotificationSerializers()
|
||||
{
|
||||
$blueprints = [];
|
||||
$serializers = $this->app->make('flarum.api.notification_serializers');
|
||||
$serializers = [
|
||||
'discussionRenamed' => BasicDiscussionSerializer::class
|
||||
];
|
||||
|
||||
// Deprecated in beta 15, remove in beta 16
|
||||
$this->app->make('events')->dispatch(
|
||||
new ConfigureNotificationTypes($blueprints, $serializers)
|
||||
);
|
||||
|
@@ -11,10 +11,10 @@ namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\DiscussionRepository;
|
||||
use Flarum\Discussion\Search\DiscussionSearcher;
|
||||
use Flarum\Filter\Filterer;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
@@ -49,14 +49,9 @@ class ListDiscussionsController extends AbstractListController
|
||||
public $sortFields = ['lastPostedAt', 'commentCount', 'createdAt'];
|
||||
|
||||
/**
|
||||
* @var DiscussionRepository
|
||||
* @var DiscussionSearcher
|
||||
*/
|
||||
protected $discussions;
|
||||
|
||||
/**
|
||||
* @var Filterer
|
||||
*/
|
||||
protected $filterer;
|
||||
protected $searcher;
|
||||
|
||||
/**
|
||||
* @var UrlGenerator
|
||||
@@ -67,10 +62,9 @@ class ListDiscussionsController extends AbstractListController
|
||||
* @param DiscussionSearcher $searcher
|
||||
* @param UrlGenerator $url
|
||||
*/
|
||||
public function __construct(DiscussionRepository $discussions, Filterer $filterer, UrlGenerator $url)
|
||||
public function __construct(DiscussionSearcher $searcher, UrlGenerator $url)
|
||||
{
|
||||
$this->discussions = $discussions;
|
||||
$this->filterer = $filterer;
|
||||
$this->searcher = $searcher;
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
@@ -80,16 +74,16 @@ class ListDiscussionsController extends AbstractListController
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$actor = $request->getAttribute('actor');
|
||||
|
||||
$filters = $this->extractFilter($request);
|
||||
$query = Arr::get($this->extractFilter($request), 'q');
|
||||
$sort = $this->extractSort($request);
|
||||
$query = $this->discussions->query();
|
||||
|
||||
$criteria = new SearchCriteria($actor, $query, $sort);
|
||||
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->extractOffset($request);
|
||||
$load = array_merge($this->extractInclude($request), ['state']);
|
||||
|
||||
$results = $this->filterer->filter($actor, $query, $filters, $sort, $limit, $offset, $load);
|
||||
$results = $this->searcher->search($criteria, $limit, $offset);
|
||||
|
||||
$document->addPaginationLinks(
|
||||
$this->url->to('api')->route('discussions.index'),
|
||||
|
@@ -10,9 +10,10 @@
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Filter\Filterer;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\User\UserRepository;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\User\Search\UserSearcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
@@ -40,9 +41,9 @@ class ListUsersController extends AbstractListController
|
||||
];
|
||||
|
||||
/**
|
||||
* @var Filterer
|
||||
* @var UserSearcher
|
||||
*/
|
||||
protected $filterer;
|
||||
protected $searcher;
|
||||
|
||||
/**
|
||||
* @var UrlGenerator
|
||||
@@ -50,20 +51,13 @@ class ListUsersController extends AbstractListController
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
protected $users;
|
||||
|
||||
/**
|
||||
* @param Filterer $filterer
|
||||
* @param UserSearcher $searcher
|
||||
* @param UrlGenerator $url
|
||||
* @param UserRepository $users
|
||||
*/
|
||||
public function __construct(Filterer $filterer, UrlGenerator $url, UserRepository $users)
|
||||
public function __construct(UserSearcher $searcher, UrlGenerator $url)
|
||||
{
|
||||
$this->filterer = $filterer;
|
||||
$this->searcher = $searcher;
|
||||
$this->url = $url;
|
||||
$this->users = $users;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,16 +69,16 @@ class ListUsersController extends AbstractListController
|
||||
|
||||
$actor->assertCan('viewUserList');
|
||||
|
||||
$query = $this->users->query();
|
||||
|
||||
$filters = $this->extractFilter($request);
|
||||
$query = Arr::get($this->extractFilter($request), 'q');
|
||||
$sort = $this->extractSort($request);
|
||||
|
||||
$criteria = new SearchCriteria($actor, $query, $sort);
|
||||
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->extractOffset($request);
|
||||
$load = $this->extractInclude($request);
|
||||
|
||||
$results = $this->filterer->filter($actor, $query, $filters, $sort, $limit, $offset, $load);
|
||||
$results = $this->searcher->search($criteria, $limit, $offset, $load);
|
||||
|
||||
$document->addPaginationLinks(
|
||||
$this->url->to('api')->route('users.index'),
|
||||
|
@@ -1,112 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Discussion\Search\DiscussionSearcher;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class SearchDiscussionsController extends AbstractListController
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $serializer = DiscussionSerializer::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $include = [
|
||||
'user',
|
||||
'lastPostedUser',
|
||||
'mostRelevantPost',
|
||||
'mostRelevantPost.user'
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $optionalInclude = [
|
||||
'firstPost',
|
||||
'lastPost'
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $sortFields = ['lastPostedAt', 'commentCount', 'createdAt'];
|
||||
|
||||
/**
|
||||
* @var DiscussionSearcher
|
||||
*/
|
||||
protected $searcher;
|
||||
|
||||
/**
|
||||
* @var UrlGenerator
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @param DiscussionSearcher $searcher
|
||||
* @param UrlGenerator $url
|
||||
*/
|
||||
public function __construct(DiscussionSearcher $searcher, UrlGenerator $url)
|
||||
{
|
||||
$this->searcher = $searcher;
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$actor = $request->getAttribute('actor');
|
||||
$query = Arr::get($this->extractFilter($request), 'q');
|
||||
$sort = $this->extractSort($request);
|
||||
|
||||
$criteria = new SearchCriteria($actor, $query, $sort);
|
||||
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->extractOffset($request);
|
||||
$load = array_merge($this->extractInclude($request), ['state']);
|
||||
|
||||
$results = $this->searcher->search($criteria, $limit, $offset);
|
||||
|
||||
$document->addPaginationLinks(
|
||||
$this->url->to('api')->route('discussions.index'),
|
||||
$request->getQueryParams(),
|
||||
$offset,
|
||||
$limit,
|
||||
$results->areMoreResults() ? null : 0
|
||||
);
|
||||
|
||||
Discussion::setStateUser($actor);
|
||||
|
||||
$results = $results->getResults()->load($load);
|
||||
|
||||
if ($relations = array_intersect($load, ['firstPost', 'lastPost'])) {
|
||||
foreach ($results as $discussion) {
|
||||
foreach ($relations as $relation) {
|
||||
if ($discussion->$relation) {
|
||||
$discussion->$relation->discussion = $discussion;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
@@ -1,93 +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\Api\Controller;
|
||||
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\User\Search\UserSearcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class SearchUsersController extends AbstractListController
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $serializer = UserSerializer::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $include = ['groups'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $sortFields = [
|
||||
'username',
|
||||
'commentCount',
|
||||
'discussionCount',
|
||||
'lastSeenAt',
|
||||
'joinedAt'
|
||||
];
|
||||
|
||||
/**
|
||||
* @var UserSearcher
|
||||
*/
|
||||
protected $searcher;
|
||||
|
||||
/**
|
||||
* @var UrlGenerator
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @param UserSearcher $searcher
|
||||
* @param UrlGenerator $url
|
||||
*/
|
||||
public function __construct(UserSearcher $searcher, UrlGenerator $url)
|
||||
{
|
||||
$this->searcher = $searcher;
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$actor = $request->getAttribute('actor');
|
||||
|
||||
$actor->assertCan('viewUserList');
|
||||
|
||||
$query = Arr::get($this->extractFilter($request), 'q');
|
||||
$sort = $this->extractSort($request);
|
||||
|
||||
$criteria = new SearchCriteria($actor, $query, $sort);
|
||||
|
||||
$limit = $this->extractLimit($request);
|
||||
$offset = $this->extractOffset($request);
|
||||
$load = $this->extractInclude($request);
|
||||
|
||||
$results = $this->searcher->search($criteria, $limit, $offset, $load);
|
||||
|
||||
$document->addPaginationLinks(
|
||||
$this->url->to('api')->route('users.index'),
|
||||
$request->getQueryParams(),
|
||||
$offset,
|
||||
$limit,
|
||||
$results->areMoreResults() ? null : 0
|
||||
);
|
||||
|
||||
return $results->getResults();
|
||||
}
|
||||
}
|
@@ -10,7 +10,6 @@
|
||||
namespace Flarum\Api\Serializer;
|
||||
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Foundation\Config;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
|
||||
@@ -22,9 +21,9 @@ class ForumSerializer extends AbstractSerializer
|
||||
protected $type = 'forums';
|
||||
|
||||
/**
|
||||
* @var Config
|
||||
* @var Application
|
||||
*/
|
||||
protected $config;
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
@@ -37,13 +36,13 @@ class ForumSerializer extends AbstractSerializer
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @param Config $config
|
||||
* @param Application $app
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param UrlGenerator $url
|
||||
*/
|
||||
public function __construct(Config $config, SettingsRepositoryInterface $settings, UrlGenerator $url)
|
||||
public function __construct(Application $app, SettingsRepositoryInterface $settings, UrlGenerator $url)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->app = $app;
|
||||
$this->settings = $settings;
|
||||
$this->url = $url;
|
||||
}
|
||||
@@ -67,7 +66,7 @@ class ForumSerializer extends AbstractSerializer
|
||||
'showLanguageSelector' => (bool) $this->settings->get('show_language_selector', true),
|
||||
'baseUrl' => $url = $this->url->to('forum')->base(),
|
||||
'basePath' => parse_url($url, PHP_URL_PATH) ?: '',
|
||||
'debug' => $this->config->inDebugMode(),
|
||||
'debug' => $this->app->inDebugMode(),
|
||||
'apiUrl' => $this->url->to('api')->base(),
|
||||
'welcomeTitle' => $this->settings->get('welcome_title'),
|
||||
'welcomeMessage' => $this->settings->get('welcome_message'),
|
||||
|
@@ -95,13 +95,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
|
||||
$route->toController(Controller\SendConfirmationEmailController::class)
|
||||
);
|
||||
|
||||
// List users
|
||||
$map->get(
|
||||
'/search/users',
|
||||
'users.search',
|
||||
$route->toController(Controller\SearchUsersController::class)
|
||||
);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Notifications
|
||||
@@ -170,13 +163,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
|
||||
$route->toController(Controller\DeleteDiscussionController::class)
|
||||
);
|
||||
|
||||
// Search discussions
|
||||
$map->get(
|
||||
'/search/discussions',
|
||||
'discussions.search',
|
||||
$route->toController(Controller\SearchDiscussionsController::class)
|
||||
);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Posts
|
||||
|
@@ -14,7 +14,6 @@ use Flarum\Database\Console\MigrateCommand;
|
||||
use Flarum\Database\Console\ResetCommand;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Foundation\Console\CacheClearCommand;
|
||||
use Flarum\Foundation\Console\InfoCommand;
|
||||
|
||||
class ConsoleServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
@@ -27,7 +26,6 @@ class ConsoleServiceProvider extends AbstractServiceProvider
|
||||
return [
|
||||
CacheClearCommand::class,
|
||||
GenerateMigrationCommand::class,
|
||||
InfoCommand::class,
|
||||
MigrateCommand::class,
|
||||
ResetCommand::class,
|
||||
];
|
||||
|
@@ -82,7 +82,7 @@ abstract class AbstractModel extends Eloquent
|
||||
}
|
||||
|
||||
$this->attributes = array_map(function ($item) {
|
||||
return is_callable($item) ? $item($this) : $item;
|
||||
return is_callable($item) ? $item() : $item;
|
||||
}, $this->attributes);
|
||||
|
||||
parent::__construct($attributes);
|
||||
|
@@ -13,9 +13,6 @@ use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||
use InvalidArgumentException;
|
||||
use ReflectionClass;
|
||||
|
||||
/**
|
||||
* @deprecated in beta 15, removed in beta 16
|
||||
*/
|
||||
class ConfigureNotificationTypes
|
||||
{
|
||||
/**
|
||||
|
@@ -9,9 +9,6 @@
|
||||
|
||||
namespace Flarum\Event;
|
||||
|
||||
/**
|
||||
* @deprecated in beta 15, remove in beta 16. Use the Post extender instead.
|
||||
*/
|
||||
class ConfigurePostTypes
|
||||
{
|
||||
private $models;
|
||||
|
@@ -14,28 +14,11 @@ use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class Csrf implements ExtenderInterface
|
||||
{
|
||||
protected $csrfExemptRoutes = [];
|
||||
protected $csrfExemptPaths = [];
|
||||
|
||||
/**
|
||||
* Exempt a named route from CSRF checks.
|
||||
*
|
||||
* @param string $routeName
|
||||
*/
|
||||
public function exemptRoute(string $routeName)
|
||||
{
|
||||
$this->csrfExemptRoutes[] = $routeName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exempt a path from csrf checks. Wildcards are supported.
|
||||
*
|
||||
* @deprecated beta 15, remove beta 16. Exempt routes should be used instead.
|
||||
*/
|
||||
public function exemptPath(string $path)
|
||||
{
|
||||
$this->csrfExemptRoutes[] = $path;
|
||||
$this->csrfExemptPaths[] = $path;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -43,7 +26,7 @@ class Csrf implements ExtenderInterface
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
$container->extend('flarum.http.csrfExemptPaths', function ($existingExemptPaths) {
|
||||
return array_merge($existingExemptPaths, $this->csrfExemptRoutes);
|
||||
return array_merge($existingExemptPaths, $this->csrfExemptPaths);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user