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

Compare commits

...

93 Commits

Author SHA1 Message Date
Alexander Skvortsov
f2aa804753 Apply fixes from StyleCI
[ci skip] [skip ci]
2020-11-13 07:56:37 +00:00
Alexander Skvortsov
baa5fdad95 Add extender and tests for filter 2020-11-13 02:56:13 -05:00
Alexander Skvortsov
0b2d01b75f Define FilterInterface 2020-11-13 02:55:58 -05:00
Alexander Skvortsov
84c4e8dd6e Ensure that search AND list endpoints are tested for users and discussions 2020-11-13 02:55:17 -05:00
Alexander Skvortsov
c4daeeb1f2 Add abstract filterer system, have discussions and users controllers use that instead of default filterer system 2020-11-13 01:47:15 -05:00
Alexander Skvortsov
87d2f3d246 If a search parameter is present (filter.q), send requests to the search endpoint instead of the filter endpoint 2020-11-12 14:29:00 -05:00
Alexander Skvortsov
0172008693 Add Search controllers for Discussions and Users, as copies of the ListDiscussions and ListUsers controllers 2020-11-12 14:20:18 -05:00
Alexander Skvortsov
76680d197d Introduce RequestUtil
Abstracts access to following request attributes:

- actor
- session
- locale
- route name
2020-11-11 17:16:56 -05:00
Alexander Skvortsov
0c95774333 Refactor Route Resolving and Dispatch (#2425)
- Split DispatchRoute. This allows us to run middleware after we figure out which route we're on, but before we actually execute the controller for that route.
- By making the route name explicitly available to middlewares, applications like CSRF and floodgate can set patterns based on route names instead of the path, which is an implementation detail.
- Support using route name match for CSRF extender, deprecate path match
2020-11-10 12:52:12 -05:00
Nina Pypchenko
67741c7a6f Make checkbox switch component background stand out in modals (#2443) 2020-11-09 20:54:21 -05:00
Alexander Skvortsov
f5cfec15e3 Add missing import 2020-11-08 21:49:11 -05:00
Alexander Skvortsov
47d2eee9ce Fix Callables for Extenders (#2423)
- Standardize signatures and variable names for extenders that take callbacks
- Adjust model extender docblock to clarify that default calue can't be an invokable class.
- Make invokable classes provided to Model->relationship
- Add integration tests to ensure Model->relationship and User->groupProcessor extenders accept callbacks
- Extract code for wrapping callbacks into central util
2020-11-08 21:36:38 -05:00
Nina Pypchenko
c10cc92deb Improved Permissions Error Messages for Initial Install (#2435)
- Made the wording of the error more generic
- Added link to the relevant section in the installation guide

Resolves #2327.
2020-11-07 14:48:11 -05:00
Sami Mazouz
529d2edcaf Add Service Provider Extender (#2437) 2020-11-06 13:30:10 -05:00
Sami Mazouz
f0e77a5789 Add Notification Channel Extender (#2432) 2020-11-05 12:09:06 -05:00
Alexander Skvortsov
87c258b2f8 Refactor and improve formatter extender (#2098)
- Deprecated all events involved with Formatter
- Refactor ->configure() method on extender not to use events
- Add extender methods for ->render() and ->parse()
- Add integration tests
2020-11-03 13:05:33 -05:00
Alexander Skvortsov
cee87848fe Added post extender with type method, deprecated ConfigurePostTypes (#2101) 2020-11-03 10:43:49 -05:00
Alexander Skvortsov
5842dd1200 Validator extender (#2102)
Added validator extender, integration tests, and deprecated related Validating event
2020-11-01 11:31:16 -05:00
Sami Mazouz
b311512502 Add Notification Type Extender and Tests (#2424) 2020-10-31 17:17:14 -04:00
flarum-bot
0b2a5fa5b8 Bundled output for commit 52e45aacad [skip ci] 2020-10-31 00:28:56 +00:00
Lucas Henrique
52e45aacad Convert common time helpers to Typescript (#2391) 2020-10-30 20:27:40 -04:00
Alexander Skvortsov
21c2a4b2a4 Updater should show on any subpath, like installer (#2426) 2020-10-30 13:28:20 -04:00
flarum-bot
12c03dc4e1 Bundled output for commit d2927cfdb9 [skip ci] 2020-10-29 16:54:36 +00:00
Alexander Skvortsov
d2927cfdb9 Ensure scripts provided by textformatter are run (#2415) 2020-10-29 12:53:23 -04:00
Daniël Klabbers
24b7a21507 Update Symfony components to v4 (#2407)
This matches the Symfony dependencies of our laravel dependencies.
2020-10-27 17:12:36 -04:00
flarum-bot
c9a04fe009 Bundled output for commit bd7fa11b5a [skip ci] 2020-10-25 17:36:51 +00:00
Alexander Skvortsov
bd7fa11b5a Export SuperTextarea util in compat 2020-10-25 13:35:15 -04:00
Daniël Klabbers
7055f6d941 update version constant 2020-10-20 16:34:54 +02:00
Alexander Skvortsov
f765001f06 Update email 2020-10-20 10:32:24 -04:00
Daniël Klabbers
683739a617 changelog v0.1.0-beta.14 and added core developer @askvortsov1 2020-10-20 16:24:43 +02:00
flarum-bot
69b7fe8d01 Bundled output for commit 1936b9117d [skip ci] 2020-10-17 17:43:39 +00:00
Alexander Skvortsov
1936b9117d Page Scroll Cleanup (#2396)
- Reintroduce cancellable scroll top on page change
- IndexPage: rely on browser to retain scroll position on page reload
- Remove obsolete browser hack
- Fix broken selector
- When on mobile, only retain scroll for IndexPage if we're coming from a discussion
- Move app.cache.scrollTop save into `onbeforeremove` so we make sure to do it before DOM is detached
2020-10-17 13:42:33 -04:00
flarum-bot
d53eeded44 Bundled output for commit 0650788e7c [skip ci] 2020-10-16 20:32:13 +00:00
Alexander Skvortsov
0650788e7c Fix scolling to first post via m.route.set
The default first post number is '1', so we scroll to that if we're calling `m.route.set` without a `near` parameter, as that means we're scrolling to the top.

This was present in beta 13's implementation, but accidentially omitted in 988b6c9.

We also remove unnecessary typecasting for simpler logic and increased consistency with beta 13.
2020-10-16 16:30:27 -04:00
flarum-bot
6a77184611 Bundled output for commit a8b36cb76d [skip ci] 2020-10-16 20:05:15 +00:00
Alexander Skvortsov
a8b36cb76d Fix check for going between discussion pages.
The current implementation for checking whether we are on a discussion page, and going to a discussion page, checks the route we are going to. This is problematic, because the route resolver represents the route being considered, not the route we are currently on. So, if we are currently using a DiscussionPageResolver, we must be going to a route handled by DiscussionPage. Instead, we need to check the route that we are currently on, which is done via `app.current.matches(DiscussionPage)`.
2020-10-16 16:03:32 -04:00
flarum-bot
5cd14d594b Bundled output for commit f4ad9d2d5a [skip ci] 2020-10-16 16:07:09 +00:00
Alexander Skvortsov
f4ad9d2d5a Fix scrolling to reply via 'reply' as near parameter 2020-10-16 12:04:45 -04:00
Alexander Skvortsov
d409484abf Notification: fix wrong external attr for Link 2020-10-16 11:53:45 -04:00
flarum-bot
1fc24635f6 Bundled output for commit ff7ac0b322 [skip ci] 2020-10-16 05:26:03 +00:00
Alexander Skvortsov
ff7ac0b322 Fix PostStream loadRange doesn't return all posts (#2384)
- Also, ensure that posts are ordered by creation timestamp
2020-10-16 01:24:45 -04:00
Wadim Kalmykov
d460aaa3ad order posts by creation date 2020-10-16 01:20:54 -04:00
Wadim Kalmykov
7634a766cb Fix loadRange doesn't return all posts 2020-10-16 01:20:54 -04:00
flarum-bot
e5f53b93a6 Bundled output for commit a38c92d409 [skip ci] 2020-10-16 01:59:56 +00:00
Alexander Skvortsov
a38c92d409 Fix broken import 2020-10-15 21:58:18 -04:00
Alexander Skvortsov
3da655a62f Rename resolver to resolvers for consistency 2020-10-15 21:26:34 -04:00
flarum-bot
46c3124b0b Bundled output for commit e6f59b834f [skip ci] 2020-10-15 22:20:11 +00:00
Alexander Skvortsov
e6f59b834f Default force attr to true on LinkButton
This retains beta 13 behavior.
2020-10-15 18:18:51 -04:00
Alexander Skvortsov
9f5737eb93 Fix routeName attr not being passed into pages 2020-10-15 18:14:20 -04:00
flarum-bot
35cb5b20a0 Bundled output for commit 988b6c9023 [skip ci] 2020-10-15 22:02:46 +00:00
Alexander Skvortsov
988b6c9023 Allow extensions to use route resolvers (#2275)
- mapRoutes: don't wrap components in resolvers if they are already resolvers
- Extract defaultResolver into its own class
- Allow either route resolver instances, or components with an optional resolverClass which should accept the component and route name in its constructor.
- Introduce a resolver for DiscussionPage, so that routing from one post to another on the same discussion triggers a scroll instead of rerendering
2020-10-15 18:01:17 -04:00
flarum-bot
c1d91be2f4 Bundled output for commit f534398645 [skip ci] 2020-10-15 21:47:33 +00:00
Alexander Skvortsov
f534398645 Fix PostStream Reply Scroll (#2366)
- Add an index to reply placeholder so we can scroll to it directly when replying.
- Stop pretending that the currently broken `bottom` scroll functionality works, and explicitly call it `reply` scrolling to be clearer
- Directly get target from state
- Explicitly scroll to placeholder on reply
- Clean up scrollToItem code a bit
- Account for edge case where index is undefined when scrolling to post

Co-authored-by: Wadim Kalmykov <36057469+w-4@users.noreply.github.com>
2020-10-15 17:46:02 -04:00
flarum-bot
cd05ec6589 Bundled output for commit 78be6e2194 [skip ci] 2020-10-15 21:41:56 +00:00
Wadim Kalmykov
78be6e2194 Fix lifecyle method workarounds (#2378)
Essentially, whenever a route is loaded, we add a key to that component. If the key changes, the page completely rerenders. Switching between different routes handled by the same key triggers those rerenders.
2020-10-15 17:40:25 -04:00
flarum-bot
eb498a0a9f Bundled output for commit ac42a5900d [skip ci] 2020-10-15 21:36:46 +00:00
Wadim Kalmykov
ac42a5900d Make PostStreamScrubber work for Posts that have top margin (#2369)
Also fixes incorrect page count when scrolling to bottom (https://github.com/flarum/core/issues/1897)
2020-10-15 17:35:22 -04:00
Alexander Skvortsov
543b136f7c Refactor PostStream animations (#2364)
- If the fadeIn animation is specified on the PostStream class itself, any time we add/remove another animation, it will redo fadeIn. To avoid this, we move fadeIn into it's own css class, which is applied, and then immediately removed after the animation is completed to ensure it only runs once.
- The "fix" for flashItem was actually broken, as it resulted in 'flash' never being removed, so we never went back to .PostStream's fadeIn. We adjust flashItem to ensure that '.flash' is removed. We also remove 'fadeIn' in case it hasn't yet been removed in oncreate.
2020-10-15 17:34:35 -04:00
flarum-bot
8546fb734f Bundled output for commit 20b9455f04 [skip ci] 2020-10-15 18:32:09 +00:00
Wadim Kalmykov
20b9455f04 make scroll listener passive (#2387)
see: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
2020-10-15 14:30:32 -04:00
Wadim Kalmykov
008f1da539 Make header and navigation components redraw after page components (#2390)
Make header and navigation components redraw after page components. Page components manipulate the header (state), but the header redraws before the page on route change. By changing the mount order, we change also the redraw order.
2020-10-15 14:29:42 -04:00
flarum-bot
a3bd431503 Bundled output for commit 6da81a71a4 [skip ci] 2020-10-14 09:39:52 +00:00
Daniël Klabbers
6da81a71a4 npm audit security vulnerabilities patched 2020-10-14 10:11:34 +02:00
flarum-bot
a48d38614b Bundled output for commit 7358437c59 [skip ci] 2020-10-11 21:27:54 +00:00
Sami Mazouz
7358437c59 Fix MarkRead Mobile Gesture (#2383) 2020-10-11 17:26:32 -04:00
flarum-bot
08ec274a24 Bundled output for commit d45478564f [skip ci] 2020-10-09 23:28:51 +00:00
Alexander Skvortsov
d45478564f Fix handling of non-409 errors in ExtensionsPage
If the error isn't a 409, we'll want to re-throw the error so it'll be handled by the default system (showing an alert).

For simplicity, we can also move 409-handling logic out of setTimeout.

Finally, we adjust the timeout to 300 milliseconds to match the modal transition animation length.
2020-10-09 19:27:07 -04:00
flarum-bot
8296ffe8c9 Bundled output for commit 1d2f0ca407 [skip ci] 2020-10-09 23:07:06 +00:00
Alexander Skvortsov
1d2f0ca407 Header UI fixes (#2371)
* Revert "Fix header contents moving when opening modal (#2131)"
* Fix header contents moving when modal opened/closed.

Conditionally apply the navbar-fixed-top class only when needed, so that we can take advantage of it without always having the navbar in position:fixed, as was done in the previous solution. That resulted in a clash with custom headers.

* Show header on refresh of scrolled page

Due to some magic in Mithril 0.1's context:retain flag, some DOM elements were cached across page reloads. Since that has been eliminated, if we refresh the page and we are scrolled down, the "affix" class which makes the header fixed (and as a result, visible) isn't applied until the first scroll. We fix this by running ScrollListener.update() immediately to set initial navbar state.
2020-10-09 19:05:53 -04:00
Wadim Kalmykov
bb69c3bd57 Reduce modal hide timeout (#2367) 2020-10-09 19:04:53 -04:00
Daniël Klabbers
2b1581875a Fixes the queue for beta 14 (#2363)
- rewrite the queue handling for illuminate 6+
- implement missing maintenance mode callable for queue Worker
- Ensure we resolve append the queue commands once the queue bindings are loaded
- Override WorkCommand because it needs the maintenance flag. It tries to use
the isDownForMaintenance method from the Container assuming it is a Laravel
Application. Circumvented this issue by resolving our Config from IOC instead.
2020-10-09 16:06:28 -04:00
Sami Mazouz
a0c36a015b Use @control-bg for Slidable content (#2381) 2020-10-09 14:37:47 -04:00
flarum-bot
656409794c Bundled output for commit 245f3c6846 [skip ci] 2020-10-07 20:25:22 +00:00
Alexander Skvortsov
245f3c6846 DiscussionPage: call onNewRoute properly
When on a discussion page, the URL changing doesn't always mean we've moved to a different page. In our custom rerender logic, we only want to call `this.onNewRoute()` if the page has actually changed.
2020-10-07 16:22:41 -04:00
Alexander Skvortsov
962b49567c Restore stricter email validation
In v5.8, Laravel expanded email validation logic to closer match the RFC. This, however, allows emails that aren't conventional (for example, emails lacking a TLD). This commit changes Flarum's UserValidator to use the `email:filter` validator, which uses PHP's filter_var, and is the pre-5.8 behavior.

See https://laravel.com/docs/5.8/validation#rule-email
2020-10-07 15:33:57 -04:00
flarum-bot
12498b7620 Bundled output for commit 84f7d29d8c [skip ci] 2020-10-07 18:11:32 +00:00
Alexander Skvortsov
84f7d29d8c Slight PostStream scrubber improvement
After we scroll to a post, we redraw to render post content.  We then update the scrubber again so its height is accurate. This commit moves that update to AFTER our adjustment of scroll position, so that scrubber height is based on actual post heights. This fixes some subtle scrubber glitches.
2020-10-07 14:09:53 -04:00
Daniël Klabbers
561e8c6b6a Add test for model object argument in callable for attribute defaults 2020-10-07 11:38:52 +02:00
Daniël Klabbers
ad9917f0d6 Allows callables for default model attribute to gain access
to the current model in order to calculate the value needed.
2020-10-07 11:26:58 +02:00
Daniël Klabbers
6977c24dd8 improve deprecated message for b15 2020-10-07 10:23:46 +02:00
flarum-bot
d1b72429ac Bundled output for commit 63692f12c5 [skip ci] 2020-10-06 15:53:21 +00:00
Wadim Kalmykov
63692f12c5 SubtreeRetainer: fix onbeforeupdate needsRebuild (#2365) 2020-10-06 11:52:05 -04:00
flarum-bot
441ccec8e7 Bundled output for commit 414b0ff6d3 [skip ci] 2020-10-06 00:52:30 +00:00
Alexander Skvortsov
414b0ff6d3 Update mithril request docs link 2020-10-05 20:50:15 -04:00
flarum-bot
2ff0e1efcb Bundled output for commit 8c46b37a6f [skip ci] 2020-10-05 22:07:23 +00:00
Lucas Henrique
8c46b37a6f Convert icon helper to Typescript (#2360) 2020-10-05 18:06:08 -04:00
flarum-bot
9be629cfcc Bundled output for commit df9be1b063 [skip ci] 2020-10-05 20:26:46 +00:00
Alexander Skvortsov
df9be1b063 Move drawer hide and modal close into onNewRoute
Let's stay consistent with previous behavior, and run these on "internal route change" (same component handles different route) as well as on initial render of a page component.
2020-10-05 16:25:23 -04:00
Alexander Skvortsov
97c36f2f7d Use Symfony TranslatorInterface for tests
This seems to be a leftover change missed in https://github.com/flarum/core/pull/2243
2020-10-05 16:02:12 -04:00
flarum-bot
13efd02085 Bundled output for commit 0b44c48433 [skip ci] 2020-10-05 18:41:55 +00:00
Alexander Skvortsov
0b44c48433 Catch promise reject when not logged in on reply 2020-10-05 14:40:33 -04:00
flarum-bot
b562072471 Bundled output for commit 718445cb0c [skip ci] 2020-10-05 14:56:45 +00:00
Wadim Kalmykov
718445cb0c call parent onremove (#2362) 2020-10-05 10:55:14 -04:00
120 changed files with 2867 additions and 605 deletions

View File

@@ -1,5 +1,102 @@
# 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

View File

@@ -10,7 +10,7 @@
"email": "franz@develophp.org"
},
{
"name": "Daniel Klabbers",
"name": "Daniël Klabbers",
"email": "daniel@klabbers.email",
"homepage": "https://luceos.com"
},
@@ -27,6 +27,10 @@
{
"name": "Matthew Kilgore",
"email": "matthew@kilgore.dev"
},
{
"name": "Alexander (Sasha) Skvortsov",
"email": "askvortsov@flarum.org"
}
],
"support": {
@@ -72,11 +76,11 @@
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"s9e/text-formatter": "^2.3.6",
"symfony/config": "^3.3",
"symfony/console": "^4.2",
"symfony/event-dispatcher": "^4.3.2",
"symfony/translation": "^3.3",
"symfony/yaml": "^3.3",
"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",
"tobscure/json-api": "^0.3.0",
"wikimedia/less.php": "^3.0"
},

14
js/dist/admin.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

16
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

27
js/package-lock.json generated
View File

@@ -3556,9 +3556,9 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
},
"jquery": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
"integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
},
"jquery.hotkeys": {
"version": "0.1.0",
@@ -4546,11 +4546,6 @@
"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",
@@ -4900,21 +4895,29 @@
}
},
"terser-webpack-plugin": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
"integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz",
"integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==",
"requires": {
"cacache": "^12.0.2",
"find-cache-dir": "^2.1.0",
"is-wsl": "^1.1.0",
"schema-utils": "^1.0.0",
"serialize-javascript": "^2.1.2",
"serialize-javascript": "^4.0.0",
"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",

View File

@@ -10,7 +10,7 @@
"dayjs": "^1.8.28",
"expose-loader": "^0.7.5",
"flarum-webpack-config": "0.1.0-beta.10",
"jquery": "^3.4.1",
"jquery": "^3.5.1",
"jquery.hotkeys": "^0.1.0",
"lodash-es": "^4.17.14",
"m.attrs.bidi": "github:tobscure/m.attrs.bidi",

View File

@@ -27,20 +27,19 @@ export default class AdminApplication extends Application {
* @inheritdoc
*/
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();
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);
// If an extension has just been enabled, then we will run its settings
// callback.
const enabled = localStorage.getItem('enabledExtension');

View File

@@ -139,16 +139,20 @@ export default class ExtensionsPage extends Page {
// 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.
const error = JSON.parse(e.responseText).errors[0];
if (e.status !== 409) {
throw e;
}
app.alerts.show(
{ type: 'error' },
app.translator.trans(`core.lib.error.${error.code}_message`, {
extension: error.extension,
extensions: error.extensions.join(', '),
})
);
}, 250);
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(', '),
})
);
}
}

View File

@@ -198,13 +198,19 @@ 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.
new ScrollListener((top) => {
// 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) => {
const $app = $('#app');
const offset = $app.offset().top;
$app.toggleClass('affix', top >= offset).toggleClass('scrolled', top > offset);
}).start();
$('.App-header').toggleClass('navbar-fixed-top', top >= offset);
});
scrollListener.start();
scrollListener.update();
$(() => {
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
@@ -272,7 +278,7 @@ export default class Application {
/**
* Make an AJAX request, handling any low-level errors that may occur.
*
* @see https://lhorie.github.io/mithril/mithril.request.html
* @see https://mithril.js.org/request.html
* @param {Object} options
* @return {Promise}
* @public

View File

@@ -83,7 +83,7 @@ export default class Store {
*/
find(type, id, query = {}, options = {}) {
let params = query;
let url = app.forum.attribute('apiUrl') + '/' + type;
let url = app.forum.attribute('apiUrl') + (query.search ? '/search/' : '/') + type;
if (id instanceof Array) {
url += '?filter[id]=' + id.join(',');

View File

@@ -19,6 +19,7 @@ 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';
@@ -67,6 +68,7 @@ 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,6 +91,7 @@ export default {
'utils/stringToColor': stringToColor,
'utils/Stream': Stream,
'utils/subclassOf': subclassOf,
'utils/SuperTextarea': SuperTextarea,
'utils/setRouteWithForcedRefresh': setRouteWithForcedRefresh,
'utils/patchMithril': patchMithril,
'utils/classList': classList,
@@ -138,4 +141,5 @@ export default {
'helpers/username': username,
'helpers/userOnline': userOnline,
'helpers/listItems': listItems,
'resolvers/DefaultResolver': DefaultResolver,
};

View File

@@ -12,12 +12,14 @@ 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) {

View File

@@ -35,7 +35,8 @@ export default class Modal extends Component {
this.attrs.animateHide();
// Here, we ensure that the animation has time to complete.
// See https://mithril.js.org/lifecycle-methods.html#onbeforeremove
return new Promise((resolve) => setTimeout(resolve, 1000));
// Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
return new Promise((resolve) => setTimeout(resolve, 300));
}
}

View File

@@ -10,7 +10,8 @@ export default class Page extends Component {
oninit(vnode) {
super.oninit(vnode);
this.onNewRoute();
app.previous = app.current;
app.current = new PageState(this.constructor, { routeName: this.attrs.routeName });
app.drawer.hide();
app.modal.close();
@@ -21,17 +22,13 @@ export default class Page extends Component {
* @type {String}
*/
this.bodyClass = '';
}
/**
* 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.previous = app.current;
app.current = new PageState(this.constructor, { routeName: this.attrs.routeName });
/**
* Whether we should scroll to the top of the page when its rendered.
*
* @type {Boolean}
*/
this.scrollTopOnCreate = true;
}
oncreate(vnode) {
@@ -40,6 +37,10 @@ export default class Page extends Component {
if (this.bodyClass) {
$('#app').addClass(this.bodyClass);
}
if (this.scrollTopOnCreate) {
$(window).scrollTop(0);
}
}
onremove() {

View File

@@ -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) {
export default function fullTime(time: Date): Mithril.Vnode {
const d = dayjs(time);
const datetime = d.format();

View File

@@ -1,14 +1,13 @@
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) {
export default function humanTime(time: Date): Mithril.Vnode {
const d = dayjs(time);
const datetime = d.format();

View File

@@ -1,12 +0,0 @@
/**
* The `icon` helper displays an icon.
*
* @param {String} fontClass The full icon class, prefix and the icons 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} />;
}

View File

@@ -0,0 +1,13 @@
import * as Mithril from 'mithril';
/**
* The `icon` helper displays an icon.
*
* @param fontClass The full icon class, prefix and the icons 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} />;
}

View File

@@ -0,0 +1,41 @@
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() }];
}
}

View File

@@ -58,7 +58,7 @@ export default class ScrollListener {
*/
start() {
if (!this.active) {
window.addEventListener('scroll', (this.active = this.loop.bind(this)));
window.addEventListener('scroll', (this.active = this.loop.bind(this)), { passive: true });
}
}

View File

@@ -28,6 +28,9 @@ 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();
}
/**
@@ -60,6 +63,8 @@ export default class SubtreeRetainer {
*/
check(...callbacks) {
this.callbacks = this.callbacks.concat(callbacks);
// Update the data cache when new checks are added.
this.needsRebuild();
}
/**

View File

@@ -1,3 +1,6 @@
import dayjs from 'dayjs';
import 'dayjs/plugin/relativeTime';
/**
* The `humanTime` utility converts a date to a localized, human-readable time-
* ago string.

View File

@@ -1,6 +1,9 @@
import DefaultResolver from '../resolvers/DefaultResolver';
/**
* The `mapRoutes` utility converts a map of named application routes into a
* format that can be understood by Mithril.
* format that can be understood by Mithril, and wraps them in route resolvers
* to provide each route with the current route name.
*
* @see https://mithril.js.org/route.html#signature
* @param {Object} routes
@@ -10,14 +13,17 @@
export default function mapRoutes(routes, basePath = '') {
const map = {};
for (const key in routes) {
const route = routes[key];
for (const routeName in routes) {
const route = routes[routeName];
map[basePath + route.path] = {
render() {
return m(route.component, { routeName: 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}]`);
}
}
return map;

View File

@@ -115,17 +115,19 @@ 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

View File

@@ -71,6 +71,7 @@ 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';
@@ -146,6 +147,7 @@ export default Object.assign(compat, {
'components/DiscussionListItem': DiscussionListItem,
'components/LoadingPost': LoadingPost,
'components/PostsUserPage': PostsUserPage,
'resolvers/DiscussionPageResolver': DiscussionPageResolver,
routes: routes,
ForumApplication: ForumApplication,
});

View File

@@ -56,9 +56,7 @@ export default class CommentPost extends Post {
]);
}
onupdate(vnode) {
super.onupdate();
refreshContent() {
const contentHtml = this.isEditing() ? '' : this.attrs.post.contentHtml();
// If the post content has changed since the last render, we'll run through
@@ -66,13 +64,28 @@ 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 () {
eval.call(window, $(this).text());
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);
});
}
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 });
}

View File

@@ -91,12 +91,12 @@ export default class DiscussionListItem extends Component {
)
: ''}
<a
<span
className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')}
onclick={this.markAsRead.bind(this)}
>
{icon('fas fa-check')}
</a>
</span>
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '') + (isRead ? ' read' : '')}>
<Link

View File

@@ -47,11 +47,10 @@ 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
@@ -83,7 +82,6 @@ export default class DiscussionPage extends Page {
{PostStream.component({
discussion,
stream: this.stream,
targetPost: this.stream.targetPost,
onPositionChange: this.positionChanged.bind(this),
})}
</div>
@@ -95,34 +93,6 @@ export default class DiscussionPage extends Page {
);
}
onbeforeupdate(vnode) {
super.onbeforeupdate(vnode);
if (m.route.get() !== this.prevRoute) {
this.onNewRoute();
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.
*/
@@ -185,7 +155,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.id() - b.id())
.sort((a, b) => a.createdAt() - b.createdAt())
.slice(0, 20);
}
@@ -247,10 +217,7 @@ 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

View File

@@ -24,7 +24,7 @@ export default class DiscussionsSearchSource {
include: 'mostRelevantPost',
};
return app.store.find('discussions', params).then((results) => (this.results[query] = results));
return app.store.find('discussions', params, { search: query }).then((results) => (this.results[query] = results));
}
view(query) {

View File

@@ -42,26 +42,7 @@ 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.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();
}
this.scrollTopOnCreate = false;
}
view() {
@@ -105,18 +86,22 @@ export default class IndexPage extends Page {
$('#app').css('min-height', $(window).height() + heroHeight);
// 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);
// 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);
}
// 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.$(`.DiscussionListItem[data-id="${this.lastDiscussion.id()}"]`);
const $discussion = this.$(`li[data-id="${this.lastDiscussion.id()}"] .DiscussionListItem`);
if ($discussion.length) {
const indexTop = $('#header').outerHeight();
@@ -131,14 +116,16 @@ 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();
}
/**

View File

@@ -24,7 +24,7 @@ export default class Notification extends Component {
<Link
className={'Notification Notification--' + notification.contentType() + ' ' + (!notification.isRead() ? 'unread' : '')}
href={href}
external={href.indexOf('://') === -1}
external={href.includes('://')}
onclick={this.markAsRead.bind(this)}
>
{!notification.isRead() &&

View File

@@ -32,6 +32,13 @@ export default class PostStream extends Component {
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 };
@@ -42,6 +49,7 @@ export default class PostStream extends Component {
content = PostComponent ? PostComponent.component({ post }) : '';
attrs.key = 'post' + post.id();
attrs.oncreate = postFadeIn;
attrs['data-time'] = time.toISOString();
attrs['data-number'] = post.number();
attrs['data-id'] = post.id();
@@ -89,7 +97,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">
<div className="PostStream-item" key="reply" data-index={this.stream.count()} oncreate={postFadeIn}>
{ReplyPlaceholder.component({ discussion: this.discussion })}
</div>
);
@@ -121,16 +129,15 @@ export default class PostStream extends Component {
* Start scrolling, if appropriate, to a newly-targeted post.
*/
triggerScroll() {
if (!this.attrs.targetPost || !this.stream.needsScroll) return;
if (!this.stream.needsScroll) return;
const newTarget = this.attrs.targetPost;
const target = this.stream.targetPost;
this.stream.needsScroll = false;
if ('number' in newTarget) {
this.scrollToNumber(newTarget.number, this.stream.animateScroll);
} else if ('index' in newTarget) {
const backwards = newTarget.index === this.stream.count() - 1;
this.scrollToIndex(newTarget.index, this.stream.animateScroll, backwards);
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);
}
}
@@ -181,9 +188,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
@@ -209,8 +216,10 @@ export default class PostStream extends Component {
const visibleBottom = Math.min(height, viewportTop + viewportHeight - top);
const visiblePost = visibleBottom - visibleTop;
if (top <= viewportTop) {
index = parseFloat($this.data('index')) + visibleTop / height;
// 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 (visiblePost > 0) {
@@ -223,7 +232,10 @@ export default class PostStream extends Component {
if (time) period = time;
});
this.stream.index = index + 1;
// 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.visible = visible;
if (period) this.stream.description = dayjs(period).format('MMMM YYYY');
}
@@ -296,18 +308,17 @@ export default class PostStream extends Component {
*
* @param {Integer} index
* @param {Boolean} animate
* @param {Boolean} bottom Whether or not to scroll to the bottom of the post
* at the given index, instead of the top of it.
* @param {Boolean} reply Whether or not to scroll to the reply placeholder.
* @return {jQuery.Deferred}
*/
scrollToIndex(index, animate, bottom) {
const $item = this.$(`.PostStream-item[data-index=${index}]`);
scrollToIndex(index, animate, reply) {
const $item = reply ? $('.PostStream-item:last-child') : this.$(`.PostStream-item[data-index=${index}]`);
return this.scrollToItem($item, animate, true, bottom).then(() => {
if (index == this.stream.count() - 1) {
this.flashItem(this.$('.PostStream-item:last-child'));
}
});
this.scrollToItem($item, animate, true, reply);
if (reply) {
this.flashItem($item);
}
}
/**
@@ -317,11 +328,10 @@ 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} bottom Whether or not to scroll to the bottom of the post
* at the given index, instead of the top of it.
* @param {Boolean} reply Whether or not to scroll to the reply placeholder.
* @return {jQuery.Deferred}
*/
scrollToItem($item, animate, force, bottom) {
scrollToItem($item, animate, force, reply) {
const $container = $('html, body').stop(true);
const index = $item.data('index');
@@ -332,10 +342,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 bottom of an item, then we'll make sure the
// If we're scrolling to the reply placeholder, we'll make sure its
// bottom will line up with the top of the composer.
if (force || itemTop < scrollTop || itemBottom > scrollBottom) {
const top = bottom ? itemBottom - $(window).height() + app.composer.computedHeight() : $item.is(':first-child') ? 0 : itemTop;
const top = reply ? itemBottom - $(window).height() + app.composer.computedHeight() : $item.is(':first-child') ? 0 : itemTop;
if (!animate) {
$container.scrollTop(top);
@@ -349,7 +359,7 @@ export default class PostStream extends Component {
// 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();
this.stream.index = index;
if (index !== undefined) this.stream.index = index + 1;
};
// If we don't update this before the scroll, the scrubber will start
@@ -359,24 +369,29 @@ export default class PostStream extends Component {
return Promise.all([$container.promise(), this.stream.loadPromise]).then(() => {
m.redraw.sync();
// We want to adjust this again after posts have been loaded in so that
// the height of the scrubber is accurate.
updateScrubberHeight();
// After post data has been loaded in, we will attempt to scroll back
// to the top of the requested post (or to the top of the page if the
// first post was requested). In some cases, we may have scrolled to
// the end of the available post range, in which case, the next range
// of posts will be loaded in. However, in those cases, the post we
// requested won't exist, so scrolling to it would cause an error.
// Accordingly, we start by checking that it's offset is defined.
const offset = $(`.PostStream-item[data-index=${index}]`).offset();
if (index === 0) {
// 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 (offset) {
$(window).scrollTop($(`.PostStream-item[data-index=${index}]`).offset().top - this.getMarginTop());
} 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();
this.calculatePosition();
this.stream.paused = false;
});
@@ -388,10 +403,11 @@ 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) => {
if (e.animationName === 'fadeIn') {
$item.removeClass('flash');
}
$item.removeClass('flash');
});
}
}

View File

@@ -33,7 +33,7 @@ export default class ReplyPlaceholder extends Component {
}
const reply = () => {
DiscussionControls.replyAction.call(this.attrs.discussion, true);
DiscussionControls.replyAction.call(this.attrs.discussion, true).catch(() => {});
};
return (

View File

@@ -27,19 +27,6 @@ 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.onNewRoute();
this.prevUsername = currUsername;
this.loadUser(currUsername);
}
}
view() {
@@ -148,7 +135,7 @@ export default class UserPage extends Page {
items.add(
'posts',
<LinkButton href={app.route('user.posts', { username: user.username() })} force icon="far fa-comment">
<LinkButton href={app.route('user.posts', { username: user.username() })} icon="far fa-comment">
{app.translator.trans('core.forum.user.posts_link')} <span className="Button-badge">{user.commentCount()}</span>
</LinkButton>,
100
@@ -156,7 +143,7 @@ export default class UserPage extends Page {
items.add(
'discussions',
<LinkButton href={app.route('user.discussions', { username: user.username() })} force icon="fas fa-bars">
<LinkButton href={app.route('user.discussions', { username: user.username() })} icon="fas fa-bars">
{app.translator.trans('core.forum.user.discussions_link')} <span className="Button-badge">{user.discussionCount()}</span>
</LinkButton>,
90

View File

@@ -16,10 +16,14 @@ export default class UsersSearchResults {
search(query) {
return app.store
.find('users', {
filter: { q: query },
page: { limit: 5 },
})
.find(
'users',
{
filter: { q: query },
page: { limit: 5 },
},
{ search: query }
)
.then((results) => {
this.results[query] = results;
m.redraw();

View File

@@ -0,0 +1,49 @@
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);
}
}

View File

@@ -4,6 +4,7 @@ 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.
@@ -14,8 +15,8 @@ export default function (app) {
app.routes = {
index: { path: '/all', component: IndexPage },
discussion: { path: '/d/:id', component: DiscussionPage },
'discussion.near': { path: '/d/:id/:near', component: DiscussionPage },
discussion: { path: '/d/:id', component: DiscussionPage, resolverClass: DiscussionPageResolver },
'discussion.near': { path: '/d/:id/:near', component: DiscussionPage, resolverClass: DiscussionPageResolver },
user: { path: '/u/:username', component: PostsUserPage },
'user.posts': { path: '/u/:username', component: PostsUserPage },

View File

@@ -119,7 +119,7 @@ export default class DiscussionListState {
params.page = { offset };
params.include = params.include.join(',');
return this.app.store.find('discussions', params);
return this.app.store.find('discussions', params, { search: params.filter.q });
}
/**

View File

@@ -96,7 +96,9 @@ 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') {
return this.goToLast();
const resultPromise = this.goToLast();
this.targetPost.reply = true;
return resultPromise;
}
this.paused = true;
@@ -280,7 +282,13 @@ class PostStreamState {
}
});
return loadIds.length ? app.store.find('posts', loadIds) : Promise.resolve(loaded);
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);
}
/**

View File

@@ -236,12 +236,16 @@
.App-header {
padding: 8px;
height: @header-height;
position: fixed;
position: absolute;
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);
}

View File

@@ -105,6 +105,10 @@
text-align: left;
}
}
.off.Checkbox--switch .Checkbox-display {
background: @muted-more-color;
}
}
.Modal-footer {
border: 0;

View File

@@ -6,18 +6,7 @@
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.4s ease-in-out);
&:not(:last-child) {
border-bottom: 1px solid @control-bg;
@@ -104,3 +93,16 @@
.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);
}

View File

@@ -44,7 +44,7 @@
.sliding& {
position: relative;
background: #fff;
background: @control-bg;
z-index: 2;
border-radius: 2px;
.box-shadow(0 2px 6px @shadow-color);

View File

@@ -54,9 +54,10 @@ class AdminServiceProvider extends AbstractServiceProvider
HttpMiddleware\StartSession::class,
HttpMiddleware\RememberFromCookie::class,
HttpMiddleware\AuthenticateWithSession::class,
HttpMiddleware\CheckCsrfToken::class,
HttpMiddleware\SetLocale::class,
Middleware\RequireAdministrateAbility::class,
'flarum.admin.route_resolver',
HttpMiddleware\CheckCsrfToken::class,
Middleware\RequireAdministrateAbility::class
];
});
@@ -68,6 +69,10 @@ class AdminServiceProvider extends AbstractServiceProvider
);
});
$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;
@@ -75,7 +80,7 @@ class AdminServiceProvider extends AbstractServiceProvider
$pipe->pipe($this->app->make($middleware));
}
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.admin.routes')));
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
return $pipe;
});

View File

@@ -51,8 +51,9 @@ 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
];
});
@@ -64,6 +65,10 @@ class ApiServiceProvider extends AbstractServiceProvider
);
});
$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;
@@ -71,10 +76,16 @@ class ApiServiceProvider extends AbstractServiceProvider
$pipe->pipe($this->app->make($middleware));
}
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.api.routes')));
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
return $pipe;
});
$this->app->singleton('flarum.api.notification_serializers', function () {
return [
'discussionRenamed' => BasicDiscussionSerializer::class
];
});
}
/**
@@ -82,7 +93,7 @@ class ApiServiceProvider extends AbstractServiceProvider
*/
public function boot()
{
$this->registerNotificationSerializers();
$this->setNotificationSerializers();
AbstractSerializeController::setContainer($this->app);
AbstractSerializeController::setEventDispatcher($events = $this->app->make('events'));
@@ -94,13 +105,12 @@ class ApiServiceProvider extends AbstractServiceProvider
/**
* Register notification serializers.
*/
protected function registerNotificationSerializers()
protected function setNotificationSerializers()
{
$blueprints = [];
$serializers = [
'discussionRenamed' => BasicDiscussionSerializer::class
];
$serializers = $this->app->make('flarum.api.notification_serializers');
// Deprecated in beta 15, remove in beta 16
$this->app->make('events')->dispatch(
new ConfigureNotificationTypes($blueprints, $serializers)
);

View File

@@ -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,9 +49,14 @@ class ListDiscussionsController extends AbstractListController
public $sortFields = ['lastPostedAt', 'commentCount', 'createdAt'];
/**
* @var DiscussionSearcher
* @var DiscussionRepository
*/
protected $searcher;
protected $discussions;
/**
* @var Filterer
*/
protected $filterer;
/**
* @var UrlGenerator
@@ -62,9 +67,10 @@ class ListDiscussionsController extends AbstractListController
* @param DiscussionSearcher $searcher
* @param UrlGenerator $url
*/
public function __construct(DiscussionSearcher $searcher, UrlGenerator $url)
public function __construct(DiscussionRepository $discussions, Filterer $filterer, UrlGenerator $url)
{
$this->searcher = $searcher;
$this->discussions = $discussions;
$this->filterer = $filterer;
$this->url = $url;
}
@@ -74,16 +80,16 @@ class ListDiscussionsController extends AbstractListController
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);
$filters = $this->extractFilter($request);
$sort = $this->extractSort($request);
$query = $this->discussions->query();
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = array_merge($this->extractInclude($request), ['state']);
$results = $this->searcher->search($criteria, $limit, $offset);
$results = $this->filterer->filter($actor, $query, $filters, $sort, $limit, $offset, $load);
$document->addPaginationLinks(
$this->url->to('api')->route('discussions.index'),

View File

@@ -10,10 +10,9 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Filter\Filterer;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Flarum\User\Search\UserSearcher;
use Illuminate\Support\Arr;
use Flarum\User\UserRepository;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -41,9 +40,9 @@ class ListUsersController extends AbstractListController
];
/**
* @var UserSearcher
* @var Filterer
*/
protected $searcher;
protected $filterer;
/**
* @var UrlGenerator
@@ -51,13 +50,20 @@ class ListUsersController extends AbstractListController
protected $url;
/**
* @param UserSearcher $searcher
* @param UrlGenerator $url
* @var UserRepository
*/
public function __construct(UserSearcher $searcher, UrlGenerator $url)
protected $users;
/**
* @param Filterer $filterer
* @param UrlGenerator $url
* @param UserRepository $users
*/
public function __construct(Filterer $filterer, UrlGenerator $url, UserRepository $users)
{
$this->searcher = $searcher;
$this->filterer = $filterer;
$this->url = $url;
$this->users = $users;
}
/**
@@ -69,16 +75,16 @@ class ListUsersController extends AbstractListController
$actor->assertCan('viewUserList');
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$query = $this->users->query();
$criteria = new SearchCriteria($actor, $query, $sort);
$filters = $this->extractFilter($request);
$sort = $this->extractSort($request);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = $this->extractInclude($request);
$results = $this->searcher->search($criteria, $limit, $offset, $load);
$results = $this->filterer->filter($actor, $query, $filters, $sort, $limit, $offset, $load);
$document->addPaginationLinks(
$this->url->to('api')->route('users.index'),

View File

@@ -0,0 +1,112 @@
<?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;
}
}

View File

@@ -0,0 +1,93 @@
<?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();
}
}

View File

@@ -95,6 +95,13 @@ 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
@@ -163,6 +170,13 @@ 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

View File

@@ -82,7 +82,7 @@ abstract class AbstractModel extends Eloquent
}
$this->attributes = array_map(function ($item) {
return is_callable($item) ? $item() : $item;
return is_callable($item) ? $item($this) : $item;
}, $this->attributes);
parent::__construct($attributes);

View File

@@ -13,6 +13,9 @@ use Flarum\Notification\Blueprint\BlueprintInterface;
use InvalidArgumentException;
use ReflectionClass;
/**
* @deprecated in beta 15, removed in beta 16
*/
class ConfigureNotificationTypes
{
/**

View File

@@ -9,6 +9,9 @@
namespace Flarum\Event;
/**
* @deprecated in beta 15, remove in beta 16. Use the Post extender instead.
*/
class ConfigurePostTypes
{
private $models;

View File

@@ -14,11 +14,28 @@ use Illuminate\Contracts\Container\Container;
class Csrf implements ExtenderInterface
{
protected $csrfExemptPaths = [];
protected $csrfExemptRoutes = [];
/**
* 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->csrfExemptPaths[] = $path;
$this->csrfExemptRoutes[] = $path;
return $this;
}
@@ -26,7 +43,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->csrfExemptPaths);
return array_merge($existingExemptPaths, $this->csrfExemptRoutes);
});
}
}

View File

@@ -25,7 +25,7 @@ class Event implements ExtenderInterface
* - the class attribute of a class with a public `handle` method, which accepts an instance of the event as a parameter
*
* @param string $event
* @param callable $listener
* @param callable|string $listener
*/
public function listen(string $event, $listener)
{

63
src/Extend/Filter.php Normal file
View File

@@ -0,0 +1,63 @@
<?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\Extend;
use Flarum\Extension\Extension;
use Flarum\Filter\Filterer;
use Flarum\Foundation\ContainerUtil;
use Illuminate\Contracts\Container\Container;
class Filter implements ExtenderInterface
{
private $resource;
private $filters = [];
private $filterMutators = [];
/**
* @param string $resource: The ::class attribute of the resource this applies to, which is typically an Eloquent model.
*/
public function __construct($resource)
{
$this->resource = $resource;
}
/**
* Add a filter to run when the resource is filtered.
*
* @param string $filterClass: The ::class attribute of the filter you are adding.
*/
public function addFilter(string $filterClass)
{
$this->filters[] = $filterClass;
return $this;
}
/**
* Add a callback through which to run all filter queries after filters have been applied.
*/
public function addFilterMutator($callback)
{
$this->filterMutators[] = $callback;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
foreach ($this->filters as $filter) {
Filterer::addFilter($this->resource, $container->make($filter));
}
foreach ($this->filterMutators as $mutator) {
Filterer::addFilterMutator($this->resource, ContainerUtil::wrapCallback($mutator, $container));
}
}
}

View File

@@ -10,38 +10,93 @@
namespace Flarum\Extend;
use Flarum\Extension\Extension;
use Flarum\Formatter\Event\Configuring;
use Flarum\Formatter\Formatter as ActualFormatter;
use Flarum\Foundation\ContainerUtil;
use Illuminate\Contracts\Container\Container;
use Illuminate\Events\Dispatcher;
class Formatter implements ExtenderInterface, LifecycleInterface
{
private $callback;
private $configurationCallbacks = [];
private $parsingCallbacks = [];
private $renderingCallbacks = [];
/**
* Configure the formatter. This can be used to add support for custom markdown/bbcode/etc tags,
* or otherwise change the formatter. Please see documentation for the s9e text formatter library for more
* information on how to use this.
*
* @param callable|string $callback
*
* The callback can be a closure or invokable class, and should accept:
* - \s9e\TextFormatter\Configurator $configurator
*/
public function configure($callback)
{
$this->callback = $callback;
$this->configurationCallbacks[] = $callback;
return $this;
}
/**
* Prepare the system for parsing. This can be used to modify the text that will be parsed, or to modify the parser.
* Please note that the text to be parsed must be returned, regardless of whether it's changed.
*
* @param callable|string $callback
*
* The callback can be a closure or invokable class, and should accept:
* - \s9e\TextFormatter\Parser $parser
* - mixed $context
* - string $text: The text to be parsed.
*
* The callback should return:
* - string $text: The text to be parsed.
*/
public function parse($callback)
{
$this->parsingCallbacks[] = $callback;
return $this;
}
/**
* Prepare the system for rendering. This can be used to modify the xml that will be rendered, or to modify the renderer.
* Please note that the xml to be rendered must be returned, regardless of whether it's changed.
*
* @param callable|string $callback
*
* The callback can be a closure or invokable class, and should accept:
* - \s9e\TextFormatter\Rendered $renderer
* - mixed $context
* - string $xml: The xml to be rendered.
* - ServerRequestInterface $request
*
* The callback should return:
* - string $xml: The xml to be rendered.
*/
public function render($callback)
{
$this->renderingCallbacks[] = $callback;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
$events = $container->make(Dispatcher::class);
$events->listen(
Configuring::class,
function (Configuring $event) use ($container) {
if (is_string($this->callback)) {
$callback = $container->make($this->callback);
} else {
$callback = $this->callback;
}
$callback($event->configurator);
$container->extend('flarum.formatter', function ($formatter, $container) {
foreach ($this->configurationCallbacks as $callback) {
$formatter->addConfigurationCallback(ContainerUtil::wrapCallback($callback, $container));
}
);
foreach ($this->parsingCallbacks as $callback) {
$formatter->addParsingCallback(ContainerUtil::wrapCallback($callback, $container));
}
foreach ($this->renderingCallbacks as $callback) {
$formatter->addRenderingCallback(ContainerUtil::wrapCallback($callback, $container));
}
return $formatter;
});
}
public function onEnable(Container $container, Extension $extension)

View File

@@ -12,6 +12,7 @@ namespace Flarum\Extend;
use Flarum\Extension\Event\Disabled;
use Flarum\Extension\Event\Enabled;
use Flarum\Extension\Extension;
use Flarum\Foundation\ContainerUtil;
use Flarum\Foundation\Event\ClearingCache;
use Flarum\Frontend\Assets;
use Flarum\Frontend\Compiler\Source\SourceCollector;
@@ -171,11 +172,7 @@ class Frontend implements ExtenderInterface
"flarum.frontend.$this->frontend",
function (ActualFrontend $frontend, Container $container) {
foreach ($this->content as $content) {
if (is_string($content)) {
$content = $container->make($content);
}
$frontend->content($content);
$frontend->content(ContainerUtil::wrapCallback($content, $container));
}
}
);

View File

@@ -11,12 +11,14 @@ namespace Flarum\Extend;
use Flarum\Database\AbstractModel;
use Flarum\Extension\Extension;
use Flarum\Foundation\ContainerUtil;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Arr;
class Model implements ExtenderInterface
{
private $modelClass;
private $customRelations = [];
/**
* @param string $modelClass The ::class attribute of the model you are modifying.
@@ -48,7 +50,9 @@ class Model implements ExtenderInterface
}
/**
* Add a default value for a given attribute, which can be an explicit value, or a closure.
* Add a default value for a given attribute, which can be an explicit value, a closure,
* or an instance of an invokable class. Unlike with some other extenders,
* it CANNOT be the `::class` attribute of an invokable class.
*
* @param string $attribute
* @param mixed $value
@@ -157,7 +161,7 @@ class Model implements ExtenderInterface
* @param string $name: The name of the relation. This doesn't have to be anything in particular,
* but has to be unique from other relation names for this model, and should
* work as the name of a method.
* @param callable $callable
* @param callable|string $callback
*
* The callable can be a closure or invokable class, and should accept:
* - $instance: An instance of this model.
@@ -168,15 +172,17 @@ class Model implements ExtenderInterface
*
* @return self
*/
public function relationship(string $name, callable $callable)
public function relationship(string $name, $callback)
{
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", $callable);
$this->customRelations[$name] = $callback;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
// Nothing needed here.
foreach ($this->customRelations as $name => $callback) {
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", ContainerUtil::wrapCallback($callback, $container));
}
}
}

View File

@@ -0,0 +1,78 @@
<?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\Extend;
use Flarum\Extension\Extension;
use Illuminate\Contracts\Container\Container;
class Notification implements ExtenderInterface
{
private $blueprints = [];
private $serializers = [];
private $drivers = [];
private $typesEnabledByDefault = [];
/**
* @param string $blueprint The ::class attribute of the blueprint class.
* This blueprint should implement \Flarum\Notification\Blueprint\BlueprintInterface.
* @param string $serializer The ::class attribute of the serializer class.
* This serializer should extend from \Flarum\Api\Serializer\AbstractSerializer.
* @param string[] $driversEnabledByDefault The names of the drivers enabled by default for this notification type.
* (example: alert, email).
* @return self
*/
public function type(string $blueprint, string $serializer, array $driversEnabledByDefault = [])
{
$this->blueprints[$blueprint] = $driversEnabledByDefault;
$this->serializers[$blueprint::getType()] = $serializer;
return $this;
}
/**
* @param string $driverName The name of the notification driver.
* @param string $driver The ::class attribute of the driver class.
* This driver should implement \Flarum\Notification\Driver\NotificationDriverInterface.
* @param string[] $typesEnabledByDefault The names of blueprint classes of types enabled by default for this driver.
* @return self
*/
public function driver(string $driverName, string $driver, array $typesEnabledByDefault = [])
{
$this->drivers[$driverName] = $driver;
$this->typesEnabledByDefault[$driverName] = $typesEnabledByDefault;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
$container->extend('flarum.notification.blueprints', function ($existingBlueprints) {
$existingBlueprints = array_merge($existingBlueprints, $this->blueprints);
foreach ($this->typesEnabledByDefault as $driverName => $typesEnabledByDefault) {
foreach ($typesEnabledByDefault as $blueprintClass) {
if (isset($existingBlueprints[$blueprintClass]) && (! in_array($driverName, $existingBlueprints[$blueprintClass]))) {
$existingBlueprints[$blueprintClass][] = $driverName;
}
}
}
return $existingBlueprints;
});
$container->extend('flarum.api.notification_serializers', function ($existingSerializers) {
return array_merge($existingSerializers, $this->serializers);
});
$container->extend('flarum.notification.drivers', function ($existingDrivers) {
return array_merge($existingDrivers, $this->drivers);
});
}
}

39
src/Extend/Post.php Normal file
View File

@@ -0,0 +1,39 @@
<?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\Extend;
use Flarum\Extension\Extension;
use Flarum\Post\Post as PostModel;
use Illuminate\Contracts\Container\Container;
class Post implements ExtenderInterface
{
private $postTypes = [];
/**
* Register a new post type. This is generally done for custom 'event posts',
* such as those that appear when a discussion is renamed.
*
* @param string $postType: The ::class attribute of the custom Post type that is being added.
*/
public function type(string $postType)
{
$this->postTypes[] = $postType;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
foreach ($this->postTypes as $postType) {
PostModel::setModel($postType::$type, $postType);
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Extend;
use Flarum\Extension\Extension;
use Illuminate\Contracts\Container\Container;
class ServiceProvider implements ExtenderInterface
{
private $providers = [];
/**
* Register a service provider.
*
* @param string $serviceProviderClass The ::class attribute of the service provider class.
* @return self
*/
public function register(string $serviceProviderClass)
{
$this->providers[] = $serviceProviderClass;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
$app = $container->make('flarum');
foreach ($this->providers as $provider) {
$app->register($provider);
}
}
}

View File

@@ -35,7 +35,7 @@ class User implements ExtenderInterface
* This can be used to give a user permissions for groups they aren't actually in, based on context.
* It will not change the group badges displayed for the user.
*
* @param callable $callable
* @param callable|string $callback
*
* The callable can be a closure or invokable class, and should accept:
* - \Flarum\User\User $user: the user in question.
@@ -44,9 +44,9 @@ class User implements ExtenderInterface
* The callable should return:
* - array $groupIds: an array of ids for the groups the user belongs to.
*/
public function permissionGroups(callable $callable)
public function permissionGroups($callback)
{
$this->groupProcessors[] = $callable;
$this->groupProcessors[] = $callback;
return $this;
}

55
src/Extend/Validator.php Normal file
View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Extend;
use Flarum\Extension\Extension;
use Flarum\Foundation\ContainerUtil;
use Illuminate\Contracts\Container\Container;
class Validator implements ExtenderInterface
{
private $configurationCallbacks = [];
private $validator;
/**
* @param string $validatorClass: The ::class attribute of the validator you are modifying.
* The validator should inherit from \Flarum\Foundation\AbstractValidator.
*/
public function __construct($validatorClass)
{
$this->validator = $validatorClass;
}
/**
* Configure the validator. This is often used to adjust validation rules, but can be
* used to make other changes to the validator as well.
*
* @param callable $callable
*
* The callable can be a closure or invokable class, and should accept:
* - \Flarum\Foundation\AbstractValidator $flarumValidator: The Flarum validator wrapper
* - \Illuminate\Validation\Validator $validator: The Laravel validator instance
*/
public function configure($callback)
{
$this->configurationCallbacks[] = $callback;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
$container->resolving($this->validator, function ($validator, $container) {
foreach ($this->configurationCallbacks as $callback) {
$validator->addConfiguration(ContainerUtil::wrapCallback($callback, $container));
}
});
}
}

View File

@@ -0,0 +1,26 @@
<?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\Filter;
interface FilterInterface
{
/**
* This filter will only be run when a query contains a filter param with this key.
*/
public function getKey(): string;
/**
* Filters a query.
*
* @param WrappedFilter $filter
* @param string $value The value of the requested filter
*/
public function apply(WrappedFilter $wrappedFilter, $filterValue);
}

88
src/Filter/Filterer.php Normal file
View File

@@ -0,0 +1,88 @@
<?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\Filter;
use Flarum\Search\ApplySearchParametersTrait;
use Flarum\Search\SearchResults;
use Illuminate\Support\Arr;
class Filterer
{
use ApplySearchParametersTrait;
protected static $filters = [];
protected static $filterMutators = [];
public static function addFilter($resource, FilterInterface $filter)
{
if (! array_key_exists($resource, static::$filters)) {
static::$filters[$resource] = [];
}
if (! array_key_exists($filter->getKey(), static::$filters[$resource])) {
static::$filters[$resource][$filter->getKey()] = [];
}
static::$filters[$resource][$filter->getKey()][] = $filter;
}
public static function addFilterMutator($resource, $mutator)
{
if (! array_key_exists($resource, static::$filterMutators)) {
static::$filterMutators[$resource] = [];
}
static::$filterMutators[$resource][] = $mutator;
}
/**
* @param FilterCriteria $criteria
* @param int|null $limit
* @param int $offset
*
* @return FilterResults
*/
public function filter($actor, $query, $filters, $sort = null, $limit = null, $offset = 0, array $load = [])
{
$resource = get_class($query->getModel());
$query->whereVisibleTo($actor);
$wrappedFilter = new WrappedFilter($query->getQuery(), $actor);
foreach ($filters as $filterKey => $filterValue) {
foreach (Arr::get(static::$filters, "$resource.$filterKey", []) as $filter) {
$filter->apply($wrappedFilter, $filterValue);
}
}
$this->applySort($wrappedFilter, $sort);
$this->applyOffset($wrappedFilter, $offset);
$this->applyLimit($wrappedFilter, $limit + 1);
foreach (Arr::get(static::$filterMutators, $resource, []) as $mutator) {
$mutator($query, $actor, $filters, $sort);
}
// Execute the filter query and retrieve the results. We get one more
// results than the user asked for, so that we can say if there are more
// results. If there are, we will get rid of that extra result.
$results = $query->get();
if ($areMoreResults = $limit > 0 && $results->count() > $limit) {
$results->pop();
}
$results->load($load);
return new SearchResults($results, $areMoreResults);
}
}

View File

@@ -0,0 +1,16 @@
<?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\Filter;
use Flarum\Search\AbstractSearch;
class WrappedFilter extends AbstractSearch
{
}

View File

@@ -11,6 +11,9 @@ namespace Flarum\Formatter\Event;
use s9e\TextFormatter\Configurator;
/**
* @deprecated beta 15, removed beta 16. Use the Formatter extender instead.
*/
class Configuring
{
/**

View File

@@ -11,6 +11,9 @@ namespace Flarum\Formatter\Event;
use s9e\TextFormatter\Parser;
/**
* @deprecated beta 15, removed beta 16. Use the Formatter extender instead.
*/
class Parsing
{
/**

View File

@@ -12,6 +12,9 @@ namespace Flarum\Formatter\Event;
use Psr\Http\Message\ServerRequestInterface;
use s9e\TextFormatter\Renderer;
/**
* @deprecated beta 15, removed beta 16. Use the Formatter extender instead.
*/
class Rendering
{
/**

View File

@@ -20,6 +20,12 @@ use s9e\TextFormatter\Unparser;
class Formatter
{
protected $configurationCallbacks = [];
protected $parsingCallbacks = [];
protected $renderingCallbacks = [];
/**
* @var Repository
*/
@@ -47,6 +53,21 @@ class Formatter
$this->cacheDir = $cacheDir;
}
public function addConfigurationCallback($callback)
{
$this->configurationCallbacks[] = $callback;
}
public function addParsingCallback($callback)
{
$this->parsingCallbacks[] = $callback;
}
public function addRenderingCallback($callback)
{
$this->renderingCallbacks[] = $callback;
}
/**
* Parse text.
*
@@ -58,8 +79,13 @@ class Formatter
{
$parser = $this->getParser($context);
// Deprecated in beta 15, remove in beta 16
$this->events->dispatch(new Parsing($parser, $context, $text));
foreach ($this->parsingCallbacks as $callback) {
$text = $callback($parser, $context, $text);
}
return $parser->parse($text);
}
@@ -75,8 +101,13 @@ class Formatter
{
$renderer = $this->getRenderer();
// Deprecated in beta 15, remove in beta 16
$this->events->dispatch(new Rendering($renderer, $context, $xml, $request));
foreach ($this->renderingCallbacks as $callback) {
$xml = $callback($renderer, $context, $xml, $request);
}
return $renderer->render($xml);
}
@@ -122,8 +153,13 @@ class Formatter
$configurator->Autolink;
$configurator->tags->onDuplicate('replace');
// Deprecated in beta 15, remove in beta 16
$this->events->dispatch(new Configuring($configurator));
foreach ($this->configurationCallbacks as $callback) {
$callback($configurator);
}
$this->configureExternalLinks($configurator);
return $configurator;

View File

@@ -11,6 +11,7 @@ namespace Flarum\Forum\Content;
use Flarum\Api\Client;
use Flarum\Api\Controller\ListDiscussionsController;
use Flarum\Api\Controller\SearchDiscussionsController;
use Flarum\Frontend\Document;
use Flarum\Http\UrlGenerator;
use Flarum\Settings\SettingsRepositoryInterface;
@@ -114,6 +115,6 @@ class Index
*/
private function getApiDocument(User $actor, array $params)
{
return json_decode($this->api->send(ListDiscussionsController::class, $actor, $params)->getBody());
return json_decode($this->api->send(($params['filter']['q'] ? SearchDiscussionsController::class : ListDiscussionsController::class), $actor, $params)->getBody());
}
}

View File

@@ -64,8 +64,9 @@ class ForumServiceProvider extends AbstractServiceProvider
HttpMiddleware\StartSession::class,
HttpMiddleware\RememberFromCookie::class,
HttpMiddleware\AuthenticateWithSession::class,
HttpMiddleware\CheckCsrfToken::class,
HttpMiddleware\SetLocale::class,
'flarum.forum.route_resolver',
HttpMiddleware\CheckCsrfToken::class,
HttpMiddleware\ShareErrorsFromSession::class
];
});
@@ -78,6 +79,10 @@ class ForumServiceProvider extends AbstractServiceProvider
);
});
$this->app->bind('flarum.forum.route_resolver', function () {
return new HttpMiddleware\ResolveRoute($this->app->make('flarum.forum.routes'));
});
$this->app->singleton('flarum.forum.handler', function () {
$pipe = new MiddlewarePipe;
@@ -85,7 +90,7 @@ class ForumServiceProvider extends AbstractServiceProvider
$pipe->pipe($this->app->make($middleware));
}
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.forum.routes')));
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
return $pipe;
});
@@ -198,8 +203,8 @@ class ForumServiceProvider extends AbstractServiceProvider
$factory = $this->app->make(RouteHandlerFactory::class);
$defaultRoute = $this->app->make('flarum.settings')->get('default_route');
if (isset($routes->getRouteData()[0]['GET'][$defaultRoute])) {
$toDefaultController = $routes->getRouteData()[0]['GET'][$defaultRoute];
if (isset($routes->getRouteData()[0]['GET'][$defaultRoute]['handler'])) {
$toDefaultController = $routes->getRouteData()[0]['GET'][$defaultRoute]['handler'];
} else {
$toDefaultController = $factory->toForum(Content\Index::class);
}

View File

@@ -18,6 +18,16 @@ use Symfony\Component\Translation\TranslatorInterface;
abstract class AbstractValidator
{
/**
* @var array
*/
protected $configuration = [];
public function addConfiguration($callable)
{
$this->configuration[] = $callable;
}
/**
* @var array
*/
@@ -92,10 +102,17 @@ abstract class AbstractValidator
$validator = $this->validator->make($attributes, $rules, $this->getMessages());
/**
* @deprecated in beta 15, removed in beta 16.
*/
$this->events->dispatch(
new Validating($this, $validator)
);
foreach ($this->configuration as $callable) {
$callable($this, $validator);
}
return $validator;
}
}

View File

@@ -21,7 +21,7 @@ class Application
*
* @var string
*/
const VERSION = '0.1.0-beta.14-dev';
const VERSION = '0.1.0-beta.14';
/**
* The IoC container for the Flarum application.

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Foundation;
use Illuminate\Contracts\Container\Container;
class ContainerUtil
{
/**
* Wraps a callback so that string-based invokable classes get resolved only when actually used.
*
* @internal Backwards compatability not guaranteed.
*
* @param callable|string $callback: A callable, or a ::class attribute of an invokable class
* @param Container $container
*/
public static function wrapCallback($callback, Container $container)
{
if (is_string($callback)) {
$callback = function () use ($container, $callback) {
$callback = $container->make($callback);
return call_user_func_array($callback, func_get_args());
};
}
return $callback;
}
}

View File

@@ -13,6 +13,7 @@ use Flarum\Foundation\AbstractValidator;
use Illuminate\Validation\Validator;
/**
* @deprecated in Beta 15, remove in beta 16. Use the Validator extender instead.
* The `Validating` event is called when a validator instance for a
* model is being built. This event can be used to add custom rules/extensions
* to the validator for when validation takes place.

View File

@@ -9,7 +9,7 @@
namespace Flarum\Foundation;
use Flarum\Http\Middleware\DispatchRoute;
use Flarum\Http\Middleware as HttpMiddleware;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Console\Command;
use Illuminate\Contracts\Container\Container;
@@ -85,8 +85,9 @@ class InstalledApp implements AppInterface
$pipe = new MiddlewarePipe;
$pipe->pipe(new BasePath($this->basePath()));
$pipe->pipe(
new DispatchRoute($this->container->make('flarum.update.routes'))
new HttpMiddleware\ResolveRoute($this->container->make('flarum.update.routes'))
);
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
return $pipe;
}

View File

@@ -19,7 +19,7 @@ class HttpServiceProvider extends AbstractServiceProvider
public function register()
{
$this->app->singleton('flarum.http.csrfExemptPaths', function () {
return ['/api/token'];
return ['token'];
});
$this->app->bind(Middleware\CheckCsrfToken::class, function ($app) {

View File

@@ -28,7 +28,10 @@ class CheckCsrfToken implements Middleware
{
$path = $request->getAttribute('originalUri')->getPath();
foreach ($this->exemptRoutes as $exemptRoute) {
if (fnmatch($exemptRoute, $path)) {
/**
* @deprecated path match should be removed in beta 16, only route name match should be supported.
*/
if ($exemptRoute === $request->getAttribute('routeName') || fnmatch($exemptRoute, $path)) {
return $handler->handle($request);
}
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Http\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as Handler;
class ExecuteRoute implements Middleware
{
/**
* Executes the route handler resolved in ResolveRoute.
*/
public function process(Request $request, Handler $handler): Response
{
$handler = $request->getAttribute('routeHandler');
$parameters = $request->getAttribute('routeParameters');
return $handler($request, $parameters);
}
}

View File

@@ -18,7 +18,7 @@ use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as Handler;
class DispatchRoute implements Middleware
class ResolveRoute implements Middleware
{
/**
* @var RouteCollection
@@ -41,7 +41,7 @@ class DispatchRoute implements Middleware
}
/**
* Dispatch the given request to our route collection.
* Resolve the given request from our route collection.
*
* @throws MethodNotAllowedException
* @throws RouteNotFoundException
@@ -59,10 +59,12 @@ class DispatchRoute implements Middleware
case Dispatcher::METHOD_NOT_ALLOWED:
throw new MethodNotAllowedException($method);
case Dispatcher::FOUND:
$handler = $routeInfo[1];
$parameters = $routeInfo[2];
$request = $request
->withAttribute('routeName', $routeInfo[1]['name'])
->withAttribute('routeHandler', $routeInfo[1]['handler'])
->withAttribute('routeParameters', $routeInfo[2]);
return $handler($request, $parameters);
return $handler->handle($request);
}
}

57
src/Http/RequestUtil.php Normal file
View File

@@ -0,0 +1,57 @@
<?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\Http;
use Flarum\User\User;
use Illuminate\Contracts\Session\Session;
use Psr\Http\Message\ServerRequestInterface as Request;
class RequestUtil
{
public static function getActor(Request $request): User
{
return $request->getAttribute('actor');
}
public function withActor(Request $request, User $actor): Request
{
return $request->withAttribute('actor', $actor);
}
public function getSession(Request $request): Session
{
return $request->getAttribute('session');
}
public function withSession(Request $request, Session $session): Request
{
return $request->withAttribute('session', $session);
}
public function getLocale(Request $request): string
{
return $request->getAttribute('bypassCsrfToken');
}
public function withLocale(Request $request, string $locale): Request
{
return $request->withAttribute('locale', $locale);
}
public function getRouteName(Request $request): string
{
return $request->getAttribute('routeName');
}
public function withRouteName(Request $request, string $routeName): Request
{
return $request->withAttribute('routeName', $routeName);
}
}

View File

@@ -66,7 +66,7 @@ class RouteCollection
$routeDatas = $this->routeParser->parse($path);
foreach ($routeDatas as $routeData) {
$this->dataGenerator->addRoute($method, $routeData, $handler);
$this->dataGenerator->addRoute($method, $routeData, ['name' => $name, 'handler' => $handler]);
}
$this->reverse[$name] = $routeDatas;

View File

@@ -13,9 +13,7 @@ use Flarum\Foundation\AppInterface;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\ErrorHandling\WhoopsFormatter;
use Flarum\Http\Middleware\DispatchRoute;
use Flarum\Http\Middleware\HandleErrors;
use Flarum\Http\Middleware\StartSession;
use Flarum\Http\Middleware as HttpMiddleware;
use Flarum\Install\Console\InstallCommand;
use Illuminate\Contracts\Container\Container;
use Laminas\Stratigility\MiddlewarePipe;
@@ -38,15 +36,16 @@ class Installer implements AppInterface
public function getRequestHandler()
{
$pipe = new MiddlewarePipe;
$pipe->pipe(new HandleErrors(
$pipe->pipe(new HttpMiddleware\HandleErrors(
$this->container->make(Registry::class),
$this->container->make(WhoopsFormatter::class),
$this->container->tagged(Reporter::class)
));
$pipe->pipe($this->container->make(StartSession::class));
$pipe->pipe($this->container->make(HttpMiddleware\StartSession::class));
$pipe->pipe(
new DispatchRoute($this->container->make('flarum.install.routes'))
new HttpMiddleware\ResolveRoute($this->container->make('flarum.install.routes'))
);
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
return $pipe;
}

View File

@@ -53,7 +53,7 @@ class WritablePaths implements PrerequisiteInterface
})->map(function ($path, $index) {
return [
'message' => 'The '.$this->getAbsolutePath($path).' directory is not writable.',
'detail' => 'Please chmod this directory'.(in_array($index, $this->wildcards) ? ' and its contents' : '').' to 0775.'
'detail' => 'Please make sure your web server/PHP user has write access to this directory'.(in_array($index, $this->wildcards) ? ' and its contents' : '').'. Read the <a href="https://docs.flarum.org/install.html#folder-ownership">installation documentation</a> for a detailed explanation and steps to resolve this error.'
];
});
}

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Notification\Driver;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\Job\SendNotificationsJob;
use Flarum\User\User;
use Illuminate\Contracts\Queue\Queue;
class AlertNotificationDriver implements NotificationDriverInterface
{
/**
* @var Queue
*/
private $queue;
public function __construct(Queue $queue)
{
$this->queue = $queue;
}
/**
* {@inheritDoc}
*/
public function send(BlueprintInterface $blueprint, array $users): void
{
if (count($users)) {
$this->queue->push(new SendNotificationsJob($blueprint, $users));
}
}
/**
* {@inheritdoc}
*/
public function registerType(string $blueprintClass, array $driversEnabledByDefault): void
{
User::addPreference(
User::getNotificationPreferenceKey($blueprintClass::getType(), 'alert'),
'boolval',
in_array('alert', $driversEnabledByDefault)
);
}
}

View File

@@ -0,0 +1,69 @@
<?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\Notification\Driver;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\Job\SendEmailNotificationJob;
use Flarum\Notification\MailableInterface;
use Flarum\User\User;
use Illuminate\Contracts\Queue\Queue;
use ReflectionClass;
class EmailNotificationDriver implements NotificationDriverInterface
{
/**
* @var Queue
*/
private $queue;
public function __construct(Queue $queue)
{
$this->queue = $queue;
}
/**
* {@inheritDoc}
*/
public function send(BlueprintInterface $blueprint, array $users): void
{
if ($blueprint instanceof MailableInterface) {
$this->mailNotifications($blueprint, $users);
}
}
/**
* Mail a notification to a list of users.
*
* @param MailableInterface $blueprint
* @param User[] $recipients
*/
protected function mailNotifications(MailableInterface $blueprint, array $recipients)
{
foreach ($recipients as $user) {
if ($user->shouldEmail($blueprint::getType())) {
$this->queue->push(new SendEmailNotificationJob($blueprint, $user));
}
}
}
/**
* {@inheritdoc}
*/
public function registerType(string $blueprintClass, array $driversEnabledByDefault): void
{
if ((new ReflectionClass($blueprintClass))->implementsInterface(MailableInterface::class)) {
User::addPreference(
User::getNotificationPreferenceKey($blueprintClass::getType(), 'email'),
'boolval',
in_array('email', $driversEnabledByDefault)
);
}
}
}

View File

@@ -0,0 +1,34 @@
<?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\Notification\Driver;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\User\User;
interface NotificationDriverInterface
{
/**
* Conditionally sends a notification to users, generally using a queue.
*
* @param BlueprintInterface $blueprint
* @param User[] $users
* @return void
*/
public function send(BlueprintInterface $blueprint, array $users): void;
/**
* Logic for registering a notification type, generally used for adding a user preference.
*
* @param string $blueprintClass
* @param array $driversEnabledByDefault
* @return void
*/
public function registerType(string $blueprintClass, array $driversEnabledByDefault): void;
}

View File

@@ -11,6 +11,9 @@ namespace Flarum\Notification\Event;
use Flarum\Notification\Blueprint\BlueprintInterface;
/**
* @deprecated in beta 15, removed in beta 16
*/
class Sending
{
/**

View File

@@ -10,7 +10,6 @@
namespace Flarum\Notification\Job;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\Event\Sending;
use Flarum\Notification\Notification;
use Flarum\Queue\AbstractJob;
use Flarum\User\User;
@@ -35,8 +34,6 @@ class SendNotificationsJob extends AbstractJob
public function handle()
{
event(new Sending($this->blueprint, $this->recipients));
Notification::notify($this->recipients, $this->blueprint);
}
}

View File

@@ -12,51 +12,76 @@ namespace Flarum\Notification;
use Flarum\Event\ConfigureNotificationTypes;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Notification\Blueprint\DiscussionRenamedBlueprint;
use Flarum\User\User;
use ReflectionClass;
class NotificationServiceProvider extends AbstractServiceProvider
{
/**
* {@inheritdoc}
*/
public function register()
{
$this->app->singleton('flarum.notification.drivers', function () {
return [
'alert' => Driver\AlertNotificationDriver::class,
'email' => Driver\EmailNotificationDriver::class,
];
});
$this->app->singleton('flarum.notification.blueprints', function () {
return [
DiscussionRenamedBlueprint::class => ['alert']
];
});
}
/**
* {@inheritdoc}
*/
public function boot()
{
$this->registerNotificationTypes();
$this->setNotificationDrivers();
$this->setNotificationTypes();
}
/**
* Register notification drivers.
*/
protected function setNotificationDrivers()
{
foreach ($this->app->make('flarum.notification.drivers') as $driverName => $driver) {
NotificationSyncer::addNotificationDriver($driverName, $this->app->make($driver));
}
}
/**
* Register notification types.
*/
public function registerNotificationTypes()
protected function setNotificationTypes()
{
$blueprints = [
DiscussionRenamedBlueprint::class => ['alert']
];
$blueprints = $this->app->make('flarum.notification.blueprints');
// Deprecated in beta 15, remove in beta 16
$this->app->make('events')->dispatch(
new ConfigureNotificationTypes($blueprints)
);
foreach ($blueprints as $blueprint => $enabled) {
Notification::setSubjectModel(
$type = $blueprint::getType(),
$blueprint::getSubjectModel()
);
foreach ($blueprints as $blueprint => $driversEnabledByDefault) {
$this->addType($blueprint, $driversEnabledByDefault);
}
}
User::addPreference(
User::getNotificationPreferenceKey($type, 'alert'),
'boolval',
in_array('alert', $enabled)
);
protected function addType(string $blueprint, array $driversEnabledByDefault)
{
Notification::setSubjectModel(
$type = $blueprint::getType(),
$blueprint::getSubjectModel()
);
if ((new ReflectionClass($blueprint))->implementsInterface(MailableInterface::class)) {
User::addPreference(
User::getNotificationPreferenceKey($type, 'email'),
'boolval',
in_array('email', $enabled)
);
}
foreach (NotificationSyncer::getNotificationDrivers() as $driverName => $driver) {
$driver->registerType(
$blueprint,
$driversEnabledByDefault
);
}
}
}

View File

@@ -10,10 +10,9 @@
namespace Flarum\Notification;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\Job\SendEmailNotificationJob;
use Flarum\Notification\Job\SendNotificationsJob;
use Flarum\Notification\Driver\NotificationDriverInterface;
use Flarum\Notification\Event\Sending;
use Flarum\User\User;
use Illuminate\Contracts\Queue\Queue;
/**
* The Notification Syncer commits notification blueprints to the database, and
@@ -38,14 +37,11 @@ class NotificationSyncer
protected static $sentTo = [];
/**
* @var Queue
* A map of notification drivers.
*
* @var NotificationDriverInterface[]
*/
protected $queue;
public function __construct(Queue $queue)
{
$this->queue = $queue;
}
protected static $notificationDrivers = [];
/**
* Sync a notification so that it is visible to the specified users, and not
@@ -102,12 +98,13 @@ class NotificationSyncer
// receiving this notification for the first time (we know because they
// didn't have a record in the database). As both operations can be
// intensive on resources (database and mail server), we queue them.
if (count($newRecipients)) {
$this->queue->push(new SendNotificationsJob($blueprint, $newRecipients));
foreach (static::getNotificationDrivers() as $driverName => $driver) {
$driver->send($blueprint, $newRecipients);
}
if ($blueprint instanceof MailableInterface) {
$this->mailNotifications($blueprint, $newRecipients);
if (count($newRecipients)) {
// Deprecated in beta 15, removed in beta 16
event(new Sending($blueprint, $newRecipients));
}
}
@@ -150,21 +147,6 @@ class NotificationSyncer
static::$onePerUser = false;
}
/**
* Mail a notification to a list of users.
*
* @param MailableInterface $blueprint
* @param User[] $recipients
*/
protected function mailNotifications(MailableInterface $blueprint, array $recipients)
{
foreach ($recipients as $user) {
if ($user->shouldEmail($blueprint::getType())) {
$this->queue->push(new SendEmailNotificationJob($blueprint, $user));
}
}
}
/**
* Set the deleted status of a list of notification records.
*
@@ -175,4 +157,23 @@ class NotificationSyncer
{
Notification::whereIn('id', $ids)->update(['is_deleted' => $isDeleted]);
}
/**
* Adds a notification driver to the list.
*
* @param string $driverName
* @param NotificationDriverInterface $driver
*/
public static function addNotificationDriver(string $driverName, NotificationDriverInterface $driver)
{
static::$notificationDrivers[$driverName] = $driver;
}
/**
* @return NotificationDriverInterface[]
*/
public static function getNotificationDrivers(): array
{
return static::$notificationDrivers;
}
}

View File

@@ -218,7 +218,7 @@ class Post extends AbstractModel
* @param string $model The class name of the model for that type.
* @return void
*/
public static function setModel($type, $model)
public static function setModel(string $type, string $model)
{
static::$models[$type] = $model;
}

View File

@@ -21,19 +21,20 @@ class PostServiceProvider extends AbstractServiceProvider
{
CommentPost::setFormatter($this->app->make('flarum.formatter'));
$this->registerPostTypes();
$this->setPostTypes();
$events = $this->app->make('events');
$events->subscribe(PostPolicy::class);
}
public function registerPostTypes()
protected function setPostTypes()
{
$models = [
CommentPost::class,
DiscussionRenamedPost::class
];
// Deprecated in beta 15, remove in beta 16.
$this->app->make('events')->dispatch(
new ConfigurePostTypes($models)
);

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Queue\Console;
use Flarum\Foundation\Config;
class WorkCommand extends \Illuminate\Queue\Console\WorkCommand
{
protected function downForMaintenance()
{
if ($this->option('force')) {
return false;
}
/** @var Config $config */
$config = $this->laravel->make(Config::class);
return $config->inMaintenanceMode();
}
}

View File

@@ -1,61 +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\Queue;
use Illuminate\Contracts\Queue\Factory;
use Illuminate\Queue\QueueManager;
/**
* A hacky workaround to avoid injecting an entire QueueManager (which we don't
* want to build) into Laravel's queue worker class.
*
* Laravel 6.0 will clean this up; once we upgrade, we can remove this hack and
* directly inject the factory.
*/
class HackyManagerForWorker extends QueueManager implements Factory
{
/**
* @var Factory
*/
private $factory;
/**
* HackyManagerForWorker constructor.
*
* Needs a real connection factory to delegate to.
*
* @param Factory $factory
*/
public function __construct(Factory $factory)
{
$this->factory = $factory;
}
/**
* Resolve a queue connection instance.
*
* @param string $name
* @return \Illuminate\Contracts\Queue\Queue
*/
public function connection($name = null)
{
return $this->factory->connection($name);
}
/**
* Determine if the application is in maintenance mode.
*
* @return bool
*/
public function isDownForMaintenance()
{
return false;
}
}

View File

@@ -9,8 +9,8 @@
namespace Flarum\Queue;
use Flarum\Console\Event\Configuring;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\Config;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\Paths;
@@ -22,6 +22,7 @@ use Illuminate\Queue\Console as Commands;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Queue\Failed\NullFailedJobProvider;
use Illuminate\Queue\Listener as QueueListener;
use Illuminate\Queue\QueueManager;
use Illuminate\Queue\SyncQueue;
use Illuminate\Queue\Worker;
@@ -34,7 +35,7 @@ class QueueServiceProvider extends AbstractServiceProvider
Commands\ListFailedCommand::class,
Commands\RestartCommand::class,
Commands\RetryCommand::class,
Commands\WorkCommand::class,
Console\WorkCommand::class,
];
public function register()
@@ -61,10 +62,16 @@ class QueueServiceProvider extends AbstractServiceProvider
});
$this->app->singleton(Worker::class, function ($app) {
/** @var Config $config */
$config = $app->make(Config::class);
return new Worker(
new HackyManagerForWorker($app[Factory::class]),
new QueueManager($app),
$app['events'],
$app[ExceptionHandling::class]
$app[ExceptionHandling::class],
function () use ($config) {
return $config->inMaintenanceMode();
}
);
});
@@ -110,17 +117,17 @@ class QueueServiceProvider extends AbstractServiceProvider
protected function registerCommands()
{
$this->app['events']->listen(Configuring::class, function (Configuring $event) {
$this->app->extend('flarum.console.commands', function ($commands) {
$queue = $this->app->make(Queue::class);
// There is no need to have the queue commands when using the sync driver.
if ($queue instanceof SyncQueue) {
return;
return $commands;
}
foreach ($this->commands as $command) {
$event->addCommand($command);
}
// Otherwise add our commands, while allowing them to be overridden by those
// already added through the container.
return array_merge($this->commands, $commands);
});
}

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