mirror of
https://github.com/flarum/core.git
synced 2025-08-16 05:14:20 +02:00
Compare commits
4 Commits
as/comment
...
v0.1.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
fd371c1203 | ||
|
d72416cd8a | ||
|
937ff1a0d5 | ||
|
097a87dbb6 |
48
CHANGELOG.md
48
CHANGELOG.md
@@ -3,55 +3,47 @@
|
||||
## [0.1.0-beta.13](https://github.com/flarum/core/compare/v0.1.0-beta.12...v0.1.0-beta.13)
|
||||
|
||||
### Added
|
||||
- Middleware extender (#2017, #2063, #2084)
|
||||
- Console extender (#2057)
|
||||
- CSRF extender (#2095)
|
||||
- Event extender (#2097)
|
||||
- Mail extender (#2012)
|
||||
- Model extender (#2100)
|
||||
- Posts by users that started a discussion now have the CSS class `.Post--by-start-user`
|
||||
- PHPUnit 8 compatibility
|
||||
- Show discussion start user as html class on post
|
||||
- PHPUnit 8 compatibility.
|
||||
- Composer 2 compatibility
|
||||
- Permission groups can now be hidden (#2129)
|
||||
- Confirmation popup when hiding or deleting posts (#2135)
|
||||
|
||||
### Changed
|
||||
- Updated less.php dependency version to 3.0
|
||||
- Updated less.php dependency version to 3.0.
|
||||
- All notifications now processed through the queue (#1931)
|
||||
- Updated JS dependencies
|
||||
- All notifications and other emails now processed through the queue, if enabled (#978, #1928, #1931, #2096)
|
||||
- Simplified uploads, removing need to store intermediate files (#2117)
|
||||
- Improved date handling for dates older than 1 year (#2034)
|
||||
- Linting and automatic formatting for JS (#2099)
|
||||
- Translation files from Language Packs are only loaded for extensions that are enabled (#2020)
|
||||
- PHP extenders' properties are now `private` instead of `protected`, intentionally making it harder to extend these classes (#1958)
|
||||
- Preparation for upgrading Laravel components to 5.8 and then 6.0 (#2055, #2117)
|
||||
- Allowed permission checks based on model classes in addition to instances (#1977)
|
||||
|
||||
### Fixed
|
||||
- Users can no longer restore discussions hidden by admins (#2037)
|
||||
- Issues of the Modal not showing or auto hiding (#1504, #1813, #2080)
|
||||
- Columnar layout on admin extensions page was broken in Firefox (#2029, #2111)
|
||||
- Non-dismissible modals could still be dismissed using the ESC key (#1917)
|
||||
- New discussions were added to the discussion list above unread sticky posts (#1751, #1868)
|
||||
- New discussions not visible to users when using Pusher (#2076, #2077)
|
||||
- Permission icons were aligned unevenly in admin permissions list (#2016, #2018)
|
||||
- Notification bubble not inversed on mobile with colored header (#1983, #2109)
|
||||
- Post stream scrubber clicks jumped back to first post (#1945)
|
||||
- Loading state of Switch toggle component was hard to see (#2039, #1491)
|
||||
- `Flarum\Extend\Middleware`: The methods `insertBefore()` and `insertAfter()` did not work as described (#2063, #2084)
|
||||
- Users can no longer restore discussions hidden by others (#2037)
|
||||
- Issues of the Modal not showing or auto hiding (#2080)
|
||||
- Extensions page in admin showning columns incorrectly (#2111)
|
||||
- Non dismissable modals can be dismissed using the ESC key (#1917)
|
||||
- New post injected above unread sticky (#1868)
|
||||
- New discussions not visible to users when using Pusher (#2077)
|
||||
- Icons on admin permissions page (#2016, #2018)
|
||||
- Notification bubble contrast on mobile with colored header (#2109)
|
||||
- PostStreamScrubber click jumps back to first position (#1945)
|
||||
- Loading state of Switch toggle component is hard to see (#2039, #1491)
|
||||
- Allowing permission check to use class name based gate checks (#1977)
|
||||
|
||||
### Removed
|
||||
- Support for PHP 7.1 (#2014)
|
||||
- Zend compatibility bridge (#2010)
|
||||
- SES mail support (#2011)
|
||||
- Backward compatibility layer for `Flarum\Mail\DriverInterface`, new methods from beta.12 are now required
|
||||
- `Flarum\Util\Str` helper class
|
||||
- `Flarum\Event\ConfigureMiddleware` event
|
||||
|
||||
### Deprecated
|
||||
- `Flarum\Event\AbstractConfigureRoutes` event class
|
||||
- `Flarum\Event\ConfigureApiRoutes` event class
|
||||
- `Flarum\Event\ConfigureForumRoutes` event class
|
||||
- `Flarum\Event\ConfigureLocales` event class
|
||||
- Backward compatibility dropped for mail drivers
|
||||
- Support for PHP 7.1
|
||||
- Deprecated Flarum\Util\Str helper class
|
||||
- Deprecated ConfigureMiddleware event
|
||||
|
||||
## [0.1.0-beta.12](https://github.com/flarum/core/compare/v0.1.0-beta.11.1...v0.1.0-beta.12)
|
||||
|
||||
|
@@ -38,24 +38,24 @@
|
||||
"php": ">=7.2",
|
||||
"axy/sourcemap": "^0.1.4",
|
||||
"components/font-awesome": "5.9.*",
|
||||
"dflydev/fig-cookies": "^2.0.1",
|
||||
"dflydev/fig-cookies": "^1.0.2",
|
||||
"doctrine/dbal": "^2.7",
|
||||
"franzl/whoops-middleware": "^0.4.0",
|
||||
"illuminate/bus": "5.8.*",
|
||||
"illuminate/cache": "5.8.*",
|
||||
"illuminate/config": "5.8.*",
|
||||
"illuminate/container": "5.8.*",
|
||||
"illuminate/contracts": "5.8.*",
|
||||
"illuminate/database": "5.8.*",
|
||||
"illuminate/events": "5.8.*",
|
||||
"illuminate/filesystem": "5.8.*",
|
||||
"illuminate/hashing": "5.8.*",
|
||||
"illuminate/mail": "5.8.*",
|
||||
"illuminate/queue": "5.8.*",
|
||||
"illuminate/session": "5.8.*",
|
||||
"illuminate/support": "5.8.*",
|
||||
"illuminate/validation": "5.8.*",
|
||||
"illuminate/view": "5.8.*",
|
||||
"illuminate/bus": "5.7.*",
|
||||
"illuminate/cache": "5.7.*",
|
||||
"illuminate/config": "5.7.*",
|
||||
"illuminate/container": "5.7.*",
|
||||
"illuminate/contracts": "5.7.*",
|
||||
"illuminate/database": "5.7.*",
|
||||
"illuminate/events": "5.7.*",
|
||||
"illuminate/filesystem": "5.7.*",
|
||||
"illuminate/hashing": "5.7.*",
|
||||
"illuminate/mail": "5.7.*",
|
||||
"illuminate/queue": "5.7.*",
|
||||
"illuminate/session": "5.7.*",
|
||||
"illuminate/support": "5.7.*",
|
||||
"illuminate/validation": "5.7.*",
|
||||
"illuminate/view": "5.7.*",
|
||||
"intervention/image": "^2.5.0",
|
||||
"laminas/laminas-diactoros": "^1.8.4",
|
||||
"laminas/laminas-httphandlerrunner": "^1.0",
|
||||
|
4
js/dist/admin.js
vendored
4
js/dist/admin.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/admin.js.map
vendored
2
js/dist/admin.js.map
vendored
File diff suppressed because one or more lines are too long
8
js/dist/forum.js
vendored
8
js/dist/forum.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/forum.js.map
vendored
2
js/dist/forum.js.map
vendored
File diff suppressed because one or more lines are too long
827
js/package-lock.json
generated
827
js/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,8 +15,8 @@
|
||||
"moment": "^2.22.2",
|
||||
"punycode": "^2.1.1",
|
||||
"spin.js": "^3.1.0",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-merge": "^4.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -6,6 +6,7 @@ import EditCustomFooterModal from './components/EditCustomFooterModal';
|
||||
import SessionDropdown from './components/SessionDropdown';
|
||||
import HeaderPrimary from './components/HeaderPrimary';
|
||||
import AppearancePage from './components/AppearancePage';
|
||||
import Page from './components/Page';
|
||||
import StatusWidget from './components/StatusWidget';
|
||||
import HeaderSecondary from './components/HeaderSecondary';
|
||||
import SettingsModal from './components/SettingsModal';
|
||||
@@ -14,6 +15,7 @@ import AddExtensionModal from './components/AddExtensionModal';
|
||||
import ExtensionsPage from './components/ExtensionsPage';
|
||||
import AdminLinkButton from './components/AdminLinkButton';
|
||||
import PermissionGrid from './components/PermissionGrid';
|
||||
import Widget from './components/Widget';
|
||||
import MailPage from './components/MailPage';
|
||||
import UploadImageButton from './components/UploadImageButton';
|
||||
import LoadingModal from './components/LoadingModal';
|
||||
@@ -35,6 +37,7 @@ export default Object.assign(compat, {
|
||||
'components/SessionDropdown': SessionDropdown,
|
||||
'components/HeaderPrimary': HeaderPrimary,
|
||||
'components/AppearancePage': AppearancePage,
|
||||
'components/Page': Page,
|
||||
'components/StatusWidget': StatusWidget,
|
||||
'components/HeaderSecondary': HeaderSecondary,
|
||||
'components/SettingsModal': SettingsModal,
|
||||
@@ -43,6 +46,7 @@ export default Object.assign(compat, {
|
||||
'components/ExtensionsPage': ExtensionsPage,
|
||||
'components/AdminLinkButton': AdminLinkButton,
|
||||
'components/PermissionGrid': PermissionGrid,
|
||||
'components/Widget': Widget,
|
||||
'components/MailPage': MailPage,
|
||||
'components/UploadImageButton': UploadImageButton,
|
||||
'components/LoadingModal': LoadingModal,
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Page from '../../common/components/Page';
|
||||
import Page from './Page';
|
||||
import Button from '../../common/components/Button';
|
||||
import Switch from '../../common/components/Switch';
|
||||
import EditCustomCssModal from './EditCustomCssModal';
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Page from '../../common/components/Page';
|
||||
import Page from './Page';
|
||||
import FieldSet from '../../common/components/FieldSet';
|
||||
import Select from '../../common/components/Select';
|
||||
import Button from '../../common/components/Button';
|
||||
@@ -21,7 +21,6 @@ export default class BasicsPage extends Page {
|
||||
'default_route',
|
||||
'welcome_title',
|
||||
'welcome_message',
|
||||
'display_name_driver',
|
||||
];
|
||||
this.values = {};
|
||||
|
||||
@@ -34,14 +33,6 @@ export default class BasicsPage extends Page {
|
||||
this.localeOptions[i] = `${locales[i]} (${i})`;
|
||||
}
|
||||
|
||||
this.displayNameOptions = {};
|
||||
const displayNameDrivers = app.data.displayNameDrivers;
|
||||
displayNameDrivers.forEach(function (identifier) {
|
||||
this.displayNameOptions[identifier] = identifier;
|
||||
}, this);
|
||||
|
||||
if (!this.values.display_name_driver() && displayNameDrivers.includes('username')) this.values.display_name_driver('username');
|
||||
|
||||
if (typeof this.values.show_language_selector() !== 'number') this.values.show_language_selector(1);
|
||||
}
|
||||
|
||||
@@ -123,20 +114,6 @@ export default class BasicsPage extends Page {
|
||||
],
|
||||
})}
|
||||
|
||||
{Object.keys(this.displayNameOptions).length > 1
|
||||
? FieldSet.component({
|
||||
label: app.translator.trans('core.admin.basics.display_name_heading'),
|
||||
children: [
|
||||
<div className="helpText">{app.translator.trans('core.admin.basics.display_name_text')}</div>,
|
||||
Select.component({
|
||||
options: this.displayNameOptions,
|
||||
value: this.values.display_name_driver(),
|
||||
onchange: this.values.display_name_driver,
|
||||
}),
|
||||
],
|
||||
})
|
||||
: ''}
|
||||
|
||||
{Button.component({
|
||||
type: 'submit',
|
||||
className: 'Button Button--primary',
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Page from '../../common/components/Page';
|
||||
import Page from './Page';
|
||||
import StatusWidget from './StatusWidget';
|
||||
|
||||
export default class DashboardPage extends Page {
|
||||
|
@@ -1,8 +1,17 @@
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import Component from '../../common/Component';
|
||||
|
||||
export default class DashboardWidget extends Component {
|
||||
export default class Widget extends Component {
|
||||
view() {
|
||||
return <div className={'DashboardWidget Widget ' + this.className()}>{this.content()}</div>;
|
||||
return <div className={'Widget ' + this.className()}>{this.content()}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,10 +1,13 @@
|
||||
import Page from '../../common/components/Page';
|
||||
import Page from './Page';
|
||||
import LinkButton from '../../common/components/LinkButton';
|
||||
import Button from '../../common/components/Button';
|
||||
import Dropdown from '../../common/components/Dropdown';
|
||||
import Separator from '../../common/components/Separator';
|
||||
import AddExtensionModal from './AddExtensionModal';
|
||||
import LoadingModal from './LoadingModal';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import icon from '../../common/helpers/icon';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
|
||||
export default class ExtensionsPage extends Page {
|
||||
view() {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Page from '../../common/components/Page';
|
||||
import Page from './Page';
|
||||
import FieldSet from '../../common/components/FieldSet';
|
||||
import Button from '../../common/components/Button';
|
||||
import Alert from '../../common/components/Alert';
|
||||
@@ -11,7 +11,6 @@ export default class MailPage extends Page {
|
||||
super.init();
|
||||
|
||||
this.saving = false;
|
||||
this.sendingTest = false;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
@@ -29,7 +28,7 @@ export default class MailPage extends Page {
|
||||
app
|
||||
.request({
|
||||
method: 'GET',
|
||||
url: app.forum.attribute('apiUrl') + '/mail/settings',
|
||||
url: app.forum.attribute('apiUrl') + '/mail-settings',
|
||||
})
|
||||
.then((response) => {
|
||||
this.driverFields = response['data']['attributes']['fields'];
|
||||
@@ -122,27 +121,11 @@ export default class MailPage extends Page {
|
||||
],
|
||||
})}
|
||||
|
||||
<FieldSet>
|
||||
{Button.component({
|
||||
type: 'submit',
|
||||
className: 'Button Button--primary',
|
||||
children: app.translator.trans('core.admin.email.submit_button'),
|
||||
disabled: !this.changed(),
|
||||
})}
|
||||
</FieldSet>
|
||||
|
||||
{FieldSet.component({
|
||||
label: app.translator.trans('core.admin.email.send_test_mail_heading'),
|
||||
className: 'MailPage-MailSettings',
|
||||
children: [
|
||||
<div className="helpText">{app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user.email() })}</div>,
|
||||
Button.component({
|
||||
className: 'Button Button--primary',
|
||||
children: app.translator.trans('core.admin.email.send_test_mail_button'),
|
||||
disabled: this.sendingTest || this.changed(),
|
||||
onclick: () => this.sendTestEmail(),
|
||||
}),
|
||||
],
|
||||
{Button.component({
|
||||
type: 'submit',
|
||||
className: 'Button Button--primary',
|
||||
children: app.translator.trans('core.admin.email.submit_button'),
|
||||
disabled: !this.changed(),
|
||||
})}
|
||||
</form>
|
||||
</div>
|
||||
@@ -166,34 +149,10 @@ export default class MailPage extends Page {
|
||||
return this.fields.some((key) => this.values[key]() !== app.data.settings[key]);
|
||||
}
|
||||
|
||||
sendTestEmail() {
|
||||
if (this.saving || this.sendingTest) return;
|
||||
|
||||
this.sendingTest = true;
|
||||
app.alerts.dismiss(this.testEmailSuccessAlert);
|
||||
|
||||
app
|
||||
.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/mail/test',
|
||||
})
|
||||
.then((response) => {
|
||||
this.sendingTest = false;
|
||||
app.alerts.show(
|
||||
(this.testEmailSuccessAlert = new Alert({ type: 'success', children: app.translator.trans('core.admin.email.send_test_mail_success') }))
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.sendingTest = false;
|
||||
m.redraw();
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.saving || this.sendingTest) return;
|
||||
if (this.saving) return;
|
||||
|
||||
this.saving = true;
|
||||
app.alerts.dismiss(this.successAlert);
|
||||
|
32
js/src/admin/components/Page.js
Normal file
32
js/src/admin/components/Page.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import Component from '../../common/Component';
|
||||
|
||||
/**
|
||||
* The `Page` component
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class Page extends Component {
|
||||
init() {
|
||||
app.previous = app.current;
|
||||
app.current = this;
|
||||
|
||||
app.modal.close();
|
||||
|
||||
/**
|
||||
* A class name to apply to the body while the route is active.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.bodyClass = '';
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
if (isInitialized) return;
|
||||
|
||||
if (this.bodyClass) {
|
||||
$('#app').addClass(this.bodyClass);
|
||||
|
||||
context.onunload = () => $('#app').removeClass(this.bodyClass);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import Page from '../../common/components/Page';
|
||||
import Page from './Page';
|
||||
import GroupBadge from '../../common/components/GroupBadge';
|
||||
import EditGroupModal from './EditGroupModal';
|
||||
import Group from '../../common/models/Group';
|
||||
|
34
js/src/admin/components/Widget.js
Normal file
34
js/src/admin/components/Widget.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import Component from '../../common/Component';
|
||||
|
||||
export default class DashboardWidget extends Component {
|
||||
view() {
|
||||
return <div className={'DashboardWidget ' + this.className()}>{this.content()}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class name to apply to the widget.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
className() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of the widget.
|
||||
*
|
||||
* @return {VirtualElement}
|
||||
*/
|
||||
content() {
|
||||
return [];
|
||||
}
|
||||
}
|
@@ -21,7 +21,6 @@ import Post from './models/Post';
|
||||
import Group from './models/Group';
|
||||
import Notification from './models/Notification';
|
||||
import { flattenDeep } from 'lodash-es';
|
||||
import PageState from './states/PageState';
|
||||
|
||||
/**
|
||||
* The `App` class provides a container for an application, as well as various
|
||||
@@ -116,28 +115,6 @@ export default class Application {
|
||||
*/
|
||||
requestError = null;
|
||||
|
||||
/**
|
||||
* The page the app is currently on.
|
||||
*
|
||||
* This object holds information about the type of page we are currently
|
||||
* visiting, and sometimes additional arbitrary page state that may be
|
||||
* relevant to lower-level components.
|
||||
*
|
||||
* @type {PageState}
|
||||
*/
|
||||
current = new PageState(null);
|
||||
|
||||
/**
|
||||
* The page the app was on before the current page.
|
||||
*
|
||||
* Once the application navigates to another page, the object previously
|
||||
* assigned to this.current will be moved to this.previous, while this.current
|
||||
* is re-initialized.
|
||||
*
|
||||
* @type {PageState}
|
||||
*/
|
||||
previous = new PageState(null);
|
||||
|
||||
data;
|
||||
|
||||
title = '';
|
||||
@@ -347,15 +324,12 @@ export default class Application {
|
||||
}
|
||||
|
||||
const isDebug = app.forum.attribute('debug');
|
||||
// contains a formatted errors if possible, response must be an JSON API array of errors
|
||||
// the details property is decoded to transform escaped characters such as '\n'
|
||||
const formattedError = error.response && Array.isArray(error.response.errors) && error.response.errors.map((e) => decodeURI(e.detail));
|
||||
|
||||
error.alert = new Alert({
|
||||
type: 'error',
|
||||
children,
|
||||
controls: isDebug && [
|
||||
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error, formattedError)}>
|
||||
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error)}>
|
||||
Debug
|
||||
</Button>,
|
||||
],
|
||||
@@ -364,17 +338,6 @@ export default class Application {
|
||||
try {
|
||||
options.errorHandler(error);
|
||||
} catch (error) {
|
||||
if (isDebug && error.xhr) {
|
||||
const { method, url } = error.options;
|
||||
const { status = '' } = error.xhr;
|
||||
|
||||
console.group(`${method} ${url} ${status}`);
|
||||
|
||||
console.error(...(formattedError || [error]));
|
||||
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
this.alerts.show(error.alert);
|
||||
}
|
||||
|
||||
@@ -387,13 +350,12 @@ export default class Application {
|
||||
|
||||
/**
|
||||
* @param {RequestError} error
|
||||
* @param {string[]} [formattedError]
|
||||
* @private
|
||||
*/
|
||||
showDebug(error, formattedError) {
|
||||
showDebug(error) {
|
||||
this.alerts.dismiss(this.requestError.alert);
|
||||
|
||||
this.modal.show(new RequestErrorModal({ error, formattedError }));
|
||||
this.modal.show(new RequestErrorModal({ error }));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -30,7 +30,6 @@ import Forum from './models/Forum';
|
||||
import Component from './Component';
|
||||
import Translator from './Translator';
|
||||
import AlertManager from './components/AlertManager';
|
||||
import Page from './components/Page';
|
||||
import Switch from './components/Switch';
|
||||
import Badge from './components/Badge';
|
||||
import LoadingIndicator from './components/LoadingIndicator';
|
||||
@@ -95,7 +94,6 @@ export default {
|
||||
Component: Component,
|
||||
Translator: Translator,
|
||||
'components/AlertManager': AlertManager,
|
||||
'components/Page': Page,
|
||||
'components/Switch': Switch,
|
||||
'components/Badge': Badge,
|
||||
'components/LoadingIndicator': LoadingIndicator,
|
||||
|
@@ -30,6 +30,6 @@ export default class Badge extends Component {
|
||||
config(isInitialized) {
|
||||
if (isInitialized) return;
|
||||
|
||||
if (this.props.label) this.$().tooltip();
|
||||
if (this.props.label) this.$().tooltip({ container: 'body' });
|
||||
}
|
||||
}
|
||||
|
@@ -10,14 +10,23 @@ import icon from '../helpers/icon';
|
||||
* - `state` Whether or not the checkbox is checked.
|
||||
* - `className` The class name for the root element.
|
||||
* - `disabled` Whether or not the checkbox is disabled.
|
||||
* - `loading` Whether or not the checkbox is loading.
|
||||
* - `onchange` A callback to run when the checkbox is checked/unchecked.
|
||||
* - `children` A text label to display next to the checkbox.
|
||||
*/
|
||||
export default class Checkbox extends Component {
|
||||
init() {
|
||||
/**
|
||||
* Whether or not the checkbox's value is in the process of being saved.
|
||||
*
|
||||
* @type {Boolean}
|
||||
* @public
|
||||
*/
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
view() {
|
||||
let className = 'Checkbox ' + (this.props.state ? 'on' : 'off') + ' ' + (this.props.className || '');
|
||||
if (this.props.loading) className += ' loading';
|
||||
if (this.loading) className += ' loading';
|
||||
if (this.props.disabled) className += ' disabled';
|
||||
|
||||
return (
|
||||
@@ -36,7 +45,7 @@ export default class Checkbox extends Component {
|
||||
* @protected
|
||||
*/
|
||||
getDisplay() {
|
||||
return this.props.loading ? LoadingIndicator.component({ size: 'tiny' }) : icon(this.props.state ? 'fas fa-check' : 'fas fa-times');
|
||||
return this.loading ? LoadingIndicator.component({ size: 'tiny' }) : icon(this.props.state ? 'fas fa-check' : 'fas fa-times');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -43,6 +43,8 @@ export default class ModalManager extends Component {
|
||||
this.showing = true;
|
||||
this.component = component;
|
||||
|
||||
if (app.current) app.current.retain = true;
|
||||
|
||||
m.redraw(true);
|
||||
|
||||
const dismissible = !!this.component.isDismissible();
|
||||
|
@@ -6,26 +6,16 @@ export default class RequestErrorModal extends Modal {
|
||||
}
|
||||
|
||||
title() {
|
||||
return this.props.error.xhr ? `${this.props.error.xhr.status} ${this.props.error.xhr.statusText}` : '';
|
||||
return this.props.error.xhr ? this.props.error.xhr.status + ' ' + this.props.error.xhr.statusText : '';
|
||||
}
|
||||
|
||||
content() {
|
||||
const { error, formattedError } = this.props;
|
||||
|
||||
let responseText;
|
||||
|
||||
// If the error is already formatted, just add line endings;
|
||||
// else try to parse it as JSON and stringify it with indentation
|
||||
if (formattedError) {
|
||||
responseText = formattedError.join('\n\n');
|
||||
} else {
|
||||
try {
|
||||
const json = error.response || JSON.parse(error.responseText);
|
||||
|
||||
responseText = JSON.stringify(json, null, 2);
|
||||
} catch (e) {
|
||||
responseText = error.responseText;
|
||||
}
|
||||
try {
|
||||
responseText = JSON.stringify(JSON.parse(this.props.error.responseText), null, 2);
|
||||
} catch (e) {
|
||||
responseText = this.props.error.responseText;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@@ -12,6 +12,6 @@ export default class Switch extends Checkbox {
|
||||
}
|
||||
|
||||
getDisplay() {
|
||||
return this.props.loading ? super.getDisplay() : '';
|
||||
return this.loading ? super.getDisplay() : '';
|
||||
}
|
||||
}
|
||||
|
@@ -1,33 +0,0 @@
|
||||
import subclassOf from '../../common/utils/subclassOf';
|
||||
|
||||
export default class PageState {
|
||||
constructor(type, data = {}) {
|
||||
this.type = type;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the page matches the given class and data.
|
||||
*
|
||||
* @param {object} type The page class to check against. Subclasses are
|
||||
* accepted as well.
|
||||
* @param {object} data
|
||||
* @return {boolean}
|
||||
*/
|
||||
matches(type, data = {}) {
|
||||
// Fail early when the page is of a different type
|
||||
if (!subclassOf(this.type, type)) return false;
|
||||
|
||||
// Now that the type is known to be correct, we loop through the provided
|
||||
// data to see whether it matches the data in our state.
|
||||
return Object.keys(data).every((key) => this.data[key] === data[key]);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this.data[key];
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
this.data[key] = value;
|
||||
}
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Check if class A is the same as or a subclass of class B.
|
||||
*/
|
||||
export default function subclassOf(A, B) {
|
||||
return A && (A === B || A.prototype instanceof B);
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import History from './utils/History';
|
||||
import Pane from './utils/Pane';
|
||||
import Search from './components/Search';
|
||||
import ReplyComposer from './components/ReplyComposer';
|
||||
import DiscussionPage from './components/DiscussionPage';
|
||||
import SignUpModal from './components/SignUpModal';
|
||||
@@ -13,9 +14,6 @@ import routes from './routes';
|
||||
import alertEmailConfirmation from './utils/alertEmailConfirmation';
|
||||
import Application from '../common/Application';
|
||||
import Navigation from '../common/components/Navigation';
|
||||
import NotificationListState from './states/NotificationListState';
|
||||
import GlobalSearchState from './states/GlobalSearchState';
|
||||
import DiscussionListState from './state/DiscussionListState';
|
||||
|
||||
export default class ForumApplication extends Application {
|
||||
/**
|
||||
@@ -36,6 +34,13 @@ export default class ForumApplication extends Application {
|
||||
discussionRenamed: DiscussionRenamedPost,
|
||||
};
|
||||
|
||||
/**
|
||||
* The page's search component instance.
|
||||
*
|
||||
* @type {Search}
|
||||
*/
|
||||
search = new Search();
|
||||
|
||||
/**
|
||||
* An object which controls the state of the page's side pane.
|
||||
*
|
||||
@@ -58,38 +63,10 @@ export default class ForumApplication extends Application {
|
||||
*/
|
||||
history = new History();
|
||||
|
||||
/**
|
||||
* An object which controls the state of the user's notifications.
|
||||
*
|
||||
* @type {NotificationListState}
|
||||
*/
|
||||
notifications = new NotificationListState(this);
|
||||
|
||||
/*
|
||||
* An object which stores previously searched queries and provides convenient
|
||||
* tools for retrieving and managing search values.
|
||||
*
|
||||
* @type {GlobalSearchState}
|
||||
*/
|
||||
search = new GlobalSearchState();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
routes(this);
|
||||
|
||||
/**
|
||||
* An object which controls the state of the cached discussion list, which
|
||||
* is used in the index page and the slideout pane.
|
||||
*
|
||||
* @type {DiscussionListState}
|
||||
*/
|
||||
this.discussions = new DiscussionListState({ forumApp: this });
|
||||
|
||||
/**
|
||||
* @deprecated beta 14, remove in beta 15.
|
||||
*/
|
||||
this.cache.discussionList = this.discussions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,7 +137,7 @@ export default class ForumApplication extends Application {
|
||||
* @return {Boolean}
|
||||
*/
|
||||
viewingDiscussion(discussion) {
|
||||
return this.current.matches(DiscussionPage, { discussion });
|
||||
return this.current instanceof DiscussionPage && this.current.discussion === discussion;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -23,6 +23,7 @@ import PostEdited from './components/PostEdited';
|
||||
import PostStream from './components/PostStream';
|
||||
import ChangePasswordModal from './components/ChangePasswordModal';
|
||||
import IndexPage from './components/IndexPage';
|
||||
import Page from './components/Page';
|
||||
import DiscussionRenamedNotification from './components/DiscussionRenamedNotification';
|
||||
import DiscussionsSearchSource from './components/DiscussionsSearchSource';
|
||||
import HeaderSecondary from './components/HeaderSecondary';
|
||||
@@ -91,6 +92,7 @@ export default Object.assign(compat, {
|
||||
'components/PostStream': PostStream,
|
||||
'components/ChangePasswordModal': ChangePasswordModal,
|
||||
'components/IndexPage': IndexPage,
|
||||
'components/Page': Page,
|
||||
'components/DiscussionRenamedNotification': DiscussionRenamedNotification,
|
||||
'components/DiscussionsSearchSource': DiscussionsSearchSource,
|
||||
'components/HeaderSecondary': HeaderSecondary,
|
||||
|
@@ -31,7 +31,13 @@ export default class CommentPost extends Post {
|
||||
*/
|
||||
this.revealContent = false;
|
||||
|
||||
this.subtree.check(() => this.isEditing());
|
||||
// Create an instance of the component that displays the post's author so
|
||||
// that we can force the post to rerender when the user card is shown.
|
||||
this.postUser = new PostUser({ post: this.props.post });
|
||||
this.subtree.check(
|
||||
() => this.postUser.cardVisible,
|
||||
() => this.isEditing()
|
||||
);
|
||||
}
|
||||
|
||||
content() {
|
||||
@@ -123,12 +129,13 @@ export default class CommentPost extends Post {
|
||||
headerItems() {
|
||||
const items = new ItemList();
|
||||
const post = this.props.post;
|
||||
const props = { post };
|
||||
|
||||
items.add('user', PostUser.component({ post }), 100);
|
||||
items.add('meta', PostMeta.component({ post }));
|
||||
items.add('user', this.postUser.render(), 100);
|
||||
items.add('meta', PostMeta.component(props));
|
||||
|
||||
if (post.isEdited() && !post.isHidden()) {
|
||||
items.add('edited', PostEdited.component({ post }));
|
||||
items.add('edited', PostEdited.component(props));
|
||||
}
|
||||
|
||||
// If the post is hidden, add a button that allows toggling the visibility
|
||||
|
@@ -98,7 +98,7 @@ export default class DiscussionComposer extends ComposerBody {
|
||||
.save(data)
|
||||
.then((discussion) => {
|
||||
app.composer.hide();
|
||||
app.discussions.refresh();
|
||||
app.cache.discussionList.refresh();
|
||||
m.route(app.route.discussion(discussion));
|
||||
}, this.loaded.bind(this));
|
||||
}
|
||||
|
@@ -11,38 +11,56 @@ import Placeholder from '../../common/components/Placeholder';
|
||||
*
|
||||
* - `params` A map of parameters used to construct a refined parameter object
|
||||
* to send along in the API request to get discussion results.
|
||||
* - `state` A DiscussionListState object that represents the discussion lists's state.
|
||||
*/
|
||||
export default class DiscussionList extends Component {
|
||||
init() {
|
||||
this.state = this.props.state;
|
||||
/**
|
||||
* Whether or not discussion results are loading.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.loading = true;
|
||||
|
||||
/**
|
||||
* Whether or not there are more results that can be loaded.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.moreResults = false;
|
||||
|
||||
/**
|
||||
* The discussions in the discussion list.
|
||||
*
|
||||
* @type {Discussion[]}
|
||||
*/
|
||||
this.discussions = [];
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
view() {
|
||||
const state = this.state;
|
||||
|
||||
const params = state.getParams();
|
||||
const params = this.props.params;
|
||||
let loading;
|
||||
|
||||
if (state.isLoading()) {
|
||||
if (this.loading) {
|
||||
loading = LoadingIndicator.component();
|
||||
} else if (state.moreResults) {
|
||||
} else if (this.moreResults) {
|
||||
loading = Button.component({
|
||||
children: app.translator.trans('core.forum.discussion_list.load_more_button'),
|
||||
className: 'Button',
|
||||
onclick: state.loadMore.bind(state),
|
||||
onclick: this.loadMore.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
if (state.empty()) {
|
||||
if (this.discussions.length === 0 && !this.loading) {
|
||||
const text = app.translator.trans('core.forum.discussion_list.empty_text');
|
||||
return <div className="DiscussionList">{Placeholder.component({ text })}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'DiscussionList' + (state.isSearchResults() ? ' DiscussionList--searchResults' : '')}>
|
||||
<div className={'DiscussionList' + (this.props.params.q ? ' DiscussionList--searchResults' : '')}>
|
||||
<ul className="DiscussionList-discussions">
|
||||
{state.discussions.map((discussion) => {
|
||||
{this.discussions.map((discussion) => {
|
||||
return (
|
||||
<li key={discussion.id()} data-id={discussion.id()}>
|
||||
{DiscussionListItem.component({ discussion, params })}
|
||||
@@ -54,4 +72,140 @@ export default class DiscussionList extends Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters that should be passed in the API request to get
|
||||
* discussion results.
|
||||
*
|
||||
* @return {Object}
|
||||
* @api
|
||||
*/
|
||||
requestParams() {
|
||||
const params = { include: ['user', 'lastPostedUser'], filter: {} };
|
||||
|
||||
params.sort = this.sortMap()[this.props.params.sort];
|
||||
|
||||
if (this.props.params.q) {
|
||||
params.filter.q = this.props.params.q;
|
||||
|
||||
params.include.push('mostRelevantPost', 'mostRelevantPost.user');
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a map of sort keys (which appear in the URL, and are used for
|
||||
* translation) to the API sort value that they represent.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
sortMap() {
|
||||
const map = {};
|
||||
|
||||
if (this.props.params.q) {
|
||||
map.relevance = '';
|
||||
}
|
||||
map.latest = '-lastPostedAt';
|
||||
map.top = '-commentCount';
|
||||
map.newest = '-createdAt';
|
||||
map.oldest = 'createdAt';
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear and reload the discussion list.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
refresh(clear = true) {
|
||||
if (clear) {
|
||||
this.loading = true;
|
||||
this.discussions = [];
|
||||
}
|
||||
|
||||
return this.loadResults().then(
|
||||
(results) => {
|
||||
this.discussions = [];
|
||||
this.parseResults(results);
|
||||
},
|
||||
() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a new page of discussion results.
|
||||
*
|
||||
* @param {Integer} offset The index to start the page at.
|
||||
* @return {Promise}
|
||||
*/
|
||||
loadResults(offset) {
|
||||
const preloadedDiscussions = app.preloadedApiDocument();
|
||||
|
||||
if (preloadedDiscussions) {
|
||||
return m.deferred().resolve(preloadedDiscussions).promise;
|
||||
}
|
||||
|
||||
const params = this.requestParams();
|
||||
params.page = { offset };
|
||||
params.include = params.include.join(',');
|
||||
|
||||
return app.store.find('discussions', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the next page of discussion results.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
loadMore() {
|
||||
this.loading = true;
|
||||
|
||||
this.loadResults(this.discussions.length).then(this.parseResults.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse results and append them to the discussion list.
|
||||
*
|
||||
* @param {Discussion[]} results
|
||||
* @return {Discussion[]}
|
||||
*/
|
||||
parseResults(results) {
|
||||
[].push.apply(this.discussions, results);
|
||||
|
||||
this.loading = false;
|
||||
this.moreResults = !!results.payload.links.next;
|
||||
|
||||
m.lazyRedraw();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a discussion from the list if it is present.
|
||||
*
|
||||
* @param {Discussion} discussion
|
||||
* @public
|
||||
*/
|
||||
removeDiscussion(discussion) {
|
||||
const index = this.discussions.indexOf(discussion);
|
||||
|
||||
if (index !== -1) {
|
||||
this.discussions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a discussion to the top of the list.
|
||||
*
|
||||
* @param {Discussion} discussion
|
||||
* @public
|
||||
*/
|
||||
addDiscussion(discussion) {
|
||||
this.discussions.unshift(discussion);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Page from '../../common/components/Page';
|
||||
import Page from './Page';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import DiscussionHero from './DiscussionHero';
|
||||
import PostStream from './PostStream';
|
||||
@@ -7,7 +7,6 @@ import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import SplitDropdown from '../../common/components/SplitDropdown';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import DiscussionControls from '../utils/DiscussionControls';
|
||||
import DiscussionList from './DiscussionList';
|
||||
|
||||
/**
|
||||
* The `DiscussionPage` component displays a whole discussion page, including
|
||||
@@ -36,13 +35,13 @@ export default class DiscussionPage extends Page {
|
||||
// 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
|
||||
// then the pane would which would be slow and would cause problems with
|
||||
// event handlers.
|
||||
if (app.discussions.hasDiscussions()) {
|
||||
if (app.cache.discussionList) {
|
||||
app.pane.enable();
|
||||
app.pane.hide();
|
||||
|
||||
if (app.previous.matches(DiscussionPage)) {
|
||||
if (app.previous instanceof DiscussionPage) {
|
||||
m.redraw.strategy('diff');
|
||||
}
|
||||
}
|
||||
@@ -91,9 +90,9 @@ export default class DiscussionPage extends Page {
|
||||
|
||||
return (
|
||||
<div className="DiscussionPage">
|
||||
{app.discussions.hasDiscussions() ? (
|
||||
{app.cache.discussionList ? (
|
||||
<div className="DiscussionPage-list" config={this.configPane.bind(this)}>
|
||||
{!$('.App-navigation').is(':visible') && <DiscussionList state={app.discussions} />}
|
||||
{!$('.App-navigation').is(':visible') ? app.cache.discussionList.render() : ''}
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
@@ -200,9 +199,6 @@ export default class DiscussionPage extends Page {
|
||||
this.stream = new PostStream({ discussion, includedPosts });
|
||||
this.stream.on('positionChanged', this.positionChanged.bind(this));
|
||||
this.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true);
|
||||
|
||||
app.current.set('discussion', discussion);
|
||||
app.current.set('stream', this.stream);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,6 +1,4 @@
|
||||
import ComposerBody from './ComposerBody';
|
||||
import Alert from '../../common/components/Alert';
|
||||
import Button from '../../common/components/Button';
|
||||
import icon from '../../common/helpers/icon';
|
||||
|
||||
function minimizeComposerIfFullScreen(e) {
|
||||
@@ -77,40 +75,10 @@ export default class EditPostComposer extends ComposerBody {
|
||||
}
|
||||
|
||||
onsubmit() {
|
||||
const discussion = this.props.post.discussion();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const data = this.data();
|
||||
|
||||
this.props.post.save(data).then((post) => {
|
||||
// If we're currently viewing the discussion which this edit was made
|
||||
// in, then we can scroll to the post.
|
||||
if (app.viewingDiscussion(discussion)) {
|
||||
app.current.stream.goToNumber(post.number());
|
||||
} else {
|
||||
// Otherwise, we'll create an alert message to inform the user that
|
||||
// their edit has been made, containing a button which will
|
||||
// transition to their edited post when clicked.
|
||||
let alert;
|
||||
const viewButton = Button.component({
|
||||
className: 'Button Button--link',
|
||||
children: app.translator.trans('core.forum.composer_edit.view_button'),
|
||||
onclick: () => {
|
||||
m.route(app.route.post(post));
|
||||
app.alerts.dismiss(alert);
|
||||
},
|
||||
});
|
||||
app.alerts.show(
|
||||
(alert = new Alert({
|
||||
type: 'success',
|
||||
children: app.translator.trans('core.forum.composer_edit.edited_message'),
|
||||
controls: [viewButton],
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
app.composer.hide();
|
||||
}, this.loaded.bind(this));
|
||||
this.props.post.save(data).then(() => app.composer.hide(), this.loaded.bind(this));
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,6 @@ import SelectDropdown from '../../common/components/SelectDropdown';
|
||||
import NotificationsDropdown from './NotificationsDropdown';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import Search from '../components/Search';
|
||||
|
||||
/**
|
||||
* The `HeaderSecondary` component displays secondary header controls, such as
|
||||
@@ -34,7 +33,7 @@ export default class HeaderSecondary extends Component {
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('search', Search.component({ state: app.search }), 30);
|
||||
items.add('search', app.search.render(), 30);
|
||||
|
||||
if (app.forum.attribute('showLanguageSelector') && Object.keys(app.data.locales).length > 1) {
|
||||
const locales = [];
|
||||
@@ -68,7 +67,7 @@ export default class HeaderSecondary extends Component {
|
||||
}
|
||||
|
||||
if (app.session.user) {
|
||||
items.add('notifications', NotificationsDropdown.component({ state: app.notifications }), 10);
|
||||
items.add('notifications', NotificationsDropdown.component(), 10);
|
||||
items.add('session', SessionDropdown.component(), 0);
|
||||
} else {
|
||||
if (app.forum.attribute('allowSignUp')) {
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { extend } from '../../common/extend';
|
||||
import Page from '../../common/components/Page';
|
||||
import Page from './Page';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import icon from '../../common/helpers/icon';
|
||||
import DiscussionList from './DiscussionList';
|
||||
import WelcomeHero from './WelcomeHero';
|
||||
import DiscussionComposer from './DiscussionComposer';
|
||||
@@ -17,27 +18,42 @@ import SelectDropdown from '../../common/components/SelectDropdown';
|
||||
* hero, the sidebar, and the discussion list.
|
||||
*/
|
||||
export default class IndexPage extends Page {
|
||||
static providesInitialSearch = true;
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
// If the user is returning from a discussion page, then take note of which
|
||||
// discussion they have just visited. After the view is rendered, we will
|
||||
// scroll down so that this discussion is in view.
|
||||
if (app.previous.matches(DiscussionPage)) {
|
||||
this.lastDiscussion = app.previous.get('discussion');
|
||||
if (app.previous instanceof DiscussionPage) {
|
||||
this.lastDiscussion = app.previous.discussion;
|
||||
}
|
||||
|
||||
// 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
|
||||
// cache so that results are reloaded.
|
||||
if (app.previous.matches(IndexPage)) {
|
||||
app.discussions.clear();
|
||||
if (app.previous instanceof IndexPage) {
|
||||
app.cache.discussionList = null;
|
||||
}
|
||||
|
||||
app.discussions.refreshParams(app.search.params());
|
||||
const params = this.params();
|
||||
|
||||
if (app.cache.discussionList) {
|
||||
// Compare the requested parameters (sort, search query) to the ones that
|
||||
// are currently present in the cached discussion list. If they differ, we
|
||||
// will clear the cache and set up a new discussion list component with
|
||||
// the new parameters.
|
||||
Object.keys(params).some((key) => {
|
||||
if (app.cache.discussionList.props.params[key] !== params[key]) {
|
||||
app.cache.discussionList = null;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!app.cache.discussionList) {
|
||||
app.cache.discussionList = new DiscussionList({ params });
|
||||
}
|
||||
|
||||
app.history.push('index', app.translator.trans('core.forum.header.back_to_index_tooltip'));
|
||||
|
||||
@@ -64,7 +80,7 @@ export default class IndexPage extends Page {
|
||||
<ul className="IndexPage-toolbar-view">{listItems(this.viewItems().toArray())}</ul>
|
||||
<ul className="IndexPage-toolbar-action">{listItems(this.actionItems().toArray())}</ul>
|
||||
</div>
|
||||
<DiscussionList state={app.discussions} />
|
||||
{app.cache.discussionList.render()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,7 +187,7 @@ export default class IndexPage extends Page {
|
||||
*/
|
||||
navItems() {
|
||||
const items = new ItemList();
|
||||
const params = app.search.stickyParams();
|
||||
const params = this.stickyParams();
|
||||
|
||||
items.add(
|
||||
'allDiscussions',
|
||||
@@ -195,7 +211,7 @@ export default class IndexPage extends Page {
|
||||
*/
|
||||
viewItems() {
|
||||
const items = new ItemList();
|
||||
const sortMap = app.discussions.sortMap();
|
||||
const sortMap = app.cache.discussionList.sortMap();
|
||||
|
||||
const sortOptions = {};
|
||||
for (const i in sortMap) {
|
||||
@@ -206,15 +222,15 @@ export default class IndexPage extends Page {
|
||||
'sort',
|
||||
Dropdown.component({
|
||||
buttonClassName: 'Button',
|
||||
label: sortOptions[app.search.params().sort] || Object.keys(sortMap).map((key) => sortOptions[key])[0],
|
||||
label: sortOptions[this.params().sort] || Object.keys(sortMap).map((key) => sortOptions[key])[0],
|
||||
children: Object.keys(sortOptions).map((value) => {
|
||||
const label = sortOptions[value];
|
||||
const active = (app.search.params().sort || Object.keys(sortMap)[0]) === value;
|
||||
const active = (this.params().sort || Object.keys(sortMap)[0]) === value;
|
||||
|
||||
return Button.component({
|
||||
children: label,
|
||||
icon: active ? 'fas fa-check' : true,
|
||||
onclick: app.search.changeSort.bind(app.search, value),
|
||||
onclick: this.changeSort.bind(this, value),
|
||||
active: active,
|
||||
});
|
||||
}),
|
||||
@@ -240,7 +256,7 @@ export default class IndexPage extends Page {
|
||||
icon: 'fas fa-sync',
|
||||
className: 'Button Button--icon',
|
||||
onclick: () => {
|
||||
app.discussions.refresh();
|
||||
app.cache.discussionList.refresh();
|
||||
if (app.session.user) {
|
||||
app.store.find('users', app.session.user.id());
|
||||
m.redraw();
|
||||
@@ -264,6 +280,72 @@ export default class IndexPage extends Page {
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current search query, if any. This is implemented to activate
|
||||
* the search box in the header.
|
||||
*
|
||||
* @see Search
|
||||
* @return {String}
|
||||
*/
|
||||
searching() {
|
||||
return this.params().q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the index page without a search filter. This is called when the
|
||||
* 'x' is clicked in the search box in the header.
|
||||
*
|
||||
* @see Search
|
||||
*/
|
||||
clearSearch() {
|
||||
const params = this.params();
|
||||
delete params.q;
|
||||
|
||||
m.route(app.route(this.props.routeName, params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the index page using the given sort parameter.
|
||||
*
|
||||
* @param {String} sort
|
||||
*/
|
||||
changeSort(sort) {
|
||||
const params = this.params();
|
||||
|
||||
if (sort === Object.keys(app.cache.discussionList.sortMap())[0]) {
|
||||
delete params.sort;
|
||||
} else {
|
||||
params.sort = sort;
|
||||
}
|
||||
|
||||
m.route(app.route(this.props.routeName, params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL parameters that stick between filter changes.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
stickyParams() {
|
||||
return {
|
||||
sort: m.route.param('sort'),
|
||||
q: m.route.param('q'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameters to pass to the DiscussionList component.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
params() {
|
||||
const params = this.stickyParams();
|
||||
|
||||
params.filter = m.route.param('filter');
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the composer for a new discussion or prompt the user to login.
|
||||
*
|
||||
|
@@ -21,11 +21,12 @@ export default class NotificationGrid extends Component {
|
||||
this.methods = this.notificationMethods().toArray();
|
||||
|
||||
/**
|
||||
* A map of which notification checkboxes are loading.
|
||||
* A map of notification type-method combinations to the checkbox instances
|
||||
* that represent them.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
this.loading = {};
|
||||
this.inputs = {};
|
||||
|
||||
/**
|
||||
* Information about the available notification types.
|
||||
@@ -33,11 +34,24 @@ export default class NotificationGrid extends Component {
|
||||
* @type {Array}
|
||||
*/
|
||||
this.types = this.notificationTypes().toArray();
|
||||
|
||||
// For each of the notification type-method combinations, create and store a
|
||||
// new checkbox component instance, which we will render in the view.
|
||||
this.types.forEach((type) => {
|
||||
this.methods.forEach((method) => {
|
||||
const key = this.preferenceKey(type.name, method.name);
|
||||
const preference = this.props.user.preferences()[key];
|
||||
|
||||
this.inputs[key] = new Checkbox({
|
||||
state: !!preference,
|
||||
disabled: typeof preference === 'undefined',
|
||||
onchange: () => this.toggle([key]),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
view() {
|
||||
const preferences = this.props.user.preferences();
|
||||
|
||||
return (
|
||||
<table className="NotificationGrid">
|
||||
<thead>
|
||||
@@ -57,20 +71,9 @@ export default class NotificationGrid extends Component {
|
||||
<td className="NotificationGrid-groupToggle" onclick={this.toggleType.bind(this, type.name)}>
|
||||
{icon(type.icon)} {type.label}
|
||||
</td>
|
||||
{this.methods.map((method) => {
|
||||
const key = this.preferenceKey(type.name, method.name);
|
||||
|
||||
return (
|
||||
<td className="NotificationGrid-checkbox">
|
||||
{Checkbox.component({
|
||||
state: !!preferences[key],
|
||||
loading: this.loading[key],
|
||||
disabled: !(key in preferences),
|
||||
onchange: () => this.toggle([key]),
|
||||
})}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
{this.methods.map((method) => (
|
||||
<td className="NotificationGrid-checkbox">{this.inputs[this.preferenceKey(type.name, method.name)].render()}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -109,14 +112,16 @@ export default class NotificationGrid extends Component {
|
||||
const enabled = !preferences[keys[0]];
|
||||
|
||||
keys.forEach((key) => {
|
||||
this.loading[key] = true;
|
||||
preferences[key] = enabled;
|
||||
const control = this.inputs[key];
|
||||
|
||||
control.loading = true;
|
||||
preferences[key] = control.props.state = enabled;
|
||||
});
|
||||
|
||||
m.redraw();
|
||||
|
||||
user.save({ preferences }).then(() => {
|
||||
keys.forEach((key) => (this.loading[key] = false));
|
||||
keys.forEach((key) => (this.inputs[key].loading = false));
|
||||
|
||||
m.redraw();
|
||||
});
|
||||
@@ -128,7 +133,7 @@ export default class NotificationGrid extends Component {
|
||||
* @param {String} method
|
||||
*/
|
||||
toggleMethod(method) {
|
||||
const keys = this.types.map((type) => this.preferenceKey(type.name, method)).filter((key) => key in this.props.user.preferences());
|
||||
const keys = this.types.map((type) => this.preferenceKey(type.name, method)).filter((key) => !this.inputs[key].props.disabled);
|
||||
|
||||
this.toggle(keys);
|
||||
}
|
||||
@@ -139,7 +144,7 @@ export default class NotificationGrid extends Component {
|
||||
* @param {String} type
|
||||
*/
|
||||
toggleType(type) {
|
||||
const keys = this.methods.map((method) => this.preferenceKey(type, method.name)).filter((key) => key in this.props.user.preferences());
|
||||
const keys = this.methods.map((method) => this.preferenceKey(type, method.name)).filter((key) => !this.inputs[key].props.disabled);
|
||||
|
||||
this.toggle(keys);
|
||||
}
|
||||
|
@@ -10,11 +10,23 @@ import Discussion from '../../common/models/Discussion';
|
||||
*/
|
||||
export default class NotificationList extends Component {
|
||||
init() {
|
||||
this.state = this.props.state;
|
||||
/**
|
||||
* Whether or not the notifications are loading.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.loading = false;
|
||||
|
||||
/**
|
||||
* Whether or not there are more results that can be loaded.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.moreResults = false;
|
||||
}
|
||||
|
||||
view() {
|
||||
const pages = this.state.getNotificationPages();
|
||||
const pages = app.cache.notifications || [];
|
||||
|
||||
return (
|
||||
<div className="NotificationList">
|
||||
@@ -24,7 +36,7 @@ export default class NotificationList extends Component {
|
||||
className: 'Button Button--icon Button--link',
|
||||
icon: 'fas fa-check',
|
||||
title: app.translator.trans('core.forum.notifications.mark_all_as_read_tooltip'),
|
||||
onclick: this.state.markAllAsRead.bind(this.state),
|
||||
onclick: this.markAllAsRead.bind(this),
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -85,7 +97,7 @@ export default class NotificationList extends Component {
|
||||
});
|
||||
})
|
||||
: ''}
|
||||
{this.state.isLoading() ? (
|
||||
{this.loading ? (
|
||||
<LoadingIndicator className="LoadingIndicator--block" />
|
||||
) : pages.length ? (
|
||||
''
|
||||
@@ -109,8 +121,8 @@ export default class NotificationList extends Component {
|
||||
const contentTop = $scrollParent === $notifications ? 0 : $notifications.offset().top;
|
||||
const contentHeight = $notifications[0].scrollHeight;
|
||||
|
||||
if (this.state.hasMoreResults() && !this.state.isLoading() && scrollTop + viewportHeight >= contentTop + contentHeight) {
|
||||
this.state.loadMore();
|
||||
if (this.moreResults && !this.loading && scrollTop + viewportHeight >= contentTop + contentHeight) {
|
||||
this.loadMore();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,4 +132,77 @@ export default class NotificationList extends Component {
|
||||
$scrollParent.off('scroll', scrollHandler);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load notifications into the application's cache if they haven't already
|
||||
* been loaded.
|
||||
*/
|
||||
load() {
|
||||
if (app.session.user.newNotificationCount()) {
|
||||
delete app.cache.notifications;
|
||||
}
|
||||
|
||||
if (app.cache.notifications) {
|
||||
return;
|
||||
}
|
||||
|
||||
app.session.user.pushAttributes({ newNotificationCount: 0 });
|
||||
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the next page of notification results.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
loadMore() {
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
const params = app.cache.notifications ? { page: { offset: app.cache.notifications.length * 10 } } : null;
|
||||
|
||||
return app.store
|
||||
.find('notifications', params)
|
||||
.then(this.parseResults.bind(this))
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse results and append them to the notification list.
|
||||
*
|
||||
* @param {Notification[]} results
|
||||
* @return {Notification[]}
|
||||
*/
|
||||
parseResults(results) {
|
||||
app.cache.notifications = app.cache.notifications || [];
|
||||
|
||||
if (results.length) app.cache.notifications.push(results);
|
||||
|
||||
this.moreResults = !!results.payload.links.next;
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all of the notifications as read.
|
||||
*/
|
||||
markAllAsRead() {
|
||||
if (!app.cache.notifications) return;
|
||||
|
||||
app.session.user.pushAttributes({ unreadNotificationCount: 0 });
|
||||
|
||||
app.cache.notifications.forEach((notifications) => {
|
||||
notifications.forEach((notification) => notification.pushAttributes({ isRead: true }));
|
||||
});
|
||||
|
||||
app.request({
|
||||
url: app.forum.attribute('apiUrl') + '/notifications/read',
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,8 @@ export default class NotificationsDropdown extends Dropdown {
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.list = new NotificationList();
|
||||
}
|
||||
|
||||
getButton() {
|
||||
@@ -42,7 +44,7 @@ export default class NotificationsDropdown extends Dropdown {
|
||||
getMenu() {
|
||||
return (
|
||||
<div className={'Dropdown-menu ' + this.props.menuClassName} onclick={this.menuClick.bind(this)}>
|
||||
{this.showing ? NotificationList.component({ state: this.props.state }) : ''}
|
||||
{this.showing ? this.list.render() : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -51,7 +53,7 @@ export default class NotificationsDropdown extends Dropdown {
|
||||
if (app.drawer.isOpen()) {
|
||||
this.goToRoute();
|
||||
} else {
|
||||
this.props.state.load();
|
||||
this.list.load();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Page from '../../common/components/Page';
|
||||
import Page from './Page';
|
||||
import NotificationList from './NotificationList';
|
||||
|
||||
/**
|
||||
@@ -11,16 +11,13 @@ export default class NotificationsPage extends Page {
|
||||
|
||||
app.history.push('notifications');
|
||||
|
||||
app.notifications.load();
|
||||
this.list = new NotificationList();
|
||||
this.list.load();
|
||||
|
||||
this.bodyClass = 'App--notifications';
|
||||
}
|
||||
|
||||
view() {
|
||||
return (
|
||||
<div className="NotificationsPage">
|
||||
<NotificationList state={app.notifications}></NotificationList>
|
||||
</div>
|
||||
);
|
||||
return <div className="NotificationsPage">{this.list.render()}</div>;
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import Component from '../Component';
|
||||
import PageState from '../states/PageState';
|
||||
import Component from '../../common/Component';
|
||||
|
||||
/**
|
||||
* The `Page` component
|
||||
@@ -9,7 +8,7 @@ import PageState from '../states/PageState';
|
||||
export default class Page extends Component {
|
||||
init() {
|
||||
app.previous = app.current;
|
||||
app.current = new PageState(this.constructor);
|
||||
app.current = this;
|
||||
|
||||
app.drawer.hide();
|
||||
app.modal.close();
|
@@ -116,7 +116,6 @@ export default class Post extends Component {
|
||||
let classes = (existing || '').split(' ').concat(['Post']);
|
||||
|
||||
const user = this.props.post.user();
|
||||
const discussion = this.props.post.discussion();
|
||||
|
||||
if (this.loading) {
|
||||
classes.push('Post--loading');
|
||||
@@ -126,7 +125,7 @@ export default class Post extends Component {
|
||||
classes.push('Post--by-actor');
|
||||
}
|
||||
|
||||
if (user && user === discussion.user()) {
|
||||
if (user && app.current.discussion && app.current.discussion.attribute('startUserId') == user.id()) {
|
||||
classes.push('Post--by-start-user');
|
||||
}
|
||||
|
||||
|
@@ -13,6 +13,15 @@ import listItems from '../../common/helpers/listItems';
|
||||
* - `post`
|
||||
*/
|
||||
export default class PostUser extends Component {
|
||||
init() {
|
||||
/**
|
||||
* Whether or not the user hover card is visible.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.cardVisible = false;
|
||||
}
|
||||
|
||||
view() {
|
||||
const post = this.props.post;
|
||||
const user = post.user();
|
||||
@@ -29,7 +38,7 @@ export default class PostUser extends Component {
|
||||
|
||||
let card = '';
|
||||
|
||||
if (!post.isHidden()) {
|
||||
if (!post.isHidden() && this.cardVisible) {
|
||||
card = UserCard.component({
|
||||
user,
|
||||
className: 'UserCard--popover',
|
||||
@@ -72,6 +81,10 @@ export default class PostUser extends Component {
|
||||
* Show the user card.
|
||||
*/
|
||||
showCard() {
|
||||
this.cardVisible = true;
|
||||
|
||||
m.redraw();
|
||||
|
||||
setTimeout(() => this.$('.UserCard').addClass('in'));
|
||||
}
|
||||
|
||||
@@ -79,6 +92,11 @@ export default class PostUser extends Component {
|
||||
* Hide the user card.
|
||||
*/
|
||||
hideCard() {
|
||||
this.$('.UserCard').removeClass('in');
|
||||
this.$('.UserCard')
|
||||
.removeClass('in')
|
||||
.one('transitionend webkitTransitionEnd oTransitionEnd', () => {
|
||||
this.cardVisible = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -57,7 +57,7 @@ export default class RenameDiscussionModal extends Modal {
|
||||
.save({ title })
|
||||
.then(() => {
|
||||
if (app.viewingDiscussion(this.discussion)) {
|
||||
app.current.get('stream').update();
|
||||
app.current.stream.update();
|
||||
}
|
||||
m.redraw();
|
||||
this.hide();
|
||||
|
@@ -89,8 +89,7 @@ export default class ReplyComposer extends ComposerBody {
|
||||
// If we're currently viewing the discussion which this reply was made
|
||||
// in, then we can update the post stream and scroll to the post.
|
||||
if (app.viewingDiscussion(discussion)) {
|
||||
const stream = app.current.get('stream');
|
||||
stream.update().then(() => stream.goToNumber(post.number()));
|
||||
app.current.stream.update().then(() => app.current.stream.goToNumber(post.number()));
|
||||
} else {
|
||||
// Otherwise, we'll create an alert message to inform the user that
|
||||
// their reply has been posted, containing a button which will
|
||||
|
@@ -12,17 +12,19 @@ import UsersSearchSource from './UsersSearchSource';
|
||||
* The `Search` component displays a menu of as-you-type results from a variety
|
||||
* of sources.
|
||||
*
|
||||
* The search box will be 'activated' if the app's seach state's
|
||||
* getInitialSearch() value is a truthy value. If this is the case, an 'x'
|
||||
* button will be shown next to the search field, and clicking it will clear the search.
|
||||
*
|
||||
* PROPS:
|
||||
*
|
||||
* - state: AlertState instance.
|
||||
* The search box will be 'activated' if the app's current controller implements
|
||||
* a `searching` method that returns a truthy value. If this is the case, an 'x'
|
||||
* button will be shown next to the search field, and clicking it will call the
|
||||
* `clearSearch` method on the controller.
|
||||
*/
|
||||
export default class Search extends Component {
|
||||
init() {
|
||||
this.state = this.props.state;
|
||||
/**
|
||||
* The value of the search input.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.value = m.prop('');
|
||||
|
||||
/**
|
||||
* Whether or not the search input has focus.
|
||||
@@ -45,6 +47,13 @@ export default class Search extends Component {
|
||||
*/
|
||||
this.loadingSources = 0;
|
||||
|
||||
/**
|
||||
* A list of queries that have been searched for.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
this.searched = [];
|
||||
|
||||
/**
|
||||
* The index of the currently-selected <li> in the results list. This can be
|
||||
* a unique string (to account for the fact that an item's position may jump
|
||||
@@ -57,7 +66,13 @@ export default class Search extends Component {
|
||||
}
|
||||
|
||||
view() {
|
||||
const currentSearch = this.state.getInitialSearch();
|
||||
const currentSearch = this.getCurrentSearch();
|
||||
|
||||
// Initialize search input value in the view rather than the constructor so
|
||||
// that we have access to app.current.
|
||||
if (typeof this.value() === 'undefined') {
|
||||
this.value(currentSearch || '');
|
||||
}
|
||||
|
||||
// Initialize search sources in the view rather than the constructor so
|
||||
// that we have access to app.forum.
|
||||
@@ -73,7 +88,7 @@ export default class Search extends Component {
|
||||
className={
|
||||
'Search ' +
|
||||
classList({
|
||||
open: this.state.getValue() && this.hasFocus,
|
||||
open: this.value() && this.hasFocus,
|
||||
focused: this.hasFocus,
|
||||
active: !!currentSearch,
|
||||
loading: !!this.loadingSources,
|
||||
@@ -85,8 +100,8 @@ export default class Search extends Component {
|
||||
className="FormControl"
|
||||
type="search"
|
||||
placeholder={extractText(app.translator.trans('core.forum.header.search_placeholder'))}
|
||||
value={this.state.getValue()}
|
||||
oninput={m.withAttr('value', this.state.setValue.bind(this.state))}
|
||||
value={this.value()}
|
||||
oninput={m.withAttr('value', this.value)}
|
||||
onfocus={() => (this.hasFocus = true)}
|
||||
onblur={() => (this.hasFocus = false)}
|
||||
/>
|
||||
@@ -101,7 +116,7 @@ export default class Search extends Component {
|
||||
)}
|
||||
</div>
|
||||
<ul className="Dropdown-menu Search-results">
|
||||
{this.state.getValue() && this.hasFocus ? this.sources.map((source) => source.view(this.state.getValue())) : ''}
|
||||
{this.value() && this.hasFocus ? this.sources.map((source) => source.view(this.value())) : ''}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
@@ -114,7 +129,6 @@ export default class Search extends Component {
|
||||
if (isInitialized) return;
|
||||
|
||||
const search = this;
|
||||
const state = this.state;
|
||||
|
||||
this.$('.Search-results')
|
||||
.on('mousedown', (e) => e.preventDefault())
|
||||
@@ -144,7 +158,7 @@ export default class Search extends Component {
|
||||
|
||||
clearTimeout(search.searchTimeout);
|
||||
search.searchTimeout = setTimeout(() => {
|
||||
if (state.isCached(query)) return;
|
||||
if (search.searched.indexOf(query) !== -1) return;
|
||||
|
||||
if (query.length >= 3) {
|
||||
search.sources.map((source) => {
|
||||
@@ -159,7 +173,7 @@ export default class Search extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
state.cache(query);
|
||||
search.searched.push(query);
|
||||
m.redraw();
|
||||
}, 250);
|
||||
})
|
||||
@@ -171,6 +185,15 @@ export default class Search extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active search in the app's current controller.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
getCurrentSearch() {
|
||||
return app.current && typeof app.current.searching === 'function' && app.current.searching();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the currently selected search result and close the list.
|
||||
*/
|
||||
@@ -178,7 +201,7 @@ export default class Search extends Component {
|
||||
clearTimeout(this.searchTimeout);
|
||||
this.loadingSources = 0;
|
||||
|
||||
if (this.state.getValue()) {
|
||||
if (this.value()) {
|
||||
m.route(this.getItem(this.index).find('a').attr('href'));
|
||||
} else {
|
||||
this.clear();
|
||||
@@ -188,10 +211,16 @@ export default class Search extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the search
|
||||
* Clear the search input and the current controller's active search.
|
||||
*/
|
||||
clear() {
|
||||
this.state.clear();
|
||||
this.value('');
|
||||
|
||||
if (this.getCurrentSearch()) {
|
||||
app.current.clearSearch();
|
||||
} else {
|
||||
m.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -2,8 +2,8 @@
|
||||
* The `SearchSource` interface defines a section of search results in the
|
||||
* search dropdown.
|
||||
*
|
||||
* Search sources should be registered with the `Search` component class
|
||||
* by extending the `sourceItems` method. When the user types a
|
||||
* Search sources should be registered with the `Search` component instance
|
||||
* (app.search) by extending the `sourceItems` method. When the user types a
|
||||
* query, each search source will be prompted to load search results via the
|
||||
* `search` method. When the dropdown is redrawn, it will be constructed by
|
||||
* putting together the output from the `view` method of each source.
|
||||
|
@@ -109,8 +109,6 @@ export default class SettingsPage extends UserPage {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated beta 14, remove in beta 15.
|
||||
*
|
||||
* Generate a callback that will save a value to the given preference.
|
||||
*
|
||||
* @param {String} key
|
||||
@@ -118,11 +116,11 @@ export default class SettingsPage extends UserPage {
|
||||
*/
|
||||
preferenceSaver(key) {
|
||||
return (value, component) => {
|
||||
if (component) component.props.loading = true;
|
||||
if (component) component.loading = true;
|
||||
m.redraw();
|
||||
|
||||
this.user.savePreferences({ [key]: value }).then(() => {
|
||||
if (component) component.props.loading = false;
|
||||
if (component) component.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
};
|
||||
@@ -141,15 +139,10 @@ export default class SettingsPage extends UserPage {
|
||||
Switch.component({
|
||||
children: app.translator.trans('core.forum.settings.privacy_disclose_online_label'),
|
||||
state: this.user.preferences().discloseOnline,
|
||||
onchange: (value) => {
|
||||
this.discloseOnlineLoading = true;
|
||||
|
||||
this.user.savePreferences({ discloseOnline: value }).then(() => {
|
||||
this.discloseOnlineLoading = false;
|
||||
m.redraw();
|
||||
});
|
||||
onchange: (value, component) => {
|
||||
this.user.pushAttributes({ lastSeenAt: null });
|
||||
this.preferenceSaver('discloseOnline')(value, component);
|
||||
},
|
||||
loading: this.discloseOnlineLoading,
|
||||
})
|
||||
);
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Page from '../../common/components/Page';
|
||||
import Page from './Page';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import affixSidebar from '../utils/affixSidebar';
|
||||
import UserCard from './UserCard';
|
||||
@@ -71,8 +71,6 @@ export default class UserPage extends Page {
|
||||
show(user) {
|
||||
this.user = user;
|
||||
|
||||
app.current.set('user', user);
|
||||
|
||||
app.setTitle(user.displayName());
|
||||
|
||||
m.redraw();
|
||||
|
@@ -1,190 +0,0 @@
|
||||
export default class DiscussionListState {
|
||||
constructor({ params = {}, forumApp = app } = {}) {
|
||||
this.params = params;
|
||||
|
||||
this.app = forumApp;
|
||||
|
||||
this.discussions = [];
|
||||
|
||||
this.moreResults = false;
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters that should be passed in the API request to get
|
||||
* discussion results.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
requestParams() {
|
||||
const params = { include: ['user', 'lastPostedUser'], filter: {} };
|
||||
|
||||
params.sort = this.sortMap()[this.params.sort];
|
||||
|
||||
if (this.params.q) {
|
||||
params.filter.q = this.params.q;
|
||||
|
||||
params.include.push('mostRelevantPost', 'mostRelevantPost.user');
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a map of sort keys (which appear in the URL, and are used for
|
||||
* translation) to the API sort value that they represent.
|
||||
*/
|
||||
sortMap() {
|
||||
const map = {};
|
||||
|
||||
if (this.params.q) {
|
||||
map.relevance = '';
|
||||
}
|
||||
map.latest = '-lastPostedAt';
|
||||
map.top = '-commentCount';
|
||||
map.newest = '-createdAt';
|
||||
map.oldest = 'createdAt';
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the search parameters.
|
||||
*/
|
||||
getParams() {
|
||||
return this.params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached discussions.
|
||||
*/
|
||||
clear() {
|
||||
this.discussions = [];
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are no cached discussions or the new params differ from the
|
||||
* old ones, update params and refresh the discussion list from the database.
|
||||
*/
|
||||
refreshParams(newParams) {
|
||||
if (!this.hasDiscussions() || Object.keys(newParams).some((key) => this.getParams()[key] !== newParams[key])) {
|
||||
this.params = newParams;
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear and reload the discussion list.
|
||||
*/
|
||||
refresh({ clear = true } = {}) {
|
||||
this.loading = true;
|
||||
|
||||
if (clear) {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
return this.loadResults().then(
|
||||
(results) => {
|
||||
this.parseResults(results);
|
||||
},
|
||||
() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a new page of discussion results.
|
||||
*
|
||||
* @param offset The index to start the page at.
|
||||
*/
|
||||
loadResults(offset) {
|
||||
const preloadedDiscussions = this.app.preloadedApiDocument();
|
||||
|
||||
if (preloadedDiscussions) {
|
||||
return Promise.resolve(preloadedDiscussions);
|
||||
}
|
||||
|
||||
const params = this.requestParams();
|
||||
params.page = { offset };
|
||||
params.include = params.include.join(',');
|
||||
|
||||
return this.app.store.find('discussions', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the next page of discussion results.
|
||||
*/
|
||||
loadMore() {
|
||||
this.loading = true;
|
||||
|
||||
this.loadResults(this.discussions.length).then(this.parseResults.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse results and append them to the discussion list.
|
||||
*/
|
||||
parseResults(results) {
|
||||
this.discussions.push(...results);
|
||||
|
||||
this.loading = false;
|
||||
this.moreResults = !!results.payload.links && !!results.payload.links.next;
|
||||
|
||||
m.redraw();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a discussion from the list if it is present.
|
||||
*/
|
||||
removeDiscussion(discussion) {
|
||||
const index = this.discussions.indexOf(discussion);
|
||||
|
||||
if (index !== -1) {
|
||||
this.discussions.splice(index, 1);
|
||||
}
|
||||
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a discussion to the top of the list.
|
||||
*/
|
||||
addDiscussion(discussion) {
|
||||
this.discussions.unshift(discussion);
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Are there discussions stored in the discussion list state?
|
||||
*/
|
||||
hasDiscussions() {
|
||||
return this.discussions.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Are discussions currently being loaded?
|
||||
*/
|
||||
isLoading() {
|
||||
return this.loading;
|
||||
}
|
||||
|
||||
/**
|
||||
* In the last request, has the user searched for a discussion?
|
||||
*/
|
||||
isSearchResults() {
|
||||
return !!this.params.q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Have the search results come up empty?
|
||||
*/
|
||||
empty() {
|
||||
return !this.hasDiscussions() && !this.isLoading();
|
||||
}
|
||||
}
|
@@ -1,95 +0,0 @@
|
||||
import SearchState from './SearchState';
|
||||
|
||||
export default class GlobalSearchState extends SearchState {
|
||||
constructor(cachedSearches = [], searchRoute = 'index') {
|
||||
super(cachedSearches);
|
||||
this.searchRoute = searchRoute;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
if (this.value === undefined) {
|
||||
this.value = this.getInitialSearch() || '';
|
||||
}
|
||||
|
||||
return super.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the search input and the current controller's active search.
|
||||
*/
|
||||
clear() {
|
||||
super.clear();
|
||||
|
||||
if (this.getInitialSearch()) {
|
||||
this.clearInitialSearch();
|
||||
} else {
|
||||
m.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL parameters that stick between filter changes.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
stickyParams() {
|
||||
return {
|
||||
sort: m.route.param('sort'),
|
||||
q: m.route.param('q'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameters to pass to the DiscussionList component.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
params() {
|
||||
const params = this.stickyParams();
|
||||
|
||||
params.filter = m.route.param('filter');
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the index page using the given sort parameter.
|
||||
*
|
||||
* @param {String} sort
|
||||
*/
|
||||
changeSort(sort) {
|
||||
const params = this.params();
|
||||
|
||||
if (sort === Object.keys(app.discussions.sortMap())[0]) {
|
||||
delete params.sort;
|
||||
} else {
|
||||
params.sort = sort;
|
||||
}
|
||||
|
||||
m.route(app.route(this.searchRoute, params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current search query, if any. This is implemented to activate
|
||||
* the search box in the header.
|
||||
*
|
||||
* @see Search
|
||||
* @return {String}
|
||||
*/
|
||||
getInitialSearch() {
|
||||
return app.current.type.providesInitialSearch && this.params().q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the index page without a search filter. This is called when the
|
||||
* 'x' is clicked in the search box in the header.
|
||||
*
|
||||
* @see Search
|
||||
*/
|
||||
clearInitialSearch() {
|
||||
const params = this.params();
|
||||
delete params.q;
|
||||
|
||||
m.route(app.route(this.searchRoute, params));
|
||||
}
|
||||
}
|
@@ -1,94 +0,0 @@
|
||||
export default class NotificationListState {
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
|
||||
this.notificationPages = [];
|
||||
|
||||
this.loading = false;
|
||||
|
||||
this.moreResults = false;
|
||||
}
|
||||
|
||||
getNotificationPages() {
|
||||
return this.notificationPages;
|
||||
}
|
||||
|
||||
isLoading() {
|
||||
return this.loading;
|
||||
}
|
||||
|
||||
hasMoreResults() {
|
||||
return this.moreResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load notifications into the application's cache if they haven't already
|
||||
* been loaded.
|
||||
*/
|
||||
load() {
|
||||
if (this.app.session.user.newNotificationCount()) {
|
||||
this.notificationPages = [];
|
||||
}
|
||||
|
||||
if (this.notificationPages.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.app.session.user.pushAttributes({ newNotificationCount: 0 });
|
||||
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the next page of notification results.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
loadMore() {
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
const params = this.notificationPages.length > 0 ? { page: { offset: this.notificationPages.length * 10 } } : null;
|
||||
|
||||
return this.app.store
|
||||
.find('notifications', params)
|
||||
.then(this.parseResults.bind(this))
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse results and append them to the notification list.
|
||||
*
|
||||
* @param {Notification[]} results
|
||||
* @return {Notification[]}
|
||||
*/
|
||||
parseResults(results) {
|
||||
if (results.length) this.notificationPages.push(results);
|
||||
|
||||
this.moreResults = !!results.payload.links.next;
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all of the notifications as read.
|
||||
*/
|
||||
markAllAsRead() {
|
||||
if (this.notificationPages.length === 0) return;
|
||||
|
||||
this.app.session.user.pushAttributes({ unreadNotificationCount: 0 });
|
||||
|
||||
this.notificationPages.forEach((notifications) => {
|
||||
notifications.forEach((notification) => notification.pushAttributes({ isRead: true }));
|
||||
});
|
||||
|
||||
this.app.request({
|
||||
url: this.app.forum.attribute('apiUrl') + '/notifications/read',
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,35 +0,0 @@
|
||||
export default class SearchState {
|
||||
constructor(cachedSearches = []) {
|
||||
this.cachedSearches = cachedSearches;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the search value.
|
||||
*/
|
||||
clear() {
|
||||
this.setValue('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark that we have already searched for this query so that we don't
|
||||
* have to ping the endpoint again.
|
||||
*/
|
||||
cache(query) {
|
||||
this.cachedSearches.push(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this query has been searched before.
|
||||
*/
|
||||
isCached(query) {
|
||||
return this.cachedSearches.indexOf(query) !== -1;
|
||||
}
|
||||
}
|
@@ -178,7 +178,7 @@ export default {
|
||||
app.composer.show();
|
||||
|
||||
if (goToLast && app.viewingDiscussion(this) && !app.composer.isFullScreen()) {
|
||||
app.current.get('stream').goToNumber('reply');
|
||||
app.current.stream.goToNumber('reply');
|
||||
}
|
||||
|
||||
deferred.resolve(component);
|
||||
@@ -229,7 +229,13 @@ export default {
|
||||
app.history.back();
|
||||
}
|
||||
|
||||
return this.delete().then(() => app.discussions.removeDiscussion(this));
|
||||
return this.delete().then(() => {
|
||||
// If there is a discussion list in the cache, remove this discussion.
|
||||
if (app.cache.discussionList) {
|
||||
app.cache.discussionList.removeDiscussion(this);
|
||||
m.redraw();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -181,7 +181,10 @@ export default {
|
||||
// If this was the last post in the discussion, then we will assume that
|
||||
// the whole discussion was deleted too.
|
||||
if (!discussion.postIds().length) {
|
||||
app.discussions.removeDiscussion(discussion);
|
||||
// If there is a discussion list in the cache, remove this discussion.
|
||||
if (app.cache.discussionList) {
|
||||
app.cache.discussionList.removeDiscussion(discussion);
|
||||
}
|
||||
|
||||
if (app.viewingDiscussion(discussion)) {
|
||||
app.history.back();
|
||||
|
@@ -112,7 +112,7 @@ export default {
|
||||
.delete()
|
||||
.then(() => {
|
||||
this.showDeletionAlert(user, 'success');
|
||||
if (app.current.matches(UserPage, { user })) {
|
||||
if (app.current instanceof UserPage && app.current.user === user) {
|
||||
app.history.back();
|
||||
} else {
|
||||
window.location.reload();
|
||||
|
@@ -11,7 +11,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.DashboardWidget {
|
||||
.Widget {
|
||||
background: @body-bg;
|
||||
color: @text-color;
|
||||
border-radius: @border-radius;
|
||||
|
@@ -236,12 +236,16 @@
|
||||
.App-header {
|
||||
padding: 8px;
|
||||
height: @header-height;
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: @zindex-header;
|
||||
|
||||
.affix & {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
& when (@config-colored-header = true) {
|
||||
.light-contents(@header-color, @header-control-bg, @header-control-color);
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
|
||||
.NotificationList {
|
||||
overflow: hidden;
|
||||
& .loading-indicator {
|
||||
height: 100px;
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
.NotificationsDropdown {
|
||||
.Dropdown-menu {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.NotificationList-content {
|
||||
max-height: 70vh;
|
||||
|
@@ -288,7 +288,6 @@
|
||||
margin-top: -5px;
|
||||
float: right;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.transition(opacity 0.2s);
|
||||
|
||||
|
@@ -12,6 +12,7 @@ namespace Flarum\Admin;
|
||||
use Flarum\Extension\Event\Disabled;
|
||||
use Flarum\Extension\Event\Enabled;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Foundation\ErrorHandling\Registry;
|
||||
use Flarum\Foundation\ErrorHandling\Reporter;
|
||||
use Flarum\Foundation\ErrorHandling\ViewFormatter;
|
||||
@@ -49,7 +50,6 @@ class AdminServiceProvider extends AbstractServiceProvider
|
||||
|
||||
$this->app->singleton('flarum.admin.middleware', function () {
|
||||
return [
|
||||
'flarum.admin.error_handler',
|
||||
HttpMiddleware\ParseJsonBody::class,
|
||||
HttpMiddleware\StartSession::class,
|
||||
HttpMiddleware\RememberFromCookie::class,
|
||||
@@ -60,17 +60,16 @@ class AdminServiceProvider extends AbstractServiceProvider
|
||||
];
|
||||
});
|
||||
|
||||
$this->app->bind('flarum.admin.error_handler', function () {
|
||||
return new HttpMiddleware\HandleErrors(
|
||||
$this->app->make(Registry::class),
|
||||
$this->app['flarum']->inDebugMode() ? $this->app->make(WhoopsFormatter::class) : $this->app->make(ViewFormatter::class),
|
||||
$this->app->tagged(Reporter::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.admin.handler', function () {
|
||||
$this->app->singleton('flarum.admin.handler', function (Application $app) {
|
||||
$pipe = new MiddlewarePipe;
|
||||
|
||||
// All requests should first be piped through our global error handler
|
||||
$pipe->pipe(new HttpMiddleware\HandleErrors(
|
||||
$app->make(Registry::class),
|
||||
$app->inDebugMode() ? $app->make(WhoopsFormatter::class) : $app->make(ViewFormatter::class),
|
||||
$app->tagged(Reporter::class)
|
||||
));
|
||||
|
||||
foreach ($this->app->make('flarum.admin.middleware') as $middleware) {
|
||||
$pipe->pipe($this->app->make($middleware));
|
||||
}
|
||||
|
@@ -14,18 +14,12 @@ use Flarum\Frontend\Document;
|
||||
use Flarum\Group\Permission;
|
||||
use Flarum\Settings\Event\Deserializing;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
class AdminPayload
|
||||
{
|
||||
/**
|
||||
* @var Container;
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
@@ -42,20 +36,13 @@ class AdminPayload
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* @param Container $container
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param ExtensionManager $extensions
|
||||
* @param ConnectionInterface $db
|
||||
* @param Dispatcher $events
|
||||
*/
|
||||
public function __construct(
|
||||
Container $container,
|
||||
SettingsRepositoryInterface $settings,
|
||||
ExtensionManager $extensions,
|
||||
ConnectionInterface $db,
|
||||
Dispatcher $events
|
||||
) {
|
||||
$this->container = $container;
|
||||
public function __construct(SettingsRepositoryInterface $settings, ExtensionManager $extensions, ConnectionInterface $db, Dispatcher $events)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->extensions = $extensions;
|
||||
$this->db = $db;
|
||||
@@ -74,8 +61,6 @@ class AdminPayload
|
||||
$document->payload['permissions'] = Permission::map();
|
||||
$document->payload['extensions'] = $this->extensions->getExtensions()->toArray();
|
||||
|
||||
$document->payload['displayNameDrivers'] = array_keys($this->container->make('flarum.user.display_name.supported_drivers'));
|
||||
|
||||
$document->payload['phpVersion'] = PHP_VERSION;
|
||||
$document->payload['mysqlVersion'] = $this->db->selectOne('select version() as version')->version;
|
||||
}
|
||||
|
@@ -13,8 +13,10 @@ use Flarum\Api\Controller\AbstractSerializeController;
|
||||
use Flarum\Api\Serializer\AbstractSerializer;
|
||||
use Flarum\Api\Serializer\BasicDiscussionSerializer;
|
||||
use Flarum\Api\Serializer\NotificationSerializer;
|
||||
use Flarum\Event\ConfigureApiRoutes;
|
||||
use Flarum\Event\ConfigureNotificationTypes;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Foundation\ErrorHandling\JsonApiFormatter;
|
||||
use Flarum\Foundation\ErrorHandling\Registry;
|
||||
use Flarum\Foundation\ErrorHandling\Reporter;
|
||||
@@ -44,7 +46,6 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
|
||||
$this->app->singleton('flarum.api.middleware', function () {
|
||||
return [
|
||||
'flarum.api.error_handler',
|
||||
HttpMiddleware\ParseJsonBody::class,
|
||||
Middleware\FakeHttpMethods::class,
|
||||
HttpMiddleware\StartSession::class,
|
||||
@@ -56,17 +57,15 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
];
|
||||
});
|
||||
|
||||
$this->app->bind('flarum.api.error_handler', function () {
|
||||
return new HttpMiddleware\HandleErrors(
|
||||
$this->app->make(Registry::class),
|
||||
new JsonApiFormatter($this->app['flarum']->inDebugMode()),
|
||||
$this->app->tagged(Reporter::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.api.handler', function () {
|
||||
$this->app->singleton('flarum.api.handler', function (Application $app) {
|
||||
$pipe = new MiddlewarePipe;
|
||||
|
||||
$pipe->pipe(new HttpMiddleware\HandleErrors(
|
||||
$app->make(Registry::class),
|
||||
new JsonApiFormatter($app->inDebugMode()),
|
||||
$app->tagged(Reporter::class)
|
||||
));
|
||||
|
||||
foreach ($this->app->make('flarum.api.middleware') as $middleware) {
|
||||
$pipe->pipe($this->app->make($middleware));
|
||||
}
|
||||
@@ -121,5 +120,9 @@ class ApiServiceProvider extends AbstractServiceProvider
|
||||
|
||||
$callback = include __DIR__.'/routes.php';
|
||||
$callback($routes, $factory);
|
||||
|
||||
$this->app->make('events')->dispatch(
|
||||
new ConfigureApiRoutes($routes, $factory)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,53 +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\User\AssertPermissionTrait;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Mail\Mailer;
|
||||
use Illuminate\Mail\Message;
|
||||
use Laminas\Diactoros\Response\EmptyResponse;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
|
||||
class SendTestMailController implements RequestHandlerInterface
|
||||
{
|
||||
use AssertPermissionTrait;
|
||||
|
||||
protected $container;
|
||||
|
||||
protected $mailer;
|
||||
|
||||
protected $translator;
|
||||
|
||||
public function __construct(Container $container, Mailer $mailer, TranslatorInterface $translator)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->mailer = $mailer;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$actor = $request->getAttribute('actor');
|
||||
$this->assertAdmin($actor);
|
||||
|
||||
$body = $this->translator->trans('core.email.send_test.body', ['{username}' => $actor->username]);
|
||||
|
||||
$this->mailer->raw($body, function (Message $message) use ($actor) {
|
||||
$message->to($actor->email);
|
||||
$message->subject($this->translator->trans('core.email.send_test.subject'));
|
||||
});
|
||||
|
||||
return new EmptyResponse();
|
||||
}
|
||||
}
|
@@ -12,7 +12,7 @@ namespace Flarum\Api\Controller;
|
||||
use Flarum\Api\Serializer\CurrentUserSerializer;
|
||||
use Flarum\Api\Serializer\UserSerializer;
|
||||
use Flarum\User\Command\EditUser;
|
||||
use Flarum\User\Exception\NotAuthenticatedException;
|
||||
use Flarum\User\Exception\PermissionDeniedException;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
@@ -62,7 +62,7 @@ class UpdateUserController extends AbstractShowController
|
||||
$password = Arr::get($request->getParsedBody(), 'meta.password');
|
||||
|
||||
if (! $actor->checkPassword($password)) {
|
||||
throw new NotAuthenticatedException;
|
||||
throw new PermissionDeniedException;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -10,24 +10,40 @@
|
||||
namespace Flarum\Api\Serializer;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\User\Gate;
|
||||
|
||||
class DiscussionSerializer extends BasicDiscussionSerializer
|
||||
{
|
||||
/**
|
||||
* @var \Flarum\User\Gate
|
||||
*/
|
||||
protected $gate;
|
||||
|
||||
/**
|
||||
* @param \Flarum\User\Gate $gate
|
||||
*/
|
||||
public function __construct(Gate $gate)
|
||||
{
|
||||
$this->gate = $gate;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getDefaultAttributes($discussion)
|
||||
{
|
||||
$gate = $this->gate->forUser($this->actor);
|
||||
|
||||
$attributes = parent::getDefaultAttributes($discussion) + [
|
||||
'commentCount' => (int) $discussion->comment_count,
|
||||
'participantCount' => (int) $discussion->participant_count,
|
||||
'createdAt' => $this->formatDate($discussion->created_at),
|
||||
'lastPostedAt' => $this->formatDate($discussion->last_posted_at),
|
||||
'lastPostNumber' => (int) $discussion->last_post_number,
|
||||
'canReply' => $this->actor->can('reply', $discussion),
|
||||
'canRename' => $this->actor->can('rename', $discussion),
|
||||
'canDelete' => $this->actor->can('delete', $discussion),
|
||||
'canHide' => $this->actor->can('hide', $discussion)
|
||||
'canReply' => $gate->allows('reply', $discussion),
|
||||
'canRename' => $gate->allows('rename', $discussion),
|
||||
'canDelete' => $gate->allows('delete', $discussion),
|
||||
'canHide' => $gate->allows('hide', $discussion)
|
||||
];
|
||||
|
||||
if ($discussion->hidden_at) {
|
||||
|
@@ -10,9 +10,23 @@
|
||||
namespace Flarum\Api\Serializer;
|
||||
|
||||
use Flarum\Post\CommentPost;
|
||||
use Flarum\User\Gate;
|
||||
|
||||
class PostSerializer extends BasicPostSerializer
|
||||
{
|
||||
/**
|
||||
* @var \Flarum\User\Gate
|
||||
*/
|
||||
protected $gate;
|
||||
|
||||
/**
|
||||
* @param \Flarum\User\Gate $gate
|
||||
*/
|
||||
public function __construct(Gate $gate)
|
||||
{
|
||||
$this->gate = $gate;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -22,13 +36,15 @@ class PostSerializer extends BasicPostSerializer
|
||||
|
||||
unset($attributes['content']);
|
||||
|
||||
$canEdit = $this->actor->can('edit', $post);
|
||||
$gate = $this->gate->forUser($this->actor);
|
||||
|
||||
$canEdit = $gate->allows('edit', $post);
|
||||
|
||||
if ($post instanceof CommentPost) {
|
||||
if ($canEdit) {
|
||||
$attributes['content'] = $post->content;
|
||||
}
|
||||
if ($this->actor->can('viewIps', $post)) {
|
||||
if ($gate->allows('viewIps', $post)) {
|
||||
$attributes['ipAddress'] = $post->ip_address;
|
||||
}
|
||||
} else {
|
||||
@@ -46,8 +62,8 @@ class PostSerializer extends BasicPostSerializer
|
||||
|
||||
$attributes += [
|
||||
'canEdit' => $canEdit,
|
||||
'canDelete' => $this->actor->can('delete', $post),
|
||||
'canHide' => $this->actor->can('hide', $post)
|
||||
'canDelete' => $gate->allows('delete', $post),
|
||||
'canHide' => $gate->allows('hide', $post)
|
||||
];
|
||||
|
||||
return $attributes;
|
||||
|
@@ -9,8 +9,23 @@
|
||||
|
||||
namespace Flarum\Api\Serializer;
|
||||
|
||||
use Flarum\User\Gate;
|
||||
|
||||
class UserSerializer extends BasicUserSerializer
|
||||
{
|
||||
/**
|
||||
* @var \Flarum\User\Gate
|
||||
*/
|
||||
protected $gate;
|
||||
|
||||
/**
|
||||
* @param Gate $gate
|
||||
*/
|
||||
public function __construct(Gate $gate)
|
||||
{
|
||||
$this->gate = $gate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Flarum\User\User $user
|
||||
* @return array
|
||||
@@ -19,14 +34,16 @@ class UserSerializer extends BasicUserSerializer
|
||||
{
|
||||
$attributes = parent::getDefaultAttributes($user);
|
||||
|
||||
$canEdit = $this->actor->can('edit', $user);
|
||||
$gate = $this->gate->forUser($this->actor);
|
||||
|
||||
$canEdit = $gate->allows('edit', $user);
|
||||
|
||||
$attributes += [
|
||||
'joinTime' => $this->formatDate($user->joined_at),
|
||||
'discussionCount' => (int) $user->discussion_count,
|
||||
'commentCount' => (int) $user->comment_count,
|
||||
'canEdit' => $canEdit,
|
||||
'canDelete' => $this->actor->can('delete', $user),
|
||||
'canDelete' => $gate->allows('delete', $user),
|
||||
];
|
||||
|
||||
if ($user->getPreference('discloseOnline') || $this->actor->can('viewLastSeenAt', $user)) {
|
||||
|
@@ -309,15 +309,8 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
|
||||
|
||||
// List available mail drivers, available fields and validation status
|
||||
$map->get(
|
||||
'/mail/settings',
|
||||
'/mail-settings',
|
||||
'mailSettings.index',
|
||||
$route->toController(Controller\ShowMailSettingsController::class)
|
||||
);
|
||||
|
||||
// Send test mail post
|
||||
$map->post(
|
||||
'/mail/test',
|
||||
'mailTest',
|
||||
$route->toController(Controller\SendTestMailController::class)
|
||||
);
|
||||
};
|
||||
|
58
src/Console/Event/Configuring.php
Normal file
58
src/Console/Event/Configuring.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?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\Console\Event;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Symfony\Component\Console\Application;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
class Configuring
|
||||
{
|
||||
/**
|
||||
* @var Container
|
||||
*/
|
||||
public $app;
|
||||
|
||||
/**
|
||||
* @var Application
|
||||
*/
|
||||
public $console;
|
||||
|
||||
/**
|
||||
* @param Container $container
|
||||
* @param Application $console
|
||||
*/
|
||||
public function __construct(Container $container, Application $console)
|
||||
{
|
||||
$this->app = $container;
|
||||
$this->console = $console;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a console command to the flarum binary.
|
||||
*
|
||||
* @param Command|string $command
|
||||
*/
|
||||
public function addCommand($command)
|
||||
{
|
||||
if (is_string($command)) {
|
||||
$command = $this->app->make($command);
|
||||
}
|
||||
|
||||
if ($command instanceof Command) {
|
||||
$command->setLaravel($this->app);
|
||||
}
|
||||
|
||||
$this->console->add($command);
|
||||
}
|
||||
}
|
@@ -9,10 +9,12 @@
|
||||
|
||||
namespace Flarum\Console;
|
||||
|
||||
use Flarum\Console\Event\Configuring;
|
||||
use Flarum\Foundation\ErrorHandling\Registry;
|
||||
use Flarum\Foundation\ErrorHandling\Reporter;
|
||||
use Flarum\Foundation\SiteInterface;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\ConsoleEvents;
|
||||
use Symfony\Component\Console\Event\ConsoleErrorEvent;
|
||||
@@ -37,20 +39,32 @@ class Server
|
||||
$console->add($command);
|
||||
}
|
||||
|
||||
$this->handleErrors($console);
|
||||
$this->extend($console); // deprecated
|
||||
|
||||
exit($console->run());
|
||||
}
|
||||
|
||||
private function handleErrors(Application $console)
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
private function extend(Application $console)
|
||||
{
|
||||
$container = \Illuminate\Container\Container::getInstance();
|
||||
|
||||
$this->handleErrors($container, $console);
|
||||
|
||||
$events = $container->make(Dispatcher::class);
|
||||
$events->dispatch(new Configuring($container, $console));
|
||||
}
|
||||
|
||||
private function handleErrors(Container $container, Application $console)
|
||||
{
|
||||
$dispatcher = new EventDispatcher();
|
||||
|
||||
$dispatcher->addListener(ConsoleEvents::ERROR, function (ConsoleErrorEvent $event) {
|
||||
$container = Container::getInstance();
|
||||
|
||||
$dispatcher->addListener(ConsoleEvents::ERROR, function (ConsoleErrorEvent $event) use ($container) {
|
||||
/** @var Registry $registry */
|
||||
$registry = $container->make(Registry::class);
|
||||
|
||||
$error = $registry->handle($event->getError());
|
||||
|
||||
/** @var Reporter[] $reporters */
|
||||
|
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace Flarum\Database;
|
||||
|
||||
use Flarum\Event\ConfigureModelDates;
|
||||
use Flarum\Event\ConfigureModelDefaultAttributes;
|
||||
use Flarum\Event\GetModelRelationship;
|
||||
use Illuminate\Database\Eloquent\Model as Eloquent;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Arr;
|
||||
@@ -81,6 +84,11 @@ abstract class AbstractModel extends Eloquent
|
||||
$this->attributes = array_merge($this->attributes, Arr::get(static::$defaults, $class, []));
|
||||
}
|
||||
|
||||
// Deprecated in beta 13, remove in beta 14.
|
||||
static::$dispatcher->dispatch(
|
||||
new ConfigureModelDefaultAttributes($this, $this->attributes)
|
||||
);
|
||||
|
||||
$this->attributes = array_map(function ($item) {
|
||||
return is_callable($item) ? $item() : $item;
|
||||
}, $this->attributes);
|
||||
@@ -95,6 +103,10 @@ abstract class AbstractModel extends Eloquent
|
||||
*/
|
||||
public function getDates()
|
||||
{
|
||||
static::$dispatcher->dispatch(
|
||||
new ConfigureModelDates($this, $this->dates)
|
||||
);
|
||||
|
||||
$dates = $this->dates;
|
||||
|
||||
foreach (array_merge(array_reverse(class_parents($this)), [static::class]) as $class) {
|
||||
@@ -145,6 +157,11 @@ abstract class AbstractModel extends Eloquent
|
||||
return $relation($this);
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated, remove in beta 14
|
||||
return static::$dispatcher->until(
|
||||
new GetModelRelationship($this, $name)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -13,7 +13,6 @@ use Flarum\Console\AbstractCommand;
|
||||
use Flarum\Database\Migrator;
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Foundation\Paths;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
@@ -27,18 +26,18 @@ class MigrateCommand extends AbstractCommand
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* @var Paths
|
||||
* @var Application
|
||||
*/
|
||||
protected $paths;
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @param Container $container
|
||||
* @param Paths $paths
|
||||
* @param Application $application
|
||||
*/
|
||||
public function __construct(Container $container, Paths $paths)
|
||||
public function __construct(Container $container, Application $application)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->paths = $paths;
|
||||
$this->app = $application;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -92,8 +91,8 @@ class MigrateCommand extends AbstractCommand
|
||||
$this->info('Publishing assets...');
|
||||
|
||||
$this->container->make('files')->copyDirectory(
|
||||
$this->paths->vendor.'/components/font-awesome/webfonts',
|
||||
$this->paths->public.'/assets/fonts'
|
||||
$this->app->vendorPath().'/components/font-awesome/webfonts',
|
||||
$this->app->publicPath().'/assets/fonts'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ class DatabaseServiceProvider extends AbstractServiceProvider
|
||||
$this->app->singleton(Manager::class, function ($app) {
|
||||
$manager = new Manager($app);
|
||||
|
||||
$config = $this->app['flarum']->config('database');
|
||||
$config = $app->config('database');
|
||||
$config['engine'] = 'InnoDB';
|
||||
$config['prefix_indexes'] = true;
|
||||
|
||||
@@ -54,10 +54,6 @@ class DatabaseServiceProvider extends AbstractServiceProvider
|
||||
|
||||
$this->app->alias(ConnectionInterface::class, 'db.connection');
|
||||
$this->app->alias(ConnectionInterface::class, 'flarum.db');
|
||||
|
||||
$this->app->singleton(MigrationRepositoryInterface::class, function ($app) {
|
||||
return new DatabaseMigrationRepository($app['flarum.db'], 'migrations');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace Flarum\Database;
|
||||
|
||||
use Flarum\Foundation\Paths;
|
||||
use Flarum\Extension\Extension;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
|
||||
class MigrationCreator
|
||||
@@ -22,27 +22,27 @@ class MigrationCreator
|
||||
protected $files;
|
||||
|
||||
/**
|
||||
* @var Paths
|
||||
* @var string
|
||||
*/
|
||||
protected $paths;
|
||||
protected $publicPath;
|
||||
|
||||
/**
|
||||
* Create a new migrator instance.
|
||||
*
|
||||
* @param Filesystem $files
|
||||
* @param Paths $paths
|
||||
* @param string $publicPath
|
||||
*/
|
||||
public function __construct(Filesystem $files, Paths $paths)
|
||||
public function __construct(Filesystem $files, $publicPath)
|
||||
{
|
||||
$this->files = $files;
|
||||
$this->paths = $paths;
|
||||
$this->publicPath = $publicPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new migration for the given extension.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $extension
|
||||
* @param Extension $extension
|
||||
* @param string $table
|
||||
* @param bool $create
|
||||
* @return string
|
||||
@@ -105,11 +105,9 @@ class MigrationCreator
|
||||
*/
|
||||
protected function getMigrationPath($extension)
|
||||
{
|
||||
if ($extension) {
|
||||
return $this->paths->vendor.'/'.$extension.'/migrations';
|
||||
} else {
|
||||
return __DIR__.'/../../migrations';
|
||||
}
|
||||
$parent = $extension ? public_path('extensions/'.$extension) : __DIR__.'/../..';
|
||||
|
||||
return $parent.'/migrations';
|
||||
}
|
||||
|
||||
/**
|
||||
|
31
src/Database/MigrationServiceProvider.php
Normal file
31
src/Database/MigrationServiceProvider.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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\Database;
|
||||
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Foundation\Application;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
|
||||
class MigrationServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton(MigrationRepositoryInterface::class, function ($app) {
|
||||
return new DatabaseMigrationRepository($app['flarum.db'], 'migrations');
|
||||
});
|
||||
|
||||
$this->app->bind(MigrationCreator::class, function (Application $app) {
|
||||
return new MigrationCreator($app->make(Filesystem::class), $app->basePath());
|
||||
});
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ namespace Flarum\Discussion;
|
||||
use Flarum\Event\ScopeModelVisibility;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\AbstractPolicy;
|
||||
use Flarum\User\Gate;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -28,6 +29,11 @@ class DiscussionPolicy extends AbstractPolicy
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var Gate
|
||||
*/
|
||||
protected $gate;
|
||||
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
@@ -35,11 +41,13 @@ class DiscussionPolicy extends AbstractPolicy
|
||||
|
||||
/**
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param Gate $gate
|
||||
* @param Dispatcher $events
|
||||
*/
|
||||
public function __construct(SettingsRepositoryInterface $settings, Dispatcher $events)
|
||||
public function __construct(SettingsRepositoryInterface $settings, Gate $gate, Dispatcher $events)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->gate = $gate;
|
||||
$this->events = $events;
|
||||
}
|
||||
|
||||
|
90
src/Event/AbstractConfigureRoutes.php
Normal file
90
src/Event/AbstractConfigureRoutes.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?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\Event;
|
||||
|
||||
use Flarum\Http\RouteCollection;
|
||||
use Flarum\Http\RouteHandlerFactory;
|
||||
|
||||
/**
|
||||
* @deprecated Will be removed in Beta.14.
|
||||
*/
|
||||
abstract class AbstractConfigureRoutes
|
||||
{
|
||||
/**
|
||||
* @var RouteCollection
|
||||
*/
|
||||
public $routes;
|
||||
|
||||
/**
|
||||
* @var RouteHandlerFactory
|
||||
*/
|
||||
protected $route;
|
||||
|
||||
/**
|
||||
* @param RouteCollection $routes
|
||||
* @param \Flarum\Http\RouteHandlerFactory $route
|
||||
*/
|
||||
public function __construct(RouteCollection $routes, RouteHandlerFactory $route)
|
||||
{
|
||||
$this->routes = $routes;
|
||||
$this->route = $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param string $name
|
||||
* @param string $controller
|
||||
*/
|
||||
public function get($url, $name, $controller)
|
||||
{
|
||||
$this->route('get', $url, $name, $controller);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param string $name
|
||||
* @param string $controller
|
||||
*/
|
||||
public function post($url, $name, $controller)
|
||||
{
|
||||
$this->route('post', $url, $name, $controller);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param string $name
|
||||
* @param string $controller
|
||||
*/
|
||||
public function patch($url, $name, $controller)
|
||||
{
|
||||
$this->route('patch', $url, $name, $controller);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param string $name
|
||||
* @param string $controller
|
||||
*/
|
||||
public function delete($url, $name, $controller)
|
||||
{
|
||||
$this->route('delete', $url, $name, $controller);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $method
|
||||
* @param string $url
|
||||
* @param string $name
|
||||
* @param string $controller
|
||||
*/
|
||||
protected function route($method, $url, $name, $controller)
|
||||
{
|
||||
$this->routes->$method($url, $name, $this->route->toController($controller));
|
||||
}
|
||||
}
|
17
src/Event/ConfigureApiRoutes.php
Normal file
17
src/Event/ConfigureApiRoutes.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* @deprecated Will be removed in Beta.14. Use Flarum\Extend\Routes instead.
|
||||
*/
|
||||
class ConfigureApiRoutes extends AbstractConfigureRoutes
|
||||
{
|
||||
}
|
26
src/Event/ConfigureForumRoutes.php
Normal file
26
src/Event/ConfigureForumRoutes.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Event;
|
||||
|
||||
use Flarum\Forum\Controller\FrontendController;
|
||||
|
||||
/**
|
||||
* @deprecated Will be removed in Beta.14. Use Flarum\Extend\Routes or Flarum\Extend\Frontend instead.
|
||||
*/
|
||||
class ConfigureForumRoutes extends AbstractConfigureRoutes
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get($url, $name, $handler = FrontendController::class)
|
||||
{
|
||||
parent::get($url, $name, $handler);
|
||||
}
|
||||
}
|
79
src/Event/ConfigureLocales.php
Normal file
79
src/Event/ConfigureLocales.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?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\Event;
|
||||
|
||||
use DirectoryIterator;
|
||||
use Flarum\Locale\LocaleManager;
|
||||
use Illuminate\Support\Arr;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @deprecated Will be removed in Beta.14. Use Flarum\Extend\LanguagePack instead.
|
||||
*/
|
||||
class ConfigureLocales
|
||||
{
|
||||
/**
|
||||
* @var LocaleManager
|
||||
*/
|
||||
public $locales;
|
||||
|
||||
/**
|
||||
* @param LocaleManager $locales
|
||||
*/
|
||||
public function __construct(LocaleManager $locales)
|
||||
{
|
||||
$this->locales = $locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load language pack resources from the given directory.
|
||||
*
|
||||
* @param string $directory
|
||||
*/
|
||||
public function loadLanguagePackFrom($directory)
|
||||
{
|
||||
$name = $title = basename($directory);
|
||||
|
||||
if (file_exists($manifest = $directory.'/composer.json')) {
|
||||
$json = json_decode(file_get_contents($manifest), true);
|
||||
|
||||
if (empty($json)) {
|
||||
throw new RuntimeException("Error parsing composer.json in $name: ".json_last_error_msg());
|
||||
}
|
||||
|
||||
$locale = Arr::get($json, 'extra.flarum-locale.code');
|
||||
$title = Arr::get($json, 'extra.flarum-locale.title', $title);
|
||||
}
|
||||
|
||||
if (! isset($locale)) {
|
||||
throw new RuntimeException("Language pack $name must define \"extra.flarum-locale.code\" in composer.json.");
|
||||
}
|
||||
|
||||
$this->locales->addLocale($locale, $title);
|
||||
|
||||
if (! is_dir($localeDir = $directory.'/locale')) {
|
||||
throw new RuntimeException("Language pack $name must have a \"locale\" subdirectory.");
|
||||
}
|
||||
|
||||
if (file_exists($file = $localeDir.'/config.js')) {
|
||||
$this->locales->addJsFile($locale, $file);
|
||||
}
|
||||
|
||||
if (file_exists($file = $localeDir.'/config.css')) {
|
||||
$this->locales->addCssFile($locale, $file);
|
||||
}
|
||||
|
||||
foreach (new DirectoryIterator($localeDir) as $file) {
|
||||
if ($file->isFile() && in_array($file->getExtension(), ['yml', 'yaml'])) {
|
||||
$this->locales->addTranslations($locale, $file->getPathname());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
50
src/Event/ConfigureModelDates.php
Normal file
50
src/Event/ConfigureModelDates.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Event;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
|
||||
/**
|
||||
* @deprecated in beta 13, removed in beta 14
|
||||
*
|
||||
* The `ConfigureModelDates` event is called to retrieve a list of fields for a model
|
||||
* that should be converted into date objects.
|
||||
*/
|
||||
class ConfigureModelDates
|
||||
{
|
||||
/**
|
||||
* @var AbstractModel
|
||||
*/
|
||||
public $model;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $dates;
|
||||
|
||||
/**
|
||||
* @param AbstractModel $model
|
||||
* @param array $dates
|
||||
*/
|
||||
public function __construct(AbstractModel $model, array &$dates)
|
||||
{
|
||||
$this->model = $model;
|
||||
$this->dates = &$dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $model
|
||||
* @return bool
|
||||
*/
|
||||
public function isModel($model)
|
||||
{
|
||||
return $this->model instanceof $model;
|
||||
}
|
||||
}
|
47
src/Event/ConfigureModelDefaultAttributes.php
Normal file
47
src/Event/ConfigureModelDefaultAttributes.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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\Event;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
|
||||
/**
|
||||
* @deprecated in beta 13, removed in beta 14
|
||||
*/
|
||||
class ConfigureModelDefaultAttributes
|
||||
{
|
||||
/**
|
||||
* @var AbstractModel
|
||||
*/
|
||||
public $model;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $attributes;
|
||||
|
||||
/**
|
||||
* @param AbstractModel $model
|
||||
* @param array $attributes
|
||||
*/
|
||||
public function __construct(AbstractModel $model, array &$attributes)
|
||||
{
|
||||
$this->model = $model;
|
||||
$this->attributes = &$attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $model
|
||||
* @return bool
|
||||
*/
|
||||
public function isModel($model)
|
||||
{
|
||||
return $this->model instanceof $model;
|
||||
}
|
||||
}
|
51
src/Event/GetModelRelationship.php
Normal file
51
src/Event/GetModelRelationship.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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\Event;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
|
||||
/**
|
||||
* @deprecated beta 13, use the Model extender instead.
|
||||
*
|
||||
* The `GetModelRelationship` event is called to retrieve Relation object for a
|
||||
* model. Listeners should return an Eloquent Relation object.
|
||||
*/
|
||||
class GetModelRelationship
|
||||
{
|
||||
/**
|
||||
* @var AbstractModel
|
||||
*/
|
||||
public $model;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $relationship;
|
||||
|
||||
/**
|
||||
* @param AbstractModel $model
|
||||
* @param string $relationship
|
||||
*/
|
||||
public function __construct(AbstractModel $model, $relationship)
|
||||
{
|
||||
$this->model = $model;
|
||||
$this->relationship = $relationship;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $model
|
||||
* @param string $relationship
|
||||
* @return bool
|
||||
*/
|
||||
public function isRelationship($model, $relationship)
|
||||
{
|
||||
return $this->model instanceof $model && $this->relationship === $relationship;
|
||||
}
|
||||
}
|
@@ -1,38 +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 User implements ExtenderInterface
|
||||
{
|
||||
private $displayNameDrivers = [];
|
||||
|
||||
/**
|
||||
* Add a mail driver.
|
||||
*
|
||||
* @param string $identifier Identifier for display name driver. E.g. 'username' for UserNameDriver
|
||||
* @param string $driver ::class attribute of driver class, which must implement Flarum\User\DisplayName\DriverInterface
|
||||
*/
|
||||
public function displayNameDriver(string $identifier, $driver)
|
||||
{
|
||||
$this->drivers[$identifier] = $driver;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
$container->extend('flarum.user.display_name.supported_drivers', function ($existingDrivers) {
|
||||
return array_merge($existingDrivers, $this->drivers);
|
||||
});
|
||||
}
|
||||
}
|
@@ -15,7 +15,7 @@ use Flarum\Extension\Event\Disabling;
|
||||
use Flarum\Extension\Event\Enabled;
|
||||
use Flarum\Extension\Event\Enabling;
|
||||
use Flarum\Extension\Event\Uninstalled;
|
||||
use Flarum\Foundation\Paths;
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
@@ -29,10 +29,7 @@ class ExtensionManager
|
||||
{
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var Paths
|
||||
*/
|
||||
protected $paths;
|
||||
protected $app;
|
||||
|
||||
protected $container;
|
||||
|
||||
@@ -55,14 +52,14 @@ class ExtensionManager
|
||||
|
||||
public function __construct(
|
||||
SettingsRepositoryInterface $config,
|
||||
Paths $paths,
|
||||
Application $app,
|
||||
Container $container,
|
||||
Migrator $migrator,
|
||||
Dispatcher $dispatcher,
|
||||
Filesystem $filesystem
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->paths = $paths;
|
||||
$this->app = $app;
|
||||
$this->container = $container;
|
||||
$this->migrator = $migrator;
|
||||
$this->dispatcher = $dispatcher;
|
||||
@@ -74,11 +71,11 @@ class ExtensionManager
|
||||
*/
|
||||
public function getExtensions()
|
||||
{
|
||||
if (is_null($this->extensions) && $this->filesystem->exists($this->paths->vendor.'/composer/installed.json')) {
|
||||
if (is_null($this->extensions) && $this->filesystem->exists($this->app->vendorPath().'/composer/installed.json')) {
|
||||
$extensions = new Collection();
|
||||
|
||||
// Load all packages installed by composer.
|
||||
$installed = json_decode($this->filesystem->get($this->paths->vendor.'/composer/installed.json'), true);
|
||||
$installed = json_decode($this->filesystem->get($this->app->vendorPath().'/composer/installed.json'), true);
|
||||
|
||||
// Composer 2.0 changes the structure of the installed.json manifest
|
||||
$installed = $installed['packages'] ?? $installed;
|
||||
@@ -89,8 +86,8 @@ class ExtensionManager
|
||||
}
|
||||
|
||||
$path = isset($package['install-path'])
|
||||
? $this->paths->vendor.'/composer/'.$package['install-path']
|
||||
: $this->paths->vendor.'/'.Arr::get($package, 'name');
|
||||
? $this->getExtensionsDir().'/composer/'.$package['install-path']
|
||||
: $this->getExtensionsDir().'/'.Arr::get($package, 'name');
|
||||
|
||||
// Instantiates an Extension object using the package path and composer.json file.
|
||||
$extension = new Extension($path, $package);
|
||||
@@ -206,7 +203,7 @@ class ExtensionManager
|
||||
if ($extension->hasAssets()) {
|
||||
$this->filesystem->copyDirectory(
|
||||
$extension->getPath().'/assets',
|
||||
$this->paths->public.'/assets/extensions/'.$extension->getId()
|
||||
$this->app->publicPath().'/assets/extensions/'.$extension->getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -218,7 +215,7 @@ class ExtensionManager
|
||||
*/
|
||||
protected function unpublishAssets(Extension $extension)
|
||||
{
|
||||
$this->filesystem->deleteDirectory($this->paths->public.'/assets/extensions/'.$extension->getId());
|
||||
$this->filesystem->deleteDirectory($this->app->publicPath().'/assets/extensions/'.$extension->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,7 +227,7 @@ class ExtensionManager
|
||||
*/
|
||||
public function getAsset(Extension $extension, $path)
|
||||
{
|
||||
return $this->paths->public.'/assets/extensions/'.$extension->getId().$path;
|
||||
return $this->app->publicPath().'/assets/extensions/'.$extension->getId().$path;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,4 +332,14 @@ class ExtensionManager
|
||||
|
||||
return isset($enabled[$extension]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The extensions path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getExtensionsDir()
|
||||
{
|
||||
return $this->app->vendorPath();
|
||||
}
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ namespace Flarum\Extension;
|
||||
|
||||
use Flarum\Extension\Event\Disabling;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class ExtensionServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
@@ -26,8 +27,8 @@ class ExtensionServiceProvider extends AbstractServiceProvider
|
||||
// listener on the app rather than in the service provider's boot method
|
||||
// below, so that extensions have a chance to register things on the
|
||||
// container before the core boots up (and starts resolving services).
|
||||
$this->app['flarum']->booting(function () {
|
||||
$this->app->make('flarum.extensions')->extend($this->app);
|
||||
$this->app->booting(function (Container $app) {
|
||||
$app->make('flarum.extensions')->extend($app);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,6 @@
|
||||
namespace Flarum\Formatter;
|
||||
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Foundation\Paths;
|
||||
use Illuminate\Cache\Repository;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
@@ -25,7 +24,7 @@ class FormatterServiceProvider extends AbstractServiceProvider
|
||||
return new Formatter(
|
||||
new Repository($container->make('cache.filestore')),
|
||||
$container->make('events'),
|
||||
$this->app[Paths::class]->storage.'/formatter'
|
||||
$this->app->storagePath().'/formatter'
|
||||
);
|
||||
});
|
||||
|
||||
|
@@ -9,10 +9,12 @@
|
||||
|
||||
namespace Flarum\Forum;
|
||||
|
||||
use Flarum\Event\ConfigureForumRoutes;
|
||||
use Flarum\Extension\Event\Disabled;
|
||||
use Flarum\Extension\Event\Enabled;
|
||||
use Flarum\Formatter\Formatter;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Foundation\ErrorHandling\Registry;
|
||||
use Flarum\Foundation\ErrorHandling\Reporter;
|
||||
use Flarum\Foundation\ErrorHandling\ViewFormatter;
|
||||
@@ -58,7 +60,6 @@ class ForumServiceProvider extends AbstractServiceProvider
|
||||
|
||||
$this->app->singleton('flarum.forum.middleware', function () {
|
||||
return [
|
||||
'flarum.forum.error_handler',
|
||||
HttpMiddleware\ParseJsonBody::class,
|
||||
HttpMiddleware\CollectGarbage::class,
|
||||
HttpMiddleware\StartSession::class,
|
||||
@@ -70,17 +71,16 @@ class ForumServiceProvider extends AbstractServiceProvider
|
||||
];
|
||||
});
|
||||
|
||||
$this->app->bind('flarum.forum.error_handler', function () {
|
||||
return new HttpMiddleware\HandleErrors(
|
||||
$this->app->make(Registry::class),
|
||||
$this->app['flarum']->inDebugMode() ? $this->app->make(WhoopsFormatter::class) : $this->app->make(ViewFormatter::class),
|
||||
$this->app->tagged(Reporter::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->singleton('flarum.forum.handler', function () {
|
||||
$this->app->singleton('flarum.forum.handler', function (Application $app) {
|
||||
$pipe = new MiddlewarePipe;
|
||||
|
||||
// All requests should first be piped through our global error handler
|
||||
$pipe->pipe(new HttpMiddleware\HandleErrors(
|
||||
$app->make(Registry::class),
|
||||
$app->inDebugMode() ? $app->make(WhoopsFormatter::class) : $app->make(ViewFormatter::class),
|
||||
$app->tagged(Reporter::class)
|
||||
));
|
||||
|
||||
foreach ($this->app->make('flarum.forum.middleware') as $middleware) {
|
||||
$pipe->pipe($this->app->make($middleware));
|
||||
}
|
||||
@@ -186,6 +186,10 @@ class ForumServiceProvider extends AbstractServiceProvider
|
||||
|
||||
$callback = include __DIR__.'/routes.php';
|
||||
$callback($routes, $factory);
|
||||
|
||||
$this->app->make('events')->dispatch(
|
||||
new ConfigureForumRoutes($routes, $factory)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -9,22 +9,21 @@
|
||||
|
||||
namespace Flarum\Foundation;
|
||||
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
abstract class AbstractServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* @var Container
|
||||
* @var Application
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @param Container $container
|
||||
* @param Application $app
|
||||
*/
|
||||
public function __construct(Container $container)
|
||||
public function __construct(Application $app)
|
||||
{
|
||||
$this->app = $container;
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -9,33 +9,49 @@
|
||||
|
||||
namespace Flarum\Foundation;
|
||||
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Foundation\Application as ApplicationContract;
|
||||
use Illuminate\Events\EventServiceProvider;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Application
|
||||
class Application extends Container implements ApplicationContract
|
||||
{
|
||||
/**
|
||||
* The Flarum version.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const VERSION = '0.1.0-beta.14-dev';
|
||||
const VERSION = '0.1.0-beta.13';
|
||||
|
||||
/**
|
||||
* The IoC container for the Flarum application.
|
||||
* The base path for the Flarum installation.
|
||||
*
|
||||
* @var Container
|
||||
* @var string
|
||||
*/
|
||||
private $container;
|
||||
protected $basePath;
|
||||
|
||||
/**
|
||||
* The paths for the Flarum installation.
|
||||
* The public path for the Flarum installation.
|
||||
*
|
||||
* @var Paths
|
||||
* @var string
|
||||
*/
|
||||
protected $paths;
|
||||
protected $publicPath;
|
||||
|
||||
/**
|
||||
* The custom storage path defined by the developer.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $storagePath;
|
||||
|
||||
/**
|
||||
* A custom vendor path to find dependencies in non-standard environments.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $vendorPath;
|
||||
|
||||
/**
|
||||
* Indicates if the application has "booted".
|
||||
@@ -72,20 +88,34 @@ class Application
|
||||
*/
|
||||
protected $loadedProviders = [];
|
||||
|
||||
/**
|
||||
* The deferred services and their providers.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $deferredServices = [];
|
||||
|
||||
/**
|
||||
* Create a new Flarum application instance.
|
||||
*
|
||||
* @param Container $container
|
||||
* @param Paths $paths
|
||||
* @param string|null $basePath
|
||||
* @param string|null $publicPath
|
||||
*/
|
||||
public function __construct(Container $container, Paths $paths)
|
||||
public function __construct($basePath = null, $publicPath = null)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->paths = $paths;
|
||||
|
||||
$this->registerBaseBindings();
|
||||
|
||||
$this->registerBaseServiceProviders();
|
||||
|
||||
$this->registerCoreContainerAliases();
|
||||
|
||||
if ($basePath) {
|
||||
$this->setBasePath($basePath);
|
||||
}
|
||||
|
||||
if ($publicPath) {
|
||||
$this->setPublicPath($publicPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +125,7 @@ class Application
|
||||
*/
|
||||
public function config($key, $default = null)
|
||||
{
|
||||
return Arr::get($this->container->make('flarum.config'), $key, $default);
|
||||
return Arr::get($this->make('flarum.config'), $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +146,7 @@ class Application
|
||||
*/
|
||||
public function url($path = null)
|
||||
{
|
||||
$config = $this->container->make('flarum.config');
|
||||
$config = $this->make('flarum.config');
|
||||
$url = Arr::get($config, 'url', Arr::get($_SERVER, 'REQUEST_URI'));
|
||||
|
||||
if (is_array($url)) {
|
||||
@@ -134,21 +164,26 @@ class Application
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version number of the application.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function version()
|
||||
{
|
||||
return static::VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the basic bindings into the container.
|
||||
*/
|
||||
protected function registerBaseBindings()
|
||||
{
|
||||
\Illuminate\Container\Container::setInstance($this->container);
|
||||
static::setInstance($this);
|
||||
|
||||
$this->container->instance('app', $this->container);
|
||||
$this->container->alias('app', \Illluminate\Container\Container::class);
|
||||
$this->instance('app', $this);
|
||||
|
||||
$this->container->instance('flarum', $this);
|
||||
$this->container->alias('flarum', self::class);
|
||||
|
||||
$this->container->instance('flarum.paths', $this->paths);
|
||||
$this->container->alias('flarum.paths', Paths::class);
|
||||
$this->instance(Container::class, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,51 +191,171 @@ class Application
|
||||
*/
|
||||
protected function registerBaseServiceProviders()
|
||||
{
|
||||
$this->register(new EventServiceProvider($this->container));
|
||||
$this->register(new EventServiceProvider($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the base path for the application.
|
||||
*
|
||||
* @param string $basePath
|
||||
* @return $this
|
||||
*/
|
||||
public function setBasePath($basePath)
|
||||
{
|
||||
$this->basePath = rtrim($basePath, '\/');
|
||||
|
||||
$this->bindPathsInContainer();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the public path for the application.
|
||||
*
|
||||
* @param string $publicPath
|
||||
* @return $this
|
||||
*/
|
||||
public function setPublicPath($publicPath)
|
||||
{
|
||||
$this->publicPath = rtrim($publicPath, '\/');
|
||||
|
||||
$this->bindPathsInContainer();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind all of the application paths in the container.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function bindPathsInContainer()
|
||||
{
|
||||
foreach (['base', 'public', 'storage', 'vendor'] as $path) {
|
||||
$this->instance('path.'.$path, $this->{$path.'Path'}());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base path of the Laravel installation.
|
||||
*
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
public function basePath()
|
||||
{
|
||||
return $this->paths->base;
|
||||
return $this->basePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the public / web directory.
|
||||
*
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
public function publicPath()
|
||||
{
|
||||
return $this->paths->public;
|
||||
return $this->publicPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the storage directory.
|
||||
*
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
public function storagePath()
|
||||
{
|
||||
return $this->paths->storage;
|
||||
return $this->storagePath ?: $this->basePath.DIRECTORY_SEPARATOR.'storage';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the vendor directory where dependencies are installed.
|
||||
*
|
||||
* @return string
|
||||
* @deprecated Will be removed in Beta.15.
|
||||
*/
|
||||
public function vendorPath()
|
||||
{
|
||||
return $this->paths->vendor;
|
||||
return $this->vendorPath ?: $this->basePath.DIRECTORY_SEPARATOR.'vendor';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the storage directory.
|
||||
*
|
||||
* @param string $path
|
||||
* @return $this
|
||||
*/
|
||||
public function useStoragePath($path)
|
||||
{
|
||||
$this->storagePath = $path;
|
||||
|
||||
$this->instance('path.storage', $path);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the vendor directory.
|
||||
*
|
||||
* @param string $path
|
||||
* @return $this
|
||||
*/
|
||||
public function useVendorPath($path)
|
||||
{
|
||||
$this->vendorPath = $path;
|
||||
|
||||
$this->instance('path.vendor', $path);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or check the current application environment.
|
||||
*
|
||||
* @param mixed
|
||||
* @return string
|
||||
*/
|
||||
public function environment()
|
||||
{
|
||||
if (func_num_args() > 0) {
|
||||
$patterns = is_array(func_get_arg(0)) ? func_get_arg(0) : func_get_args();
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
if (Str::is($pattern, $this['env'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this['env'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we are running in the console.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function runningInConsole()
|
||||
{
|
||||
return php_sapi_name() == 'cli';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we are running unit tests.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function runningUnitTests()
|
||||
{
|
||||
return $this['env'] == 'testing';
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all of the configured providers.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerConfiguredProviders()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,7 +423,7 @@ class Application
|
||||
*/
|
||||
public function resolveProviderClass($provider)
|
||||
{
|
||||
return new $provider($this->container);
|
||||
return new $provider($this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,13 +434,106 @@ class Application
|
||||
*/
|
||||
protected function markAsRegistered($provider)
|
||||
{
|
||||
$this->container['events']->dispatch($class = get_class($provider), [$provider]);
|
||||
$this['events']->dispatch($class = get_class($provider), [$provider]);
|
||||
|
||||
$this->serviceProviders[] = $provider;
|
||||
|
||||
$this->loadedProviders[$class] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and boot all of the remaining deferred providers.
|
||||
*/
|
||||
public function loadDeferredProviders()
|
||||
{
|
||||
// We will simply spin through each of the deferred providers and register each
|
||||
// one and boot them if the application has booted. This should make each of
|
||||
// the remaining services available to this application for immediate use.
|
||||
foreach ($this->deferredServices as $service => $provider) {
|
||||
$this->loadDeferredProvider($service);
|
||||
}
|
||||
|
||||
$this->deferredServices = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the provider for a deferred service.
|
||||
*
|
||||
* @param string $service
|
||||
*/
|
||||
public function loadDeferredProvider($service)
|
||||
{
|
||||
if (! isset($this->deferredServices[$service])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$provider = $this->deferredServices[$service];
|
||||
|
||||
// If the service provider has not already been loaded and registered we can
|
||||
// register it with the application and remove the service from this list
|
||||
// of deferred services, since it will already be loaded on subsequent.
|
||||
if (! isset($this->loadedProviders[$provider])) {
|
||||
$this->registerDeferredProvider($provider, $service);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a deferred provider and service.
|
||||
*
|
||||
* @param string $provider
|
||||
* @param string $service
|
||||
*/
|
||||
public function registerDeferredProvider($provider, $service = null)
|
||||
{
|
||||
// Once the provider that provides the deferred service has been registered we
|
||||
// will remove it from our local list of the deferred services with related
|
||||
// providers so that this container does not try to resolve it out again.
|
||||
if ($service) {
|
||||
unset($this->deferredServices[$service]);
|
||||
}
|
||||
|
||||
$this->register($instance = new $provider($this));
|
||||
|
||||
if (! $this->booted) {
|
||||
$this->booting(function () use ($instance) {
|
||||
$this->bootProvider($instance);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the given type from the container.
|
||||
*
|
||||
* (Overriding Container::make)
|
||||
*
|
||||
* @param string $abstract
|
||||
* @param array $parameters
|
||||
* @return mixed
|
||||
*/
|
||||
public function make($abstract, array $parameters = [])
|
||||
{
|
||||
$abstract = $this->getAlias($abstract);
|
||||
|
||||
if (isset($this->deferredServices[$abstract])) {
|
||||
$this->loadDeferredProvider($abstract);
|
||||
}
|
||||
|
||||
return parent::make($abstract, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given abstract type has been bound.
|
||||
*
|
||||
* (Overriding Container::bound)
|
||||
*
|
||||
* @param string $abstract
|
||||
* @return bool
|
||||
*/
|
||||
public function bound($abstract)
|
||||
{
|
||||
return isset($this->deferredServices[$abstract]) || parent::bound($abstract);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the application has booted.
|
||||
*
|
||||
@@ -330,7 +578,7 @@ class Application
|
||||
protected function bootProvider(ServiceProvider $provider)
|
||||
{
|
||||
if (method_exists($provider, 'boot')) {
|
||||
return $this->container->call([$provider, 'boot']);
|
||||
return $this->call([$provider, 'boot']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,13 +621,96 @@ class Application
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the cached "compiled.php" file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCachedCompilePath()
|
||||
{
|
||||
return $this->basePath().'/bootstrap/cache/compiled.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the cached services.json file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCachedServicesPath()
|
||||
{
|
||||
return $this->basePath().'/bootstrap/cache/services.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the application is currently down for maintenance.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isDownForMaintenance()
|
||||
{
|
||||
return $this->config('offline');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the service providers that have been loaded.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLoadedProviders()
|
||||
{
|
||||
return $this->loadedProviders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the application's deferred services.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDeferredServices()
|
||||
{
|
||||
return $this->deferredServices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the application's deferred services.
|
||||
*
|
||||
* @param array $services
|
||||
* @return void
|
||||
*/
|
||||
public function setDeferredServices(array $services)
|
||||
{
|
||||
$this->deferredServices = $services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an array of services to the application's deferred services.
|
||||
*
|
||||
* @param array $services
|
||||
* @return void
|
||||
*/
|
||||
public function addDeferredServices(array $services)
|
||||
{
|
||||
$this->deferredServices = array_merge($this->deferredServices, $services);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given service is a deferred service.
|
||||
*
|
||||
* @param string $service
|
||||
* @return bool
|
||||
*/
|
||||
public function isDeferredService($service)
|
||||
{
|
||||
return isset($this->deferredServices[$service]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the core class aliases in the container.
|
||||
*/
|
||||
public function registerCoreContainerAliases()
|
||||
{
|
||||
$aliases = [
|
||||
'app' => [\Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class],
|
||||
'app' => [self::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class],
|
||||
'blade.compiler' => [\Illuminate\View\Compilers\BladeCompiler::class],
|
||||
'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class],
|
||||
'cache.store' => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class],
|
||||
@@ -399,8 +730,36 @@ class Application
|
||||
|
||||
foreach ($aliases as $key => $aliases) {
|
||||
foreach ((array) $aliases as $alias) {
|
||||
$this->container->alias($key, $alias);
|
||||
$this->alias($key, $alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the container of all bindings and resolved instances.
|
||||
*/
|
||||
public function flush()
|
||||
{
|
||||
parent::flush();
|
||||
|
||||
$this->loadedProviders = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the cached packages.php file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCachedPackagesPath()
|
||||
{
|
||||
return storage_path('app/cache/packages.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function resourcePath()
|
||||
{
|
||||
return storage_path('resources');
|
||||
}
|
||||
}
|
||||
|
@@ -10,8 +10,8 @@
|
||||
namespace Flarum\Foundation\Console;
|
||||
|
||||
use Flarum\Console\AbstractCommand;
|
||||
use Flarum\Foundation\Application;
|
||||
use Flarum\Foundation\Event\ClearingCache;
|
||||
use Flarum\Foundation\Paths;
|
||||
use Illuminate\Contracts\Cache\Store;
|
||||
|
||||
class CacheClearCommand extends AbstractCommand
|
||||
@@ -22,18 +22,18 @@ class CacheClearCommand extends AbstractCommand
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* @var Paths
|
||||
* @var Application
|
||||
*/
|
||||
protected $paths;
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @param Store $cache
|
||||
* @param Paths $paths
|
||||
* @param Application $app
|
||||
*/
|
||||
public function __construct(Store $cache, Paths $paths)
|
||||
public function __construct(Store $cache, Application $app)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
$this->paths = $paths;
|
||||
$this->app = $app;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -57,7 +57,7 @@ class CacheClearCommand extends AbstractCommand
|
||||
|
||||
$this->cache->flush();
|
||||
|
||||
$storagePath = $this->paths->storage;
|
||||
$storagePath = $this->app->storagePath();
|
||||
array_map('unlink', glob($storagePath.'/formatter/*'));
|
||||
array_map('unlink', glob($storagePath.'/locale/*'));
|
||||
|
||||
|
@@ -14,6 +14,7 @@ use Flarum\Api\ApiServiceProvider;
|
||||
use Flarum\Bus\BusServiceProvider;
|
||||
use Flarum\Console\ConsoleServiceProvider;
|
||||
use Flarum\Database\DatabaseServiceProvider;
|
||||
use Flarum\Database\MigrationServiceProvider;
|
||||
use Flarum\Discussion\DiscussionServiceProvider;
|
||||
use Flarum\Extension\ExtensionServiceProvider;
|
||||
use Flarum\Formatter\FormatterServiceProvider;
|
||||
@@ -50,7 +51,7 @@ use Psr\Log\LoggerInterface;
|
||||
class InstalledSite implements SiteInterface
|
||||
{
|
||||
/**
|
||||
* @var Paths
|
||||
* @var array
|
||||
*/
|
||||
private $paths;
|
||||
|
||||
@@ -64,7 +65,7 @@ class InstalledSite implements SiteInterface
|
||||
*/
|
||||
private $extenders = [];
|
||||
|
||||
public function __construct(Paths $paths, array $config)
|
||||
public function __construct(array $paths, array $config)
|
||||
{
|
||||
$this->paths = $paths;
|
||||
$this->config = $config;
|
||||
@@ -94,18 +95,22 @@ class InstalledSite implements SiteInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function bootLaravel(): Container
|
||||
private function bootLaravel(): Application
|
||||
{
|
||||
$container = new \Illuminate\Container\Container;
|
||||
$laravel = new Application($container, $this->paths);
|
||||
$laravel = new Application($this->paths['base'], $this->paths['public']);
|
||||
|
||||
$container->instance('env', 'production');
|
||||
$container->instance('flarum.config', $this->config);
|
||||
$container->instance('flarum.debug', $laravel->inDebugMode());
|
||||
$container->instance('config', $config = $this->getIlluminateConfig($laravel));
|
||||
$laravel->useStoragePath($this->paths['storage']);
|
||||
|
||||
$this->registerLogger($container);
|
||||
$this->registerCache($container);
|
||||
if (isset($this->paths['vendor'])) {
|
||||
$laravel->useVendorPath($this->paths['vendor']);
|
||||
}
|
||||
|
||||
$laravel->instance('env', 'production');
|
||||
$laravel->instance('flarum.config', $this->config);
|
||||
$laravel->instance('config', $config = $this->getIlluminateConfig($laravel));
|
||||
|
||||
$this->registerLogger($laravel);
|
||||
$this->registerCache($laravel);
|
||||
|
||||
$laravel->register(AdminServiceProvider::class);
|
||||
$laravel->register(ApiServiceProvider::class);
|
||||
@@ -124,6 +129,7 @@ class InstalledSite implements SiteInterface
|
||||
$laravel->register(HttpServiceProvider::class);
|
||||
$laravel->register(LocaleServiceProvider::class);
|
||||
$laravel->register(MailServiceProvider::class);
|
||||
$laravel->register(MigrationServiceProvider::class);
|
||||
$laravel->register(NotificationServiceProvider::class);
|
||||
$laravel->register(PostServiceProvider::class);
|
||||
$laravel->register(QueueServiceProvider::class);
|
||||
@@ -135,18 +141,18 @@ class InstalledSite implements SiteInterface
|
||||
$laravel->register(ValidationServiceProvider::class);
|
||||
$laravel->register(ViewServiceProvider::class);
|
||||
|
||||
$laravel->booting(function () use ($container) {
|
||||
$laravel->booting(function (Container $app) {
|
||||
// Run all local-site extenders before booting service providers
|
||||
// (but after those from "real" extensions, which have been set up
|
||||
// in a service provider above).
|
||||
foreach ($this->extenders as $extension) {
|
||||
$extension->extend($container);
|
||||
$extension->extend($app);
|
||||
}
|
||||
});
|
||||
|
||||
$laravel->boot();
|
||||
|
||||
return $container;
|
||||
return $laravel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,7 +164,7 @@ class InstalledSite implements SiteInterface
|
||||
return new ConfigRepository([
|
||||
'view' => [
|
||||
'paths' => [],
|
||||
'compiled' => $this->paths->storage.'/views',
|
||||
'compiled' => $this->paths['storage'].'/views',
|
||||
],
|
||||
'mail' => [
|
||||
'driver' => 'mail',
|
||||
@@ -169,43 +175,43 @@ class InstalledSite implements SiteInterface
|
||||
'disks' => [
|
||||
'flarum-assets' => [
|
||||
'driver' => 'local',
|
||||
'root' => $this->paths->public.'/assets',
|
||||
'root' => $this->paths['public'].'/assets',
|
||||
'url' => $app->url('assets')
|
||||
],
|
||||
'flarum-avatars' => [
|
||||
'driver' => 'local',
|
||||
'root' => $this->paths->public.'/assets/avatars'
|
||||
'root' => $this->paths['public'].'/assets/avatars'
|
||||
]
|
||||
]
|
||||
],
|
||||
'session' => [
|
||||
'lifetime' => 120,
|
||||
'files' => $this->paths->storage.'/sessions',
|
||||
'files' => $this->paths['storage'].'/sessions',
|
||||
'cookie' => 'session'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
private function registerLogger(Container $container)
|
||||
private function registerLogger(Application $app)
|
||||
{
|
||||
$logPath = $this->paths->storage.'/logs/flarum.log';
|
||||
$logPath = $this->paths['storage'].'/logs/flarum.log';
|
||||
$handler = new RotatingFileHandler($logPath, Logger::INFO);
|
||||
$handler->setFormatter(new LineFormatter(null, null, true, true));
|
||||
|
||||
$container->instance('log', new Logger('flarum', [$handler]));
|
||||
$container->alias('log', LoggerInterface::class);
|
||||
$app->instance('log', new Logger($app->environment(), [$handler]));
|
||||
$app->alias('log', LoggerInterface::class);
|
||||
}
|
||||
|
||||
private function registerCache(Container $container)
|
||||
private function registerCache(Application $app)
|
||||
{
|
||||
$container->singleton('cache.store', function ($container) {
|
||||
return new CacheRepository($container->make('cache.filestore'));
|
||||
$app->singleton('cache.store', function ($app) {
|
||||
return new CacheRepository($app->make('cache.filestore'));
|
||||
});
|
||||
$container->alias('cache.store', Repository::class);
|
||||
$app->alias('cache.store', Repository::class);
|
||||
|
||||
$container->singleton('cache.filestore', function () {
|
||||
return new FileStore(new Filesystem, $this->paths->storage.'/cache');
|
||||
$app->singleton('cache.filestore', function () {
|
||||
return new FileStore(new Filesystem, $this->paths['storage'].'/cache');
|
||||
});
|
||||
$container->alias('cache.filestore', Store::class);
|
||||
$app->alias('cache.filestore', Store::class);
|
||||
}
|
||||
}
|
||||
|
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Foundation;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @property-read string base
|
||||
* @property-read string public
|
||||
* @property-read string storage
|
||||
* @property-read string vendor
|
||||
*/
|
||||
class Paths
|
||||
{
|
||||
private $paths;
|
||||
|
||||
public function __construct(array $paths)
|
||||
{
|
||||
if (! isset($paths['base'], $paths['public'], $paths['storage'])) {
|
||||
throw new InvalidArgumentException(
|
||||
'Paths array requires keys base, public and storage'
|
||||
);
|
||||
}
|
||||
|
||||
$this->paths = array_map(function ($path) {
|
||||
return rtrim($path, '\/');
|
||||
}, $paths);
|
||||
|
||||
// Assume a standard Composer directory structure unless specified
|
||||
$this->paths['vendor'] = $this->vendor ?? $this->base.'/vendor';
|
||||
}
|
||||
|
||||
public function __get($name): ?string
|
||||
{
|
||||
return $this->paths[$name] ?? null;
|
||||
}
|
||||
}
|
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace Flarum\Foundation;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
class Site
|
||||
@@ -19,14 +20,18 @@ class Site
|
||||
*/
|
||||
public static function fromPaths(array $paths)
|
||||
{
|
||||
$paths = new Paths($paths);
|
||||
if (! isset($paths['base'], $paths['public'], $paths['storage'])) {
|
||||
throw new InvalidArgumentException(
|
||||
'Paths array requires keys base, public and storage'
|
||||
);
|
||||
}
|
||||
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
if (static::hasConfigFile($paths->base)) {
|
||||
if (static::hasConfigFile($paths['base'])) {
|
||||
return (
|
||||
new InstalledSite($paths, static::loadConfig($paths->base))
|
||||
)->extendWith(static::loadExtenders($paths->base));
|
||||
new InstalledSite($paths, static::loadConfig($paths['base']))
|
||||
)->extendWith(static::loadExtenders($paths['base']));
|
||||
} else {
|
||||
return new UninstalledSite($paths);
|
||||
}
|
||||
|
@@ -16,7 +16,6 @@ use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\Settings\UninstalledSettingsRepository;
|
||||
use Flarum\User\SessionServiceProvider;
|
||||
use Illuminate\Config\Repository as ConfigRepository;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Filesystem\FilesystemServiceProvider;
|
||||
use Illuminate\Validation\ValidationServiceProvider;
|
||||
@@ -31,11 +30,11 @@ use Psr\Log\LoggerInterface;
|
||||
class UninstalledSite implements SiteInterface
|
||||
{
|
||||
/**
|
||||
* @var Paths
|
||||
* @var array
|
||||
*/
|
||||
private $paths;
|
||||
|
||||
public function __construct(Paths $paths)
|
||||
public function __construct(array $paths)
|
||||
{
|
||||
$this->paths = $paths;
|
||||
}
|
||||
@@ -52,17 +51,21 @@ class UninstalledSite implements SiteInterface
|
||||
);
|
||||
}
|
||||
|
||||
private function bootLaravel(): Container
|
||||
private function bootLaravel(): Application
|
||||
{
|
||||
$container = new \Illuminate\Container\Container;
|
||||
$laravel = new Application($container, $this->paths);
|
||||
$laravel = new Application($this->paths['base'], $this->paths['public']);
|
||||
|
||||
$container->instance('env', 'production');
|
||||
$container->instance('flarum.config', []);
|
||||
$container->instance('flarum.debug', $laravel->inDebugMode());
|
||||
$container->instance('config', $config = $this->getIlluminateConfig());
|
||||
$laravel->useStoragePath($this->paths['storage']);
|
||||
|
||||
$this->registerLogger($container);
|
||||
if (isset($this->paths['vendor'])) {
|
||||
$laravel->useVendorPath($this->paths['vendor']);
|
||||
}
|
||||
|
||||
$laravel->instance('env', 'production');
|
||||
$laravel->instance('flarum.config', []);
|
||||
$laravel->instance('config', $config = $this->getIlluminateConfig());
|
||||
|
||||
$this->registerLogger($laravel);
|
||||
|
||||
$laravel->register(ErrorServiceProvider::class);
|
||||
$laravel->register(LocaleServiceProvider::class);
|
||||
@@ -72,12 +75,12 @@ class UninstalledSite implements SiteInterface
|
||||
|
||||
$laravel->register(InstallServiceProvider::class);
|
||||
|
||||
$container->singleton(
|
||||
$laravel->singleton(
|
||||
SettingsRepositoryInterface::class,
|
||||
UninstalledSettingsRepository::class
|
||||
);
|
||||
|
||||
$container->singleton('view', function ($app) {
|
||||
$laravel->singleton('view', function ($app) {
|
||||
$engines = new EngineResolver();
|
||||
$engines->register('php', function () {
|
||||
return new PhpEngine();
|
||||
@@ -94,7 +97,7 @@ class UninstalledSite implements SiteInterface
|
||||
|
||||
$laravel->boot();
|
||||
|
||||
return $container;
|
||||
return $laravel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +108,7 @@ class UninstalledSite implements SiteInterface
|
||||
return new ConfigRepository([
|
||||
'session' => [
|
||||
'lifetime' => 120,
|
||||
'files' => $this->paths->storage.'/sessions',
|
||||
'files' => $this->paths['storage'].'/sessions',
|
||||
'cookie' => 'session'
|
||||
],
|
||||
'view' => [
|
||||
@@ -114,13 +117,13 @@ class UninstalledSite implements SiteInterface
|
||||
]);
|
||||
}
|
||||
|
||||
private function registerLogger(Container $container)
|
||||
private function registerLogger(Application $app)
|
||||
{
|
||||
$logPath = $this->paths->storage.'/logs/flarum-installer.log';
|
||||
$logPath = $this->paths['storage'].'/logs/flarum-installer.log';
|
||||
$handler = new StreamHandler($logPath, Logger::DEBUG);
|
||||
$handler->setFormatter(new LineFormatter(null, null, true, true));
|
||||
|
||||
$container->instance('log', new Logger('Flarum Installer', [$handler]));
|
||||
$container->alias('log', LoggerInterface::class);
|
||||
$app->instance('log', new Logger('Flarum Installer', [$handler]));
|
||||
$app->alias('log', LoggerInterface::class);
|
||||
}
|
||||
}
|
||||
|
@@ -50,12 +50,16 @@ class JsCompiler extends RevisionCompiler
|
||||
}
|
||||
|
||||
// Add a comment to the end of our file to point to the sourcemap
|
||||
// we just constructed. We will then store the JS file and the map
|
||||
// in our asset directory.
|
||||
// we just constructed. We will then write the JS file, save the
|
||||
// map to a temporary location, and then move it to the asset dir.
|
||||
$output[] = '//# sourceMappingURL='.$this->assetsDir->url($mapFile);
|
||||
|
||||
$this->assetsDir->put($file, implode("\n", $output));
|
||||
$this->assetsDir->put($mapFile, json_encode($map, JSON_UNESCAPED_SLASHES));
|
||||
|
||||
$mapTemp = @tempnam(storage_path('tmp'), $mapFile);
|
||||
$map->save($mapTemp);
|
||||
$this->assetsDir->put($mapFile, file_get_contents($mapTemp));
|
||||
@unlink($mapTemp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@@ -10,7 +10,6 @@
|
||||
namespace Flarum\Frontend;
|
||||
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Foundation\Paths;
|
||||
use Flarum\Frontend\Compiler\Source\SourceCollector;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
@@ -22,16 +21,14 @@ class FrontendServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
$this->app->singleton('flarum.assets.factory', function () {
|
||||
return function (string $name) {
|
||||
$paths = $this->app[Paths::class];
|
||||
|
||||
$assets = new Assets(
|
||||
$name,
|
||||
$this->app->make('filesystem')->disk('flarum-assets'),
|
||||
$paths->storage
|
||||
$this->app->storagePath()
|
||||
);
|
||||
|
||||
$assets->setLessImportDirs([
|
||||
$paths->vendor.'/components/font-awesome/less' => ''
|
||||
$this->app->vendorPath().'/components/font-awesome/less' => ''
|
||||
]);
|
||||
|
||||
$assets->css([$this, 'addBaseCss']);
|
||||
|
@@ -41,6 +41,19 @@ class GroupRepository
|
||||
return $this->scopeVisibleTo($query, $actor)->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a group by name.
|
||||
*
|
||||
* @param string $name
|
||||
* @return User|null
|
||||
*/
|
||||
public function findByName($name, User $actor = null)
|
||||
{
|
||||
$query = Group::where('name_singular', $name)->orWhere('name_plural', $name);
|
||||
|
||||
return $this->scopeVisibleTo($query, $actor)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include records that are visible to a user.
|
||||
*
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user