1
0
mirror of https://github.com/flarum/core.git synced 2025-08-15 12:54:47 +02:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Alexander Skvortsov
d17cc1beee Only automatically load page for UserPage if username param is present
This avoid issues with SettingsPage
2020-10-04 22:02:16 -04:00
Alexander Skvortsov
759eb80ff9 Remove redundant code from IndexPage oninit 2020-10-04 22:00:11 -04:00
Alexander Skvortsov
46cb32046d Abstract away onNewRoute and routeHasChanged
This provides a cleaner interface for dealing with pages that handle multiple routes. We also move `modal` and `drawer` close calls to ensure those happen on route change with the same component.
2020-10-04 21:49:51 -04:00
120 changed files with 609 additions and 2886 deletions

View File

@@ -1,102 +1,5 @@
# Changelog
## [0.1.0-beta.14](https://github.com/flarum/core/compare/v0.1.0-beta.13...v0.1.0-beta.14)
### Added
- Check dependencies before enabling / disabling extensions (https://github.com/flarum/core/pull/2188)
- Set up temporary infrastructure for TypeScript in core (https://github.com/flarum/core/pull/2206)
- Better UI for request error modals (https://github.com/flarum/core/pull/1929)
- Display name extender, tests, frontend UI (https://github.com/flarum/core/pull/2174)
- Scroll to post or show alert when editing a post from another page (https://github.com/flarum/core/pull/2108)
- Feature to test email config by sending an email to the current user (https://github.com/flarum/core/pull/2023)
- Allow searching users by group ID using the group gambit (https://github.com/flarum/core/pull/2192)
- Use `liveHumanTimes` helper to update times without reload/rerender (https://github.com/flarum/core/pull/2208)
- View extender, tests (https://github.com/flarum/core/pull/2134)
- User extender to replace `PrepareUserGroups` (https://github.com/flarum/core/pull/2110)
- Increase extensibility of skeleton PHP (https://github.com/flarum/core/pull/2308, https://github.com/flarum/core/pull/2318)
- Pass a translator instance to `getEmailSubject` in `MailableInterface` (https://github.com/flarum/core/pull/2244)
- Force LF line endings on windows (https://github.com/flarum/core/pull/2321)
- Add a `Link` component for internal and external links (https://github.com/flarum/core/pull/2315)
- `ConfirmDocumentUnload` component
- Error handler middleware can now be manipulated by the middleware extender
### Changed
- Update to Mithril 2 (https://github.com/flarum/core/pull/2255)
- Stop storing component instances (https://github.com/flarum/core/issues/1821, https://github.com/flarum/core/issues/2144)
- Update to Laravel 6.x (https://github.com/flarum/core/issues/2055)
- `Flarum\Foundation\Application` no longer implements `Illuminate\Contracts\Foundation\Application` (#2142)
- `Flarum\Foundation\Application` no longer inherits `Illuminate\Container\Container` (#2142)
- `paths` have been split off from `Flarum\Foundation\Application` into `Flarum\Foundation\Paths`, which can be injected where needed (#2142)
- `Flarum\User\Gate` no longer implements `Illuminate\Contracts\Auth\Access\Gate` (https://github.com/flarum/core/pull/2181)
- Improve Group Gambit performance (https://github.com/flarum/core/pull/2192)
- Switch to `dayjs` from `momentjs` (https://github.com/flarum/core/pull/2219)
- Don't create a `bio` column in `users` for new installations (https://github.com/flarum/core/pull/2215)
- Start converting core JS to TypeScript (https://github.com/flarum/core/pull/2207)
- Make Carbon an explicit dependency (https://github.com/flarum/core/commit/3b39c212e0fef7522e7d541a9214ff3817138d5d)
- Use Symfony's translator interface instead of Laravel's (https://github.com/flarum/core/pull/2243)
- Use newer versions of fontawesome (https://github.com/flarum/core/pull/2274)
- Use URL generator instead of `app()->url()` where possible (https://github.com/flarum/core/pull/2302)
- Move config from `config.php` into an injectable helper class (https://github.com/flarum/core/pull/2271)
- Use reserved TLD for bogus and test urls (https://github.com/flarum/core/commit/6860b24b70bd04544dde90e537ce021a5fc5a689)
- Replace `m.stream` with `flarum/utils/Stream` (https://github.com/flarum/core/pull/2316)
- Replace `affixedSidebar` util with `AffixedSidebar` component
- Replace `m.withAttr` with `flarum/utils/withAttr`
- Scroll Listener is now passive, performance improvement (https://github.com/flarum/core/pull/2387)
### Fixed
- `generate:migration` command for extensions (https://github.com/flarum/core/commit/443949f7b9d7558dbc1e0994cb898cbac59bec87)
- Container config for `UninstalledSite` (https://github.com/flarum/core/commit/ecdce44d555dd36a365fd472b2916e677ef173cf)
- Tooltip glitch on page chang (https://github.com/flarum/core/issues/2118)
- Using multiple extenders in tests (https://github.com/flarum/core/commit/c4f4f218bf4b175a30880b807f9ccb1a37a25330)
- Header glitch when opening modals (https://github.com/flarum/core/pull/2131)
- Ensure `SameSite` is explicitly set for cookies (https://github.com/flarum/core/pull/2159)
- Ensure `Flarum\User\Event\AvatarChanged` event is properly dispatched (https://github.com/flarum/core/pull/2197)
- Show correct error message on wrong password when changing email (https://github.com/flarum/core/pull/2171)
- Discussion unreadCount could be higher than commentCount if posts deleted (https://github.com/flarum/core/pull/2195)
- Don't show page title on the default route (https://github.com/flarum/core/pull/2047)
- Add page title to `All Discussions` page when it isn't the default route (https://github.com/flarum/core/pull/2047)
- Accept `'0'` as `false` for `flarum/components/Checkbox` (https://github.com/flarum/core/pull/2210)
- Fix PostStreamScrubber background (https://github.com/flarum/core/pull/2222)
- Test port on BaseUrl tests (https://github.com/flarum/core/pull/2226)
- `UrlGenerator` can now generate urls with optional parameters (https://github.com/flarum/core/pull/2246)
- Allow `less` to be compiled independently of Flarum (https://github.com/flarum/core/pull/2252)
- Use correct number abbreviation (https://github.com/flarum/core/pull/2261)
- Ensure avatar html uses alt tags for accessibility (https://github.com/flarum/core/pull/2269)
- Escape regex when searching (https://github.com/flarum/core/pull/2273)
- Remove unneeded semicolons inserted during JS compilation (https://github.com/flarum/core/pull/2280)
- Don't require a username/password for SMTP (https://github.com/flarum/core/pull/2287)
- Allow uppercase entries for SMTP encryption validation (https://github.com/flarum/core/pull/2289)
- Ensure that the right number of posts is returned from list posts API (https://github.com/flarum/core/pull/2291)
- Fix a variety of PostStream bugs (https://github.com/flarum/core/pull/2160, https://github.com/flarum/core/pull/2160)
- Sliding discussion glitch on mobile (https://github.com/flarum/core/pull/2324)
- Sliding discussion button in wrong place (https://github.com/flarum/core/pull/2330, https://github.com/flarum/core/pull/2383)
- Sliding discussion glitch on mobile (https://github.com/flarum/core/pull/2381)
- Fix PostStream for posts with top margins, and scrubber position when scrolling below posts (https://github.com/flarum/core/pull/2369)
### Removed
- `Flarum\Event\AbstractConfigureRoutes` event class
- `Flarum\Event\ConfigureApiRoutes` event class
- `Flarum\Event\ConfigureForumRoutes` event class
- `Flarum\Console\Event\Configuring` event class
- `Flarum\Event\ConfigureModelDates` event class
- `Flarum\Event\ConfigureLocales` event class
- `Flarum\Event\ConfigureModelDefaultAttributes` event class
- `Flarum\Event\GetModelRelationship` event class
- `Flarum\User\Event\BioChanged` event class
- `Flarum\Database\MigrationServiceProvider` moved into `Flarum\Database\DatabaseServiceProvider`
- Unused `admin/components/Widget` component (`admin/component/DashboardWidget` should be used instead)
- Mandrill mail driver (https://github.com/flarum/core/commit/bca833d3f1c34d45d95bf905902368a2753b8908)
### Deprecated
- `Flarum\User\Event\GetDisplayName` event class
- Global path helpers, `Flarum\Foundation\Application` path methods (https://github.com/flarum/core/pull/2155)
- `Flarum\User\AssertPermissionTrait` (https://github.com/flarum/core/pull/2044)
## [0.1.0-beta.13](https://github.com/flarum/core/compare/v0.1.0-beta.12...v0.1.0-beta.13)
### Added

View File

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

14
js/dist/admin.js vendored

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.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
"integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
},
"jquery.hotkeys": {
"version": "0.1.0",
@@ -4546,6 +4546,11 @@
"integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==",
"dev": true
},
"serialize-javascript": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
"integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ=="
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -4895,29 +4900,21 @@
}
},
"terser-webpack-plugin": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz",
"integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==",
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
"integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
"requires": {
"cacache": "^12.0.2",
"find-cache-dir": "^2.1.0",
"is-wsl": "^1.1.0",
"schema-utils": "^1.0.0",
"serialize-javascript": "^4.0.0",
"serialize-javascript": "^2.1.2",
"source-map": "^0.6.1",
"terser": "^4.1.2",
"webpack-sources": "^1.4.0",
"worker-farm": "^1.7.0"
},
"dependencies": {
"serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"requires": {
"randombytes": "^2.1.0"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",

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.5.1",
"jquery": "^3.4.1",
"jquery.hotkeys": "^0.1.0",
"lodash-es": "^4.17.14",
"m.attrs.bidi": "github:tobscure/m.attrs.bidi",

View File

@@ -27,19 +27,20 @@ export default class AdminApplication extends Application {
* @inheritdoc
*/
mount() {
// Mithril does not render the home route on https://example.com/admin, so
// we need to go to https://example.com/admin#/ explicitly.
if (!document.location.hash) document.location.hash = '#/';
m.route.prefix = '#';
super.mount();
m.mount(document.getElementById('app-navigation'), { view: () => Navigation.component({ className: 'App-backControl', drawer: true }) });
m.mount(document.getElementById('header-navigation'), Navigation);
m.mount(document.getElementById('header-primary'), HeaderPrimary);
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
m.mount(document.getElementById('admin-navigation'), AdminNav);
// Mithril does not render the home route on https://example.com/admin, so
// we need to go to https://example.com/admin#/ explicitly.
if (!document.location.hash) document.location.hash = '#/';
m.route.prefix = '#';
super.mount();
// If an extension has just been enabled, then we will run its settings
// callback.
const enabled = localStorage.getItem('enabledExtension');

View File

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

View File

@@ -198,19 +198,13 @@ export default class Application {
m.route(document.getElementById('content'), basePath + '/', mapRoutes(this.routes, basePath));
// Add a class to the body which indicates that the page has been scrolled
// down. When this happens, we'll add classes to the header and app body
// which will set the navbar's position to fixed. We don't want to always
// have it fixed, as that could overlap with custom headers.
const scrollListener = new ScrollListener((top) => {
// down.
new ScrollListener((top) => {
const $app = $('#app');
const offset = $app.offset().top;
$app.toggleClass('affix', top >= offset).toggleClass('scrolled', top > offset);
$('.App-header').toggleClass('navbar-fixed-top', top >= offset);
});
scrollListener.start();
scrollListener.update();
}).start();
$(() => {
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
@@ -278,7 +272,7 @@ export default class Application {
/**
* Make an AJAX request, handling any low-level errors that may occur.
*
* @see https://mithril.js.org/request.html
* @see https://lhorie.github.io/mithril/mithril.request.html
* @param {Object} options
* @return {Promise}
* @public

View File

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

View File

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

View File

@@ -12,14 +12,12 @@ 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,8 +35,7 @@ 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
// Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
return new Promise((resolve) => setTimeout(resolve, 300));
return new Promise((resolve) => setTimeout(resolve, 1000));
}
}

View File

@@ -10,11 +10,7 @@ export default class Page extends Component {
oninit(vnode) {
super.oninit(vnode);
app.previous = app.current;
app.current = new PageState(this.constructor, { routeName: this.attrs.routeName });
app.drawer.hide();
app.modal.close();
this.onNewRoute();
/**
* A class name to apply to the body while the route is active.
@@ -23,12 +19,35 @@ export default class Page extends Component {
*/
this.bodyClass = '';
/**
* Whether we should scroll to the top of the page when its rendered.
*
* @type {Boolean}
*/
this.scrollTopOnCreate = true;
this.currentPath = m.route.get();
}
routeHasChanged() {
return this.currentPath !== m.route.get();
}
/**
* 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() {
this.currentPath = m.route.get();
app.previous = app.current;
app.current = new PageState(this.constructor, { routeName: this.attrs.routeName });
app.drawer.hide();
app.modal.close();
}
onbeforeupdate(vnode, old) {
super.onbeforeupdate(vnode, old);
if (this.routeHasChanged()) {
this.onNewRoute();
}
}
oncreate(vnode) {
@@ -37,10 +56,6 @@ 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: Date): Mithril.Vnode {
export default function fullTime(time) {
const d = dayjs(time);
const datetime = d.format();

View File

@@ -1,13 +1,14 @@
import dayjs from 'dayjs';
import * as Mithril from 'mithril';
import humanTimeUtil from '../utils/humanTime';
/**
* The `humanTime` helper displays a time in a human-friendly time-ago format
* (e.g. '12 days ago'), wrapped in a <time> tag with other information about
* the time.
*
* @param {Date} time
* @return {Object}
*/
export default function humanTime(time: Date): Mithril.Vnode {
export default function humanTime(time) {
const d = dayjs(time);
const datetime = d.format();

View File

@@ -0,0 +1,12 @@
/**
* 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

@@ -1,13 +0,0 @@
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

@@ -1,41 +0,0 @@
import Mithril from 'mithril';
/**
* Generates a route resolver for a given component.
* In addition to regular route resolver functionality:
* - It provide the current route name as an attr
* - It sets a key on the component so a rerender will be triggered on route change.
*/
export default class DefaultResolver {
component: Mithril.Component;
routeName: string;
constructor(component, routeName) {
this.component = component;
this.routeName = routeName;
}
/**
* When a route change results in a changed key, a full page
* rerender occurs. This method can be overriden in subclasses
* to prevent rerenders on some route changes.
*/
makeKey() {
return this.routeName + JSON.stringify(m.route.param());
}
makeAttrs(vnode) {
return {
...vnode.attrs,
routeName: this.routeName,
};
}
onmatch(args, requestedPath, route) {
return this.component;
}
render(vnode) {
return [{ ...vnode, attrs: this.makeAttrs(vnode), key: this.makeKey() }];
}
}

View File

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

View File

@@ -28,9 +28,6 @@ export default class SubtreeRetainer {
constructor(...callbacks) {
this.callbacks = callbacks;
this.data = {};
// Build the initial data, so it is available when calling
// needsRebuild from the onbeforeupdate hook.
this.needsRebuild();
}
/**
@@ -63,8 +60,6 @@ export default class SubtreeRetainer {
*/
check(...callbacks) {
this.callbacks = this.callbacks.concat(callbacks);
// Update the data cache when new checks are added.
this.needsRebuild();
}
/**

View File

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

View File

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

View File

@@ -115,19 +115,17 @@ export default class ForumApplication extends Application {
this.routes[defaultAction].path = '/';
this.history.push(defaultAction, this.translator.trans('core.forum.header.back_to_index_tooltip'), '/');
this.pane = new Pane(document.getElementById('app'));
m.route.prefix = '';
super.mount(this.forum.attribute('basePath'));
// We mount navigation and header components after the page, so components
// like the back button can access the updated state when rendering.
m.mount(document.getElementById('app-navigation'), { view: () => Navigation.component({ className: 'App-backControl', drawer: true }) });
m.mount(document.getElementById('header-navigation'), Navigation);
m.mount(document.getElementById('header-primary'), HeaderPrimary);
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
m.mount(document.getElementById('composer'), { view: () => Composer.component({ state: this.composer }) });
this.pane = new Pane(document.getElementById('app'));
m.route.prefix = '';
super.mount(this.forum.attribute('basePath'));
alertEmailConfirmation(this);
// Route the home link back home when clicked. We do not want it to register

View File

@@ -71,7 +71,6 @@ import Search from './components/Search';
import DiscussionListItem from './components/DiscussionListItem';
import LoadingPost from './components/LoadingPost';
import PostsUserPage from './components/PostsUserPage';
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
import routes from './routes';
import ForumApplication from './ForumApplication';
@@ -147,7 +146,6 @@ export default Object.assign(compat, {
'components/DiscussionListItem': DiscussionListItem,
'components/LoadingPost': LoadingPost,
'components/PostsUserPage': PostsUserPage,
'resolvers/DiscussionPageResolver': DiscussionPageResolver,
routes: routes,
ForumApplication: ForumApplication,
});

View File

@@ -56,7 +56,9 @@ export default class CommentPost extends Post {
]);
}
refreshContent() {
onupdate(vnode) {
super.onupdate();
const contentHtml = this.isEditing() ? '' : this.attrs.post.contentHtml();
// If the post content has changed since the last render, we'll run through
@@ -64,28 +66,13 @@ export default class CommentPost extends Post {
// necessary because TextFormatter outputs them for e.g. syntax highlighting.
if (this.contentHtml !== contentHtml) {
this.$('.Post-body script').each(function () {
const script = document.createElement('script');
script.textContent = this.textContent;
Array.from(this.attributes).forEach((attr) => script.setAttribute(attr.name, attr.value));
this.parentNode.replaceChild(script, this);
eval.call(window, $(this).text());
});
}
this.contentHtml = contentHtml;
}
oncreate(vnode) {
super.oncreate(vnode);
this.refreshContent();
}
onupdate(vnode) {
super.onupdate(vnode);
this.refreshContent();
}
isEditing() {
return app.composer.bodyMatches(EditPostComposer, { post: this.attrs.post });
}

View File

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

View File

@@ -32,25 +32,12 @@ export default class DiscussionPage extends Page {
*/
this.near = m.route.param('near') || 0;
this.load();
// If the discussion list has been loaded, then we'll enable the pane (and
// hide it by default). Also, if we've just come from another discussion
// page, then we don't want Mithril to redraw the whole page if it did,
// then the pane would redraw which would be slow and would cause problems with
// event handlers.
if (app.discussions.hasDiscussions()) {
app.pane.enable();
app.pane.hide();
}
app.history.push('discussion');
this.bodyClass = 'App--discussion';
}
onremove() {
super.onremove();
// If we are indeed navigating away from this discussion, then disable the
// discussion list pane. Also, if we're composing a reply to this
// discussion, minimize the composer unless it's empty, in which case
@@ -82,6 +69,7 @@ export default class DiscussionPage extends Page {
{PostStream.component({
discussion,
stream: this.stream,
targetPost: this.stream.targetPost,
onPositionChange: this.positionChanged.bind(this),
})}
</div>
@@ -93,6 +81,36 @@ export default class DiscussionPage extends Page {
);
}
onNewRoute() {
// If we have routed to the same discussion as we were viewing previously,
// prompt the post stream to jump to the new 'near' param.
// Otherwise, load in a new discussion. The `else` branch will
// be followed when `onNewRoute` is called from `oninit`.
const idParam = m.route.param('id');
if (this.discussion && 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 {
super.onNewRoute();
this.discussion = null;
this.load();
// If the discussion list has been loaded, then we'll enable the pane (and
// hide it by default).
if (app.discussions.hasDiscussions()) {
app.pane.enable();
app.pane.hide();
}
}
}
/**
* Load the discussion from the API or use the preloaded one.
*/
@@ -155,7 +173,7 @@ export default class DiscussionPage extends Page {
record.relationships.discussion.data.id === discussionId
)
.map((record) => app.store.getById('posts', record.id))
.sort((a, b) => a.createdAt() - b.createdAt())
.sort((a, b) => a.id() - b.id())
.slice(0, 20);
}
@@ -217,7 +235,10 @@ export default class DiscussionPage extends Page {
// replace it into the window's history and our own history stack.
const url = app.route.discussion(discussion, (this.near = startNumber));
this.prevRoute = url;
m.route.set(url, null, { replace: true });
window.history.replaceState(null, document.title, url);
app.history.push('discussion', discussion.title());
// If the user hasn't read past here before, then we'll update their read

View File

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

View File

@@ -29,6 +29,14 @@ export default class IndexPage extends Page {
this.lastDiscussion = app.previous.get('discussion');
}
app.history.push('index', app.translator.trans('core.forum.header.back_to_index_tooltip'));
this.bodyClass = 'App--index';
}
onNewRoute() {
super.onNewRoute();
// If the user is coming from the discussion list, then they have either
// just switched one of the parameters (filter, sort, search) or they
// probably want to refresh the results. We will clear the discussion list
@@ -39,10 +47,7 @@ export default class IndexPage extends Page {
app.discussions.refreshParams(app.search.params());
app.history.push('index', app.translator.trans('core.forum.header.back_to_index_tooltip'));
this.bodyClass = 'App--index';
this.scrollTopOnCreate = false;
this.setTitle();
}
view() {
@@ -86,22 +91,18 @@ export default class IndexPage extends Page {
$('#app').css('min-height', $(window).height() + heroHeight);
// Let browser handle scrolling on page reload.
if (app.previous.type == null) return;
// When on mobile, only retain scroll if we're coming from a discussion page.
// Otherwise, we've just changed the filter, so we should go to the top of the page.
if (app.screen() == 'desktop' || app.screen() == 'desktop-hd' || this.lastDiscussion) {
$(window).scrollTop(scrollTop - oldHeroHeight + heroHeight);
} else {
$(window).scrollTop(0);
}
// Scroll to the remembered position. We do this after a short delay so that
// it happens after the browser has done its own "back button" scrolling,
// which isn't right. https://github.com/flarum/core/issues/835
const scroll = () => $(window).scrollTop(scrollTop - oldHeroHeight + heroHeight);
scroll();
setTimeout(scroll, 1);
// If we've just returned from a discussion page, then the constructor will
// have set the `lastDiscussion` property. If this is the case, we want to
// scroll down to that discussion so that it's in view.
if (this.lastDiscussion) {
const $discussion = this.$(`li[data-id="${this.lastDiscussion.id()}"] .DiscussionListItem`);
const $discussion = this.$(`.DiscussionListItem[data-id="${this.lastDiscussion.id()}"]`);
if ($discussion.length) {
const indexTop = $('#header').outerHeight();
@@ -116,16 +117,14 @@ export default class IndexPage extends Page {
}
}
onbeforeremove() {
// Save the scroll position so we can restore it when we return to the
// discussion list.
app.cache.scrollTop = $(window).scrollTop();
}
onremove() {
super.onremove();
$('#app').css('min-height', '');
// Save the scroll position so we can restore it when we return to the
// discussion list.
app.cache.scrollTop = $(window).scrollTop();
}
/**

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.includes('://')}
external={href.indexOf('://') === -1}
onclick={this.markAsRead.bind(this)}
>
{!notification.isRead() &&

View File

@@ -32,13 +32,6 @@ 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 };
@@ -49,7 +42,6 @@ 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();
@@ -97,7 +89,7 @@ export default class PostStream extends Component {
// is not already doing so, then show a 'write a reply' placeholder.
if (viewingEnd && (!app.session.user || this.discussion.canReply())) {
items.push(
<div className="PostStream-item" key="reply" data-index={this.stream.count()} oncreate={postFadeIn}>
<div className="PostStream-item" key="reply">
{ReplyPlaceholder.component({ discussion: this.discussion })}
</div>
);
@@ -129,15 +121,16 @@ export default class PostStream extends Component {
* Start scrolling, if appropriate, to a newly-targeted post.
*/
triggerScroll() {
if (!this.stream.needsScroll) return;
if (!this.attrs.targetPost || !this.stream.needsScroll) return;
const target = this.stream.targetPost;
const newTarget = this.attrs.targetPost;
this.stream.needsScroll = false;
if ('number' in target) {
this.scrollToNumber(target.number, this.stream.animateScroll);
} else if ('index' in target) {
this.scrollToIndex(target.index, this.stream.animateScroll, target.reply);
if ('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);
}
}
@@ -188,9 +181,9 @@ export default class PostStream extends Component {
// seen if the browser were scrolled right up to the top of the page,
// and the viewport had a height of 0.
const $items = this.$('.PostStream-item[data-index]');
let index = $items.first().data('index') || 0;
let visible = 0;
let period = '';
let indexFromViewPort = null;
// Now loop through each of the items in the discussion. An 'item' is
// either a single post or a 'gap' of one or more posts that haven't
@@ -216,10 +209,8 @@ export default class PostStream extends Component {
const visibleBottom = Math.min(height, viewportTop + viewportHeight - top);
const visiblePost = visibleBottom - visibleTop;
// We take the index of the first item that passed the previous checks.
// It is the item that is first visible in the viewport.
if (indexFromViewPort === null) {
indexFromViewPort = parseFloat($this.data('index')) + visibleTop / height;
if (top <= viewportTop) {
index = parseFloat($this.data('index')) + visibleTop / height;
}
if (visiblePost > 0) {
@@ -232,10 +223,7 @@ export default class PostStream extends Component {
if (time) period = time;
});
// If indexFromViewPort is null, it means no posts are visible in the
// viewport. This can happen, when drafting a long reply post. In that case
// set the index to the last post.
this.stream.index = indexFromViewPort !== null ? indexFromViewPort + 1 : this.stream.count();
this.stream.index = index + 1;
this.stream.visible = visible;
if (period) this.stream.description = dayjs(period).format('MMMM YYYY');
}
@@ -308,17 +296,18 @@ export default class PostStream extends Component {
*
* @param {Integer} index
* @param {Boolean} animate
* @param {Boolean} reply Whether or not to scroll to the reply placeholder.
* @param {Boolean} bottom Whether or not to scroll to the bottom of the post
* at the given index, instead of the top of it.
* @return {jQuery.Deferred}
*/
scrollToIndex(index, animate, reply) {
const $item = reply ? $('.PostStream-item:last-child') : this.$(`.PostStream-item[data-index=${index}]`);
scrollToIndex(index, animate, bottom) {
const $item = this.$(`.PostStream-item[data-index=${index}]`);
this.scrollToItem($item, animate, true, reply);
if (reply) {
this.flashItem($item);
}
return this.scrollToItem($item, animate, true, bottom).then(() => {
if (index == this.stream.count() - 1) {
this.flashItem(this.$('.PostStream-item:last-child'));
}
});
}
/**
@@ -328,10 +317,11 @@ export default class PostStream extends Component {
* @param {Boolean} animate
* @param {Boolean} force Whether or not to force scrolling to the item, even
* if it is already in the viewport.
* @param {Boolean} reply Whether or not to scroll to the reply placeholder.
* @param {Boolean} bottom Whether or not to scroll to the bottom of the post
* at the given index, instead of the top of it.
* @return {jQuery.Deferred}
*/
scrollToItem($item, animate, force, reply) {
scrollToItem($item, animate, force, bottom) {
const $container = $('html, body').stop(true);
const index = $item.data('index');
@@ -342,10 +332,10 @@ export default class PostStream extends Component {
const scrollBottom = scrollTop + $(window).height();
// If the item is already in the viewport, we may not need to scroll.
// If we're scrolling to the reply placeholder, we'll make sure its
// If we're scrolling to the bottom of an item, then we'll make sure the
// bottom will line up with the top of the composer.
if (force || itemTop < scrollTop || itemBottom > scrollBottom) {
const top = reply ? itemBottom - $(window).height() + app.composer.computedHeight() : $item.is(':first-child') ? 0 : itemTop;
const top = bottom ? itemBottom - $(window).height() + app.composer.computedHeight() : $item.is(':first-child') ? 0 : itemTop;
if (!animate) {
$container.scrollTop(top);
@@ -359,7 +349,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();
if (index !== undefined) this.stream.index = index + 1;
this.stream.index = index;
};
// If we don't update this before the scroll, the scrubber will start
@@ -369,29 +359,24 @@ export default class PostStream extends Component {
return Promise.all([$container.promise(), this.stream.loadPromise]).then(() => {
m.redraw.sync();
// Rendering post contents will probably throw off our position.
// To counter this, we'll scroll either:
// - To the reply placeholder (aligned with composer top)
// - To the top of the page if we're on the first post
// - To the top of a post (if that post exists)
// If the post does not currently exist, it's probably
// outside of the range we loaded in, so we won't adjust anything,
// as it will soon be rendered by the "load more" system.
let itemOffset;
if (reply) {
const $placeholder = $('.PostStream-item:last-child');
$(window).scrollTop($placeholder.offset().top + $placeholder.height() - $(window).height() + app.composer.computedHeight());
} else if (index === 0) {
$(window).scrollTop(0);
} else if ((itemOffset = $(`.PostStream-item[data-index=${index}]`).offset())) {
$(window).scrollTop(itemOffset.top - this.getMarginTop());
}
// We want to adjust this again after posts have been loaded in
// and position adjusted so that the scrubber's height is accurate.
// 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) {
$(window).scrollTop(0);
} else if (offset) {
$(window).scrollTop($(`.PostStream-item[data-index=${index}]`).offset().top - this.getMarginTop());
}
this.calculatePosition();
this.stream.paused = false;
});
@@ -403,11 +388,10 @@ export default class PostStream extends Component {
* @param {jQuery} $item
*/
flashItem($item) {
// This might execute before the fadeIn class has been removed in PostStreamItem's
// oncreate, so we remove it just to be safe and avoid a double animation.
$item.removeClass('fadeIn');
$item.addClass('flash').on('animationend webkitAnimationEnd', (e) => {
$item.removeClass('flash');
if (e.animationName === 'fadeIn') {
$item.removeClass('flash');
}
});
}
}

View File

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

View File

@@ -29,6 +29,14 @@ export default class UserPage extends Page {
this.bodyClass = 'App--user';
}
onNewRoute() {
super.onNewRoute();
if (m.route.param('username')) {
this.loadUser(m.route.param('username'));
}
}
view() {
return (
<div className="UserPage">
@@ -135,7 +143,7 @@ export default class UserPage extends Page {
items.add(
'posts',
<LinkButton href={app.route('user.posts', { username: user.username() })} icon="far fa-comment">
<LinkButton href={app.route('user.posts', { username: user.username() })} force icon="far fa-comment">
{app.translator.trans('core.forum.user.posts_link')} <span className="Button-badge">{user.commentCount()}</span>
</LinkButton>,
100
@@ -143,7 +151,7 @@ export default class UserPage extends Page {
items.add(
'discussions',
<LinkButton href={app.route('user.discussions', { username: user.username() })} icon="fas fa-bars">
<LinkButton href={app.route('user.discussions', { username: user.username() })} force icon="fas fa-bars">
{app.translator.trans('core.forum.user.discussions_link')} <span className="Button-badge">{user.discussionCount()}</span>
</LinkButton>,
90

View File

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

View File

@@ -1,49 +0,0 @@
import DefaultResolver from '../../common/resolvers/DefaultResolver';
import DiscussionPage from '../components/DiscussionPage';
/**
* This isn't exported as it is a temporary measure.
* A more robust system will be implemented alongside UTF-8 support in beta 15.
*/
function getDiscussionIdFromSlug(slug: string | undefined) {
if (!slug) return;
return slug.split('-')[0];
}
/**
* A custom route resolver for DiscussionPage that generates the same key to all posts
* on the same discussion. It triggers a scroll when going from one post to another
* in the same discussion.
*/
export default class DiscussionPageResolver extends DefaultResolver {
static scrollToPostNumber: string | null = null;
makeKey() {
const params = { ...m.route.param() };
if ('near' in params) {
delete params.near;
}
params.id = getDiscussionIdFromSlug(params.id);
return this.routeName.replace('.near', '') + JSON.stringify(params);
}
onmatch(args, requestedPath, route) {
if (app.current.matches(DiscussionPage) && getDiscussionIdFromSlug(args.id) === getDiscussionIdFromSlug(m.route.param('id'))) {
// By default, the first post number of any discussion is 1
DiscussionPageResolver.scrollToPostNumber = args.near || '1';
}
return super.onmatch(args, requestedPath, route);
}
render(vnode) {
if (DiscussionPageResolver.scrollToPostNumber !== null) {
const number = DiscussionPageResolver.scrollToPostNumber;
// Scroll after a timeout to avoid clashes with the render.
setTimeout(() => app.current.get('stream').goToNumber(number));
DiscussionPageResolver.scrollToPostNumber = null;
}
return super.render(vnode);
}
}

View File

@@ -4,7 +4,6 @@ import PostsUserPage from './components/PostsUserPage';
import DiscussionsUserPage from './components/DiscussionsUserPage';
import SettingsPage from './components/SettingsPage';
import NotificationsPage from './components/NotificationsPage';
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
/**
* The `routes` initializer defines the forum app's routes.
@@ -15,8 +14,8 @@ export default function (app) {
app.routes = {
index: { path: '/all', component: IndexPage },
discussion: { path: '/d/:id', component: DiscussionPage, resolverClass: DiscussionPageResolver },
'discussion.near': { path: '/d/:id/:near', component: DiscussionPage, resolverClass: DiscussionPageResolver },
discussion: { path: '/d/:id', component: DiscussionPage },
'discussion.near': { path: '/d/:id/:near', component: DiscussionPage },
user: { path: '/u/:username', component: PostsUserPage },
'user.posts': { path: '/u/:username', component: PostsUserPage },

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, { search: params.filter.q });
return this.app.store.find('discussions', params);
}
/**

View File

@@ -96,9 +96,7 @@ class PostStreamState {
// If we want to go to the reply preview, then we will go to the end of the
// discussion and then scroll to the very bottom of the page.
if (number === 'reply') {
const resultPromise = this.goToLast();
this.targetPost.reply = true;
return resultPromise;
return this.goToLast();
}
this.paused = true;
@@ -282,13 +280,7 @@ class PostStreamState {
}
});
if (loadIds.length) {
return app.store.find('posts', loadIds).then((newPosts) => {
return loaded.concat(newPosts).sort((a, b) => a.createdAt() - b.createdAt());
});
}
return Promise.resolve(loaded);
return loadIds.length ? app.store.find('posts', loadIds) : Promise.resolve(loaded);
}
/**

View File

@@ -236,16 +236,12 @@
.App-header {
padding: 8px;
height: @header-height;
position: absolute;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: @zindex-header;
.affix & {
position: fixed;
}
& when (@config-colored-header = true) {
.light-contents(@header-color, @header-control-bg, @header-control-color);
}

View File

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

View File

@@ -6,7 +6,18 @@
margin-top: 10px;
}
}
@-webkit-keyframes fadeIn {
0% {opacity: 0}
100% {opacity: 1}
}
@keyframes fadeIn {
0% {opacity: 0}
100% {opacity: 1}
}
.PostStream-item {
.animation(fadeIn 0.4s ease-in-out);
&:not(:last-child) {
border-bottom: 1px solid @control-bg;
@@ -93,16 +104,3 @@
.animation(pulsate 0.2s ease-in-out);
.animation-iteration-count(1);
}
@-webkit-keyframes fadeIn {
0% {opacity: 0}
100% {opacity: 1}
}
@keyframes fadeIn {
0% {opacity: 0}
100% {opacity: 1}
}
.fadeIn {
.animation(fadeIn 0.4s ease-in-out);
.animation-iteration-count(1);
}

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -1,112 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class SearchDiscussionsController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = DiscussionSerializer::class;
/**
* {@inheritdoc}
*/
public $include = [
'user',
'lastPostedUser',
'mostRelevantPost',
'mostRelevantPost.user'
];
/**
* {@inheritdoc}
*/
public $optionalInclude = [
'firstPost',
'lastPost'
];
/**
* {@inheritdoc}
*/
public $sortFields = ['lastPostedAt', 'commentCount', 'createdAt'];
/**
* @var DiscussionSearcher
*/
protected $searcher;
/**
* @var UrlGenerator
*/
protected $url;
/**
* @param DiscussionSearcher $searcher
* @param UrlGenerator $url
*/
public function __construct(DiscussionSearcher $searcher, UrlGenerator $url)
{
$this->searcher = $searcher;
$this->url = $url;
}
/**
* {@inheritdoc}
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$criteria = new SearchCriteria($actor, $query, $sort);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = array_merge($this->extractInclude($request), ['state']);
$results = $this->searcher->search($criteria, $limit, $offset);
$document->addPaginationLinks(
$this->url->to('api')->route('discussions.index'),
$request->getQueryParams(),
$offset,
$limit,
$results->areMoreResults() ? null : 0
);
Discussion::setStateUser($actor);
$results = $results->getResults()->load($load);
if ($relations = array_intersect($load, ['firstPost', 'lastPost'])) {
foreach ($results as $discussion) {
foreach ($relations as $relation) {
if ($discussion->$relation) {
$discussion->$relation->discussion = $discussion;
}
}
}
}
return $results;
}
}

View File

@@ -1,93 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Flarum\User\Search\UserSearcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class SearchUsersController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = UserSerializer::class;
/**
* {@inheritdoc}
*/
public $include = ['groups'];
/**
* {@inheritdoc}
*/
public $sortFields = [
'username',
'commentCount',
'discussionCount',
'lastSeenAt',
'joinedAt'
];
/**
* @var UserSearcher
*/
protected $searcher;
/**
* @var UrlGenerator
*/
protected $url;
/**
* @param UserSearcher $searcher
* @param UrlGenerator $url
*/
public function __construct(UserSearcher $searcher, UrlGenerator $url)
{
$this->searcher = $searcher;
$this->url = $url;
}
/**
* {@inheritdoc}
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$actor->assertCan('viewUserList');
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$criteria = new SearchCriteria($actor, $query, $sort);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = $this->extractInclude($request);
$results = $this->searcher->search($criteria, $limit, $offset, $load);
$document->addPaginationLinks(
$this->url->to('api')->route('users.index'),
$request->getQueryParams(),
$offset,
$limit,
$results->areMoreResults() ? null : 0
);
return $results->getResults();
}
}

View File

@@ -95,13 +95,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toController(Controller\SendConfirmationEmailController::class)
);
// List users
$map->get(
'/search/users',
'users.search',
$route->toController(Controller\SearchUsersController::class)
);
/*
|--------------------------------------------------------------------------
| Notifications
@@ -170,13 +163,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toController(Controller\DeleteDiscussionController::class)
);
// Search discussions
$map->get(
'/search/discussions',
'discussions.search',
$route->toController(Controller\SearchDiscussionsController::class)
);
/*
|--------------------------------------------------------------------------
| Posts

View File

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

View File

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

View File

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

View File

@@ -14,28 +14,11 @@ use Illuminate\Contracts\Container\Container;
class Csrf implements ExtenderInterface
{
protected $csrfExemptRoutes = [];
protected $csrfExemptPaths = [];
/**
* Exempt a named route from CSRF checks.
*
* @param string $routeName
*/
public function exemptRoute(string $routeName)
{
$this->csrfExemptRoutes[] = $routeName;
return $this;
}
/**
* Exempt a path from csrf checks. Wildcards are supported.
*
* @deprecated beta 15, remove beta 16. Exempt routes should be used instead.
*/
public function exemptPath(string $path)
{
$this->csrfExemptRoutes[] = $path;
$this->csrfExemptPaths[] = $path;
return $this;
}
@@ -43,7 +26,7 @@ class Csrf implements ExtenderInterface
public function extend(Container $container, Extension $extension = null)
{
$container->extend('flarum.http.csrfExemptPaths', function ($existingExemptPaths) {
return array_merge($existingExemptPaths, $this->csrfExemptRoutes);
return array_merge($existingExemptPaths, $this->csrfExemptPaths);
});
}
}

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|string $listener
* @param callable $listener
*/
public function listen(string $event, $listener)
{

View File

@@ -1,63 +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\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,93 +10,38 @@
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 $configurationCallbacks = [];
private $parsingCallbacks = [];
private $renderingCallbacks = [];
private $callback;
/**
* 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->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;
$this->callback = $callback;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
$container->extend('flarum.formatter', function ($formatter, $container) {
foreach ($this->configurationCallbacks as $callback) {
$formatter->addConfigurationCallback(ContainerUtil::wrapCallback($callback, $container));
}
$events = $container->make(Dispatcher::class);
foreach ($this->parsingCallbacks as $callback) {
$formatter->addParsingCallback(ContainerUtil::wrapCallback($callback, $container));
}
$events->listen(
Configuring::class,
function (Configuring $event) use ($container) {
if (is_string($this->callback)) {
$callback = $container->make($this->callback);
} else {
$callback = $this->callback;
}
foreach ($this->renderingCallbacks as $callback) {
$formatter->addRenderingCallback(ContainerUtil::wrapCallback($callback, $container));
$callback($event->configurator);
}
return $formatter;
});
);
}
public function onEnable(Container $container, Extension $extension)

View File

@@ -12,7 +12,6 @@ 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;
@@ -172,7 +171,11 @@ class Frontend implements ExtenderInterface
"flarum.frontend.$this->frontend",
function (ActualFrontend $frontend, Container $container) {
foreach ($this->content as $content) {
$frontend->content(ContainerUtil::wrapCallback($content, $container));
if (is_string($content)) {
$content = $container->make($content);
}
$frontend->content($content);
}
}
);

View File

@@ -11,14 +11,12 @@ 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.
@@ -50,9 +48,7 @@ class Model implements ExtenderInterface
}
/**
* 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.
* Add a default value for a given attribute, which can be an explicit value, or a closure.
*
* @param string $attribute
* @param mixed $value
@@ -161,7 +157,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|string $callback
* @param callable $callable
*
* The callable can be a closure or invokable class, and should accept:
* - $instance: An instance of this model.
@@ -172,17 +168,15 @@ class Model implements ExtenderInterface
*
* @return self
*/
public function relationship(string $name, $callback)
public function relationship(string $name, callable $callable)
{
$this->customRelations[$name] = $callback;
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", $callable);
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
foreach ($this->customRelations as $name => $callback) {
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", ContainerUtil::wrapCallback($callback, $container));
}
// Nothing needed here.
}
}

View File

@@ -1,78 +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\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);
});
}
}

View File

@@ -1,39 +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\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

@@ -1,40 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\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|string $callback
* @param callable $callable
*
* 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($callback)
public function permissionGroups(callable $callable)
{
$this->groupProcessors[] = $callback;
$this->groupProcessors[] = $callable;
return $this;
}

View File

@@ -1,55 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\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

@@ -1,26 +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\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);
}

View File

@@ -1,88 +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\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

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

View File

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

View File

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

View File

@@ -12,9 +12,6 @@ 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,12 +20,6 @@ use s9e\TextFormatter\Unparser;
class Formatter
{
protected $configurationCallbacks = [];
protected $parsingCallbacks = [];
protected $renderingCallbacks = [];
/**
* @var Repository
*/
@@ -53,21 +47,6 @@ 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.
*
@@ -79,13 +58,8 @@ 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);
}
@@ -101,13 +75,8 @@ 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);
}
@@ -153,13 +122,8 @@ 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,7 +11,6 @@ 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;
@@ -115,6 +114,6 @@ class Index
*/
private function getApiDocument(User $actor, array $params)
{
return json_decode($this->api->send(($params['filter']['q'] ? SearchDiscussionsController::class : ListDiscussionsController::class), $actor, $params)->getBody());
return json_decode($this->api->send(ListDiscussionsController::class, $actor, $params)->getBody());
}
}

View File

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

View File

@@ -18,16 +18,6 @@ use Symfony\Component\Translation\TranslatorInterface;
abstract class AbstractValidator
{
/**
* @var array
*/
protected $configuration = [];
public function addConfiguration($callable)
{
$this->configuration[] = $callable;
}
/**
* @var array
*/
@@ -102,17 +92,10 @@ 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';
const VERSION = '0.1.0-beta.14-dev';
/**
* The IoC container for the Flarum application.

View File

@@ -1,36 +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\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,7 +13,6 @@ 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 as HttpMiddleware;
use Flarum\Http\Middleware\DispatchRoute;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Console\Command;
use Illuminate\Contracts\Container\Container;
@@ -85,9 +85,8 @@ class InstalledApp implements AppInterface
$pipe = new MiddlewarePipe;
$pipe->pipe(new BasePath($this->basePath()));
$pipe->pipe(
new HttpMiddleware\ResolveRoute($this->container->make('flarum.update.routes'))
new DispatchRoute($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 ['token'];
return ['/api/token'];
});
$this->app->bind(Middleware\CheckCsrfToken::class, function ($app) {

View File

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

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 ResolveRoute implements Middleware
class DispatchRoute implements Middleware
{
/**
* @var RouteCollection
@@ -41,7 +41,7 @@ class ResolveRoute implements Middleware
}
/**
* Resolve the given request from our route collection.
* Dispatch the given request to our route collection.
*
* @throws MethodNotAllowedException
* @throws RouteNotFoundException
@@ -59,12 +59,10 @@ class ResolveRoute implements Middleware
case Dispatcher::METHOD_NOT_ALLOWED:
throw new MethodNotAllowedException($method);
case Dispatcher::FOUND:
$request = $request
->withAttribute('routeName', $routeInfo[1]['name'])
->withAttribute('routeHandler', $routeInfo[1]['handler'])
->withAttribute('routeParameters', $routeInfo[2]);
$handler = $routeInfo[1];
$parameters = $routeInfo[2];
return $handler->handle($request);
return $handler($request, $parameters);
}
}

View File

@@ -1,29 +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\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

@@ -1,57 +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\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, ['name' => $name, 'handler' => $handler]);
$this->dataGenerator->addRoute($method, $routeData, $handler);
}
$this->reverse[$name] = $routeDatas;

View File

@@ -13,7 +13,9 @@ use Flarum\Foundation\AppInterface;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\ErrorHandling\WhoopsFormatter;
use Flarum\Http\Middleware as HttpMiddleware;
use Flarum\Http\Middleware\DispatchRoute;
use Flarum\Http\Middleware\HandleErrors;
use Flarum\Http\Middleware\StartSession;
use Flarum\Install\Console\InstallCommand;
use Illuminate\Contracts\Container\Container;
use Laminas\Stratigility\MiddlewarePipe;
@@ -36,16 +38,15 @@ class Installer implements AppInterface
public function getRequestHandler()
{
$pipe = new MiddlewarePipe;
$pipe->pipe(new HttpMiddleware\HandleErrors(
$pipe->pipe(new HandleErrors(
$this->container->make(Registry::class),
$this->container->make(WhoopsFormatter::class),
$this->container->tagged(Reporter::class)
));
$pipe->pipe($this->container->make(HttpMiddleware\StartSession::class));
$pipe->pipe($this->container->make(StartSession::class));
$pipe->pipe(
new HttpMiddleware\ResolveRoute($this->container->make('flarum.install.routes'))
new DispatchRoute($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 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.'
'detail' => 'Please chmod this directory'.(in_array($index, $this->wildcards) ? ' and its contents' : '').' to 0775.'
];
});
}

View File

@@ -1,50 +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\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

@@ -1,69 +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\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

@@ -1,34 +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\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,9 +11,6 @@ namespace Flarum\Notification\Event;
use Flarum\Notification\Blueprint\BlueprintInterface;
/**
* @deprecated in beta 15, removed in beta 16
*/
class Sending
{
/**

View File

@@ -10,6 +10,7 @@
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;
@@ -34,6 +35,8 @@ class SendNotificationsJob extends AbstractJob
public function handle()
{
event(new Sending($this->blueprint, $this->recipients));
Notification::notify($this->recipients, $this->blueprint);
}
}

View File

@@ -12,76 +12,51 @@ 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->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));
}
$this->registerNotificationTypes();
}
/**
* Register notification types.
*/
protected function setNotificationTypes()
public function registerNotificationTypes()
{
$blueprints = $this->app->make('flarum.notification.blueprints');
$blueprints = [
DiscussionRenamedBlueprint::class => ['alert']
];
// Deprecated in beta 15, remove in beta 16
$this->app->make('events')->dispatch(
new ConfigureNotificationTypes($blueprints)
);
foreach ($blueprints as $blueprint => $driversEnabledByDefault) {
$this->addType($blueprint, $driversEnabledByDefault);
}
}
protected function addType(string $blueprint, array $driversEnabledByDefault)
{
Notification::setSubjectModel(
$type = $blueprint::getType(),
$blueprint::getSubjectModel()
);
foreach (NotificationSyncer::getNotificationDrivers() as $driverName => $driver) {
$driver->registerType(
$blueprint,
$driversEnabledByDefault
foreach ($blueprints as $blueprint => $enabled) {
Notification::setSubjectModel(
$type = $blueprint::getType(),
$blueprint::getSubjectModel()
);
User::addPreference(
User::getNotificationPreferenceKey($type, 'alert'),
'boolval',
in_array('alert', $enabled)
);
if ((new ReflectionClass($blueprint))->implementsInterface(MailableInterface::class)) {
User::addPreference(
User::getNotificationPreferenceKey($type, 'email'),
'boolval',
in_array('email', $enabled)
);
}
}
}
}

View File

@@ -10,9 +10,10 @@
namespace Flarum\Notification;
use Flarum\Notification\Blueprint\BlueprintInterface;
use Flarum\Notification\Driver\NotificationDriverInterface;
use Flarum\Notification\Event\Sending;
use Flarum\Notification\Job\SendEmailNotificationJob;
use Flarum\Notification\Job\SendNotificationsJob;
use Flarum\User\User;
use Illuminate\Contracts\Queue\Queue;
/**
* The Notification Syncer commits notification blueprints to the database, and
@@ -37,11 +38,14 @@ class NotificationSyncer
protected static $sentTo = [];
/**
* A map of notification drivers.
*
* @var NotificationDriverInterface[]
* @var Queue
*/
protected static $notificationDrivers = [];
protected $queue;
public function __construct(Queue $queue)
{
$this->queue = $queue;
}
/**
* Sync a notification so that it is visible to the specified users, and not
@@ -98,13 +102,12 @@ 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.
foreach (static::getNotificationDrivers() as $driverName => $driver) {
$driver->send($blueprint, $newRecipients);
if (count($newRecipients)) {
$this->queue->push(new SendNotificationsJob($blueprint, $newRecipients));
}
if (count($newRecipients)) {
// Deprecated in beta 15, removed in beta 16
event(new Sending($blueprint, $newRecipients));
if ($blueprint instanceof MailableInterface) {
$this->mailNotifications($blueprint, $newRecipients);
}
}
@@ -147,6 +150,21 @@ 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.
*
@@ -157,23 +175,4 @@ 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(string $type, string $model)
public static function setModel($type, $model)
{
static::$models[$type] = $model;
}

View File

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

View File

@@ -1,27 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\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

@@ -0,0 +1,61 @@
<?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,7 +22,6 @@ 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;
@@ -35,7 +34,7 @@ class QueueServiceProvider extends AbstractServiceProvider
Commands\ListFailedCommand::class,
Commands\RestartCommand::class,
Commands\RetryCommand::class,
Console\WorkCommand::class,
Commands\WorkCommand::class,
];
public function register()
@@ -62,16 +61,10 @@ class QueueServiceProvider extends AbstractServiceProvider
});
$this->app->singleton(Worker::class, function ($app) {
/** @var Config $config */
$config = $app->make(Config::class);
return new Worker(
new QueueManager($app),
new HackyManagerForWorker($app[Factory::class]),
$app['events'],
$app[ExceptionHandling::class],
function () use ($config) {
return $config->inMaintenanceMode();
}
$app[ExceptionHandling::class]
);
});
@@ -117,17 +110,17 @@ class QueueServiceProvider extends AbstractServiceProvider
protected function registerCommands()
{
$this->app->extend('flarum.console.commands', function ($commands) {
$this->app['events']->listen(Configuring::class, function (Configuring $event) {
$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 $commands;
return;
}
// Otherwise add our commands, while allowing them to be overridden by those
// already added through the container.
return array_merge($this->commands, $commands);
foreach ($this->commands as $command) {
$event->addCommand($command);
}
});
}

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