mirror of
https://github.com/flarum/core.git
synced 2025-08-18 14:22:02 +02:00
Compare commits
113 Commits
ck/floatin
...
dk/harden-
Author | SHA1 | Date | |
---|---|---|---|
|
e3435a3f30 | ||
|
cd0a31a6e7 | ||
|
43d6b3104d | ||
|
33bd99d376 | ||
|
eb4b18a979 | ||
|
b62debf031 | ||
|
1f2411e15e | ||
|
d99df936b1 | ||
|
9716a15c31 | ||
|
5e2340bf10 | ||
|
c84939b19c | ||
|
4974c91481 | ||
|
f67149bb06 | ||
|
a2d77d7b81 | ||
|
da4264c8a3 | ||
|
0f9526ba9f | ||
|
e77365f32f | ||
|
c7c456cb3e | ||
|
fb51fb4e6d | ||
|
5b7d364b87 | ||
|
39a6106854 | ||
|
9e3699ea47 | ||
|
b6f0b01307 | ||
|
548f1321f1 | ||
|
e376cf2079 | ||
|
286027ff27 | ||
|
e52b769ceb | ||
|
b1f166d82a | ||
|
63675c81d6 | ||
|
f76524a5de | ||
|
c006931798 | ||
|
a5ec39b5cf | ||
|
c75db75efe | ||
|
300dadff60 | ||
|
94d69fe15f | ||
|
da598db376 | ||
|
d31e0573f8 | ||
|
2968341f77 | ||
|
9839370701 | ||
|
40dc6d0feb | ||
|
945f6478b5 | ||
|
69a10c97be | ||
|
0074f0c984 | ||
|
19465fb522 | ||
|
0fe7723a7f | ||
|
fbe2813378 | ||
|
4b69a35260 | ||
|
5e8155e1cc | ||
|
0f0f2b6d4e | ||
|
3dae397c65 | ||
|
7025a7f5e0 | ||
|
12f6b1b375 | ||
|
2de57af7c8 | ||
|
1c4817a0b3 | ||
|
0eefbf0374 | ||
|
90c0bc410e | ||
|
d642fb531c | ||
|
706eaeda41 | ||
|
3cc18c1da2 | ||
|
8dd57ffed2 | ||
|
d29495203b | ||
|
783c563305 | ||
|
908d087e00 | ||
|
374189d48e | ||
|
fe8dda6fd0 | ||
|
cd9ee48af6 | ||
|
2e9078a7cf | ||
|
0cc12aed95 | ||
|
59fdd7628a | ||
|
298f6c39f2 | ||
|
233b97329c | ||
|
1b5b143930 | ||
|
0d139e6133 | ||
|
0e6a60bd5b | ||
|
6e4c75eba6 | ||
|
386f3d3db1 | ||
|
9fffb8ec1a | ||
|
92e590f1ab | ||
|
098908cb4a | ||
|
901846d0cf | ||
|
5a3aefb76c | ||
|
cf2a636e81 | ||
|
a8ba510655 | ||
|
9c3b6c596f | ||
|
2310d782a3 | ||
|
e9642250ae | ||
|
a6dd545dbc | ||
|
a64c39835a | ||
|
db0d8e89c7 | ||
|
4e126708e9 | ||
|
b88a7cb33b | ||
|
a2f52c09fd | ||
|
30017eef09 | ||
|
d0ffa26b0b | ||
|
612a57c466 | ||
|
91e8b56961 | ||
|
ba9665e9db | ||
|
8eea0985a4 | ||
|
1bcb9d3ea1 | ||
|
2c3e1f9923 | ||
|
bc607e089e | ||
|
91d5d9c176 | ||
|
3aa118ab94 | ||
|
4b0ad6972d | ||
|
84ded0ce50 | ||
|
725863a6e2 | ||
|
c81f629b0b | ||
|
15cbe4daaa | ||
|
ddac07d991 | ||
|
08ba2599d7 | ||
|
8eef7230e9 | ||
|
a61f9e7328 | ||
|
a65e1de641 |
@@ -15,5 +15,5 @@ indent_size = 2
|
|||||||
[*.{diff,md}]
|
[*.{diff,md}]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[*.{php,xml}]
|
[*.{php,xml,json}]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -7,10 +7,24 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
name: JS / Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- name: Check out code
|
||||||
- uses: flarum/action-build@master
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Restore npm cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('js/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
|
# Our action will install npm, cd into `./js`, run `npm run build`,
|
||||||
|
# then commit and upload any changes
|
||||||
|
- name: Build production JS
|
||||||
|
uses: flarum/action-build@master
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
76
.github/workflows/codeql-analysis.yml
vendored
Normal file
76
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
# Run on:
|
||||||
|
# - pushes to master, or
|
||||||
|
# - PRs with a base of `master`
|
||||||
|
# - which do not **only** consist of changes to .md or .less files
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
paths-ignore:
|
||||||
|
- '**/*.md'
|
||||||
|
- '**/*.less'
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
paths-ignore:
|
||||||
|
- '**/*.md'
|
||||||
|
- '**/*.less'
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 1,3,5'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze / ${{ matrix.language }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'javascript' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
|
# Learn more:
|
||||||
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
21
.github/workflows/lint.yml
vendored
21
.github/workflows/lint.yml
vendored
@@ -1,6 +1,7 @@
|
|||||||
name: Lint
|
name: Lint
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- 'js/src/**'
|
- 'js/src/**'
|
||||||
@@ -10,22 +11,18 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prettier:
|
prettier:
|
||||||
|
name: JS / Prettier
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
name: JS / Prettier
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: "12"
|
node-version: "14"
|
||||||
|
|
||||||
- name: Install JS dependencies
|
- name: Check JS formatting
|
||||||
run: npm ci
|
run: npx prettier --check src
|
||||||
working-directory: ./js
|
|
||||||
|
|
||||||
- name: Check JS code for formatting
|
|
||||||
run: node_modules/.bin/prettier --check src
|
|
||||||
working-directory: ./js
|
working-directory: ./js
|
||||||
|
45
.github/workflows/pr_size_change.yml
vendored
Normal file
45
.github/workflows/pr_size_change.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: Bundle size checker
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "js/**"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "js/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bundlewatch:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
name: Bundlewatch
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: "14"
|
||||||
|
|
||||||
|
- name: Use npm v7
|
||||||
|
run: sudo npm install -g npm@7.x.x
|
||||||
|
|
||||||
|
- name: Install JS dependencies
|
||||||
|
# We need to use `npm install` here. If we don't, the workflow will fail.
|
||||||
|
run: npm install
|
||||||
|
working-directory: ./js
|
||||||
|
|
||||||
|
- name: Build production assets
|
||||||
|
run: npm run build
|
||||||
|
working-directory: ./js
|
||||||
|
|
||||||
|
- name: Check bundle size change
|
||||||
|
run: node_modules/.bin/bundlewatch --config .bundlewatch.config.json
|
||||||
|
working-directory: ./js
|
||||||
|
env:
|
||||||
|
BUNDLEWATCH_GITHUB_TOKEN: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
|
||||||
|
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
15
.github/workflows/test.yml
vendored
15
.github/workflows/test.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php: ['7.2', '7.3', '7.4', '8.0']
|
php: [7.3, 7.4, '8.0']
|
||||||
service: ['mysql:5.7', mariadb]
|
service: ['mysql:5.7', mariadb]
|
||||||
prefix: ['', flarum_]
|
prefix: ['', flarum_]
|
||||||
|
|
||||||
@@ -21,12 +21,6 @@ jobs:
|
|||||||
prefixStr: (prefix)
|
prefixStr: (prefix)
|
||||||
|
|
||||||
exclude:
|
exclude:
|
||||||
- php: 7.2
|
|
||||||
service: 'mysql:5.7'
|
|
||||||
prefix: flarum_
|
|
||||||
- php: 7.2
|
|
||||||
service: mariadb
|
|
||||||
prefix: flarum_
|
|
||||||
- php: 7.3
|
- php: 7.3
|
||||||
service: 'mysql:5.7'
|
service: 'mysql:5.7'
|
||||||
prefix: flarum_
|
prefix: flarum_
|
||||||
@@ -49,10 +43,11 @@ jobs:
|
|||||||
name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
|
name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@0b9d33cd0782337377999751fc10ea079fdd7104 # pin@v2
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php }}
|
php-version: ${{ matrix.php }}
|
||||||
coverage: xdebug
|
coverage: xdebug
|
||||||
@@ -80,3 +75,5 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Composer tests
|
- name: Run Composer tests
|
||||||
run: composer test
|
run: composer test
|
||||||
|
env:
|
||||||
|
COMPOSER_PROCESS_TIMEOUT: 600
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@ composer.phar
|
|||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
tests/.phpunit.result.cache
|
||||||
/tests/integration/tmp
|
/tests/integration/tmp
|
||||||
.vagrant
|
.vagrant
|
||||||
.idea/*
|
.idea/*
|
||||||
|
@@ -12,7 +12,3 @@ disabled:
|
|||||||
- phpdoc_order
|
- phpdoc_order
|
||||||
- phpdoc_separation
|
- phpdoc_separation
|
||||||
- phpdoc_types
|
- phpdoc_types
|
||||||
|
|
||||||
finder:
|
|
||||||
exclude:
|
|
||||||
- "stubs"
|
|
||||||
|
93
CHANGELOG.md
93
CHANGELOG.md
@@ -1,5 +1,96 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.1.0-beta.16](https://github.com/flarum/core/compare/v0.1.0-beta.15...v0.1.0-beta.16)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Allow event subscribers (https://github.com/flarum/core/pull/2535)
|
||||||
|
- Allow Settings extender to have a default value (https://github.com/flarum/core/pull/2495)
|
||||||
|
- Allow hooking into the sending of notifications before being send (https://github.com/flarum/core/pull/2533)
|
||||||
|
- PHP 8 support (https://github.com/flarum/core/pull/2507)
|
||||||
|
- Search extender (https://github.com/flarum/core/pull/2483)
|
||||||
|
- User badges to post preview (https://github.com/flarum/core/pull/2555)
|
||||||
|
- Optional extension dependencies allow a booting order (https://github.com/flarum/core/pull/2579)
|
||||||
|
- Auth extender (https://github.com/flarum/core/pull/2176)
|
||||||
|
- `X-Powered-By` header added to allow indexers easier data aggregation of Flarum adoption (https://github.com/flarum/core/pull/2618)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Run integration tests in transaction (https://github.com/flarum/core/pull/2304)
|
||||||
|
- Allow policies to return a boolean for simplified allow/deny (https://github.com/flarum/core/pull/2534)
|
||||||
|
- Converted highlight helper to typescript (https://github.com/flarum/core/pull/2532)
|
||||||
|
- Add accessibility attributes to Mark as Read button (https://github.com/flarum/core/pull/2564)
|
||||||
|
- Dismiss errors on change email modal upon a new request ([00913d5](https://github.com/flarum/core/commit/00913d5b0be2172cfce1f16aaf64a24f3d2e6d4b))
|
||||||
|
- Disabled extensions now are marked with a red circle instead of a red dot (https://github.com/flarum/core/pull/2562)
|
||||||
|
- Extension dependency errors now show the extension title instead of the ID (https://github.com/flarum/core/pull/2563)
|
||||||
|
- Change `mutate` method on ApiSerializer extender to `attributes` (https://github.com/flarum/core/pull/2578)
|
||||||
|
- Moved locale files to the core from the language pack (https://github.com/flarum/core/pull/2408)
|
||||||
|
- AdminPage extensibility and generic improvements (https://github.com/flarum/core/pull/2593)
|
||||||
|
- Remove entry of authors, link to https://flarum.org/team (https://github.com/flarum/core/pull/2625)
|
||||||
|
- Search and filtering are split (https://github.com/flarum/core/pull/2454)
|
||||||
|
- Move IP identification into a middleware (https://github.com/flarum/core/pull/2624)
|
||||||
|
- Editor Driver abstraction introduced (https://github.com/flarum/core/pull/2594)
|
||||||
|
- Allow overriding routes (https://github.com/flarum/core/pull/2577)
|
||||||
|
- Split user edit permissions into permissions for editing of user credentials, username, groups and suspending (https://github.com/flarum/core/pull/2620)
|
||||||
|
- Reduced number of admin extension categories (https://github.com/flarum/core/pull/2604)
|
||||||
|
- Move search related classes to a dedicated Query namespace (https://github.com/flarum/core/pull/2645)
|
||||||
|
- Rewrite common helpers into typescript (https://github.com/flarum/core/pull/2541)
|
||||||
|
- `TextEditor` is moved to the common namespace for use in the admin frontend (https://github.com/flarum/core/pull/2649)
|
||||||
|
- Update Laravel/Illuminate components to 8 (https://github.com/flarum/core/pull/2576)
|
||||||
|
- Eager load relations in discussion listing to improve performance (https://github.com/flarum/core/pull/2639)
|
||||||
|
- Adopt flarum/testing package (https://github.com/flarum/core/pull/2545)
|
||||||
|
- Replace `user` gambit with `author` gambit ([612a57c](https://github.com/flarum/core/commit/612a57c4664415a3ea120103483645c32acc6f12))
|
||||||
|
- Posts page of on user profile loads posts using username instead of id ([30017ee](https://github.com/flarum/core/commit/30017eef09ae9e78640c4e2cacd4909fffa8d775))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Transform css breaks iOS scroll functionality (https://github.com/flarum/core/pull/2527)
|
||||||
|
- Composer header is hidden on mobile devices (https://github.com/flarum/core/pull/2279)
|
||||||
|
- Cannot delete a post or discussion of a deleted user (https://github.com/flarum/core/pull/2521)
|
||||||
|
- DiscussionListPane jumps around not keeping the scroll position (https://github.com/flarum/core/pull/2402)
|
||||||
|
- Infinite scroll on notifications dropdown broken (https://github.com/flarum/core/pull/2524)
|
||||||
|
- The show language selector switch remains toggled on ([9347b12](https://github.com/flarum/core/commit/9347b12b47bf4ab97ffb7ca92673604b237c1012))
|
||||||
|
- Model Visibility extender throws exception on extensions that aren't installed or enabled (https://github.com/flarum/core/pull/2580)
|
||||||
|
- Extensions are marked as enabled when enabling fails to unmet extension dependencies (https://github.com/flarum/core/pull/2558)
|
||||||
|
- Routes to admin extension pages without a valid ID break the admin page (https://github.com/flarum/core/pull/2584)
|
||||||
|
- Disabled fieldset use an incorrect CSS property `disallowed` (https://github.com/flarum/core/pull/2585)
|
||||||
|
- Scrolling to a post that is already loaded the Load More button shows and does not trigger (https://github.com/flarum/core/pull/2388)
|
||||||
|
- Opening discussions on some mobile devices require a double tap (https://github.com/flarum/core/pull/2607)
|
||||||
|
- iOS devices show erratic behavior in the post stream while updating (https://github.com/flarum/core/pull/2548)
|
||||||
|
- Small mobile screens partially hides the composer when the keyboard is open (https://github.com/flarum/core/pull/2631)
|
||||||
|
- Clearing cache does not clear the template cache in storage/views (https://github.com/flarum/core/pull/2648)
|
||||||
|
- Boot errors show critical information (https://github.com/flarum/core/pull/2633)
|
||||||
|
- List user endpoint discloses last online even if user choose against it (https://github.com/flarum/core/pull/2634)
|
||||||
|
- Group gambit disclosed hidden groups (https://github.com/flarum/core/pull/2657)
|
||||||
|
- Search results on small windows not fully visible (https://github.com/flarum/core/pull/2650)
|
||||||
|
- Composer goes off screen on Safari when starting to type (https://github.com/flarum/core/pull/2660)
|
||||||
|
- A search that has no results shows the search results dropdown ([b88a7cb](https://github.com/flarum/core/commit/b88a7cb33b56e318f11670e9e2d563aef94db039))
|
||||||
|
- The composer modal moves around when typing on Safari ([a64c398](https://github.com/flarum/core/commit/a64c39835aba43e831209609f4a9638ae589aa41))
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Deprecated CSRF wildcard path match
|
||||||
|
- Deprecated policy and visibility scoping events
|
||||||
|
- Deprecated post types event
|
||||||
|
- Deprecated validation events
|
||||||
|
- Deprecated notification events
|
||||||
|
- Deprecated floodgate
|
||||||
|
- Deprecated user preferences event
|
||||||
|
- Deprecated formatting events
|
||||||
|
- Deprecated api events
|
||||||
|
- Deprecated bootstrap.php support
|
||||||
|
- PHP 7.2 support (https://github.com/flarum/core/pull/2507)
|
||||||
|
- Bidi attribute in the rendered HTML (https://github.com/flarum/core/pull/2602)
|
||||||
|
- `AccessToken::find`, use `AccessToken::findValid` instead (https://github.com/flarum/core/pull/2651)
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- `GetModelIsPrivate` event (https://github.com/flarum/core/pull/2587)
|
||||||
|
- `CheckingPassword` event (https://github.com/flarum/core/pull/2176)
|
||||||
|
- `event()` helper (https://github.com/flarum/core/pull/2608)
|
||||||
|
- `AccessToken::generate` argument `$lifetime` (https://github.com/flarum/core/pull/2651)
|
||||||
|
- `Rememberer::remember` argument `$token` should receive an instance of `RememberAccessToken` with `AccessToken` being deprecated (https://github.com/flarum/core/pull/2651)
|
||||||
|
- `Rememberer::rememberUser` (https://github.com/flarum/core/pull/2651)
|
||||||
|
- `SessionAuthenticator::logIn` argument `$userId`, should be replaced with `AccessToken` (https://github.com/flarum/core/pull/2651)
|
||||||
|
- `TextEditor` has been moved to `common` (https://github.com/flarum/core/pull/2649)
|
||||||
|
- `UserFilter` ([91e8b56](https://github.com/flarum/core/commit/91e8b569618957c86757ef89bac666e9102db5ae))
|
||||||
|
|
||||||
|
|
||||||
## [0.1.0-beta.15](https://github.com/flarum/core/compare/v0.1.0-beta.14.1...v0.1.0-beta.15)
|
## [0.1.0-beta.15](https://github.com/flarum/core/compare/v0.1.0-beta.14.1...v0.1.0-beta.15)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -210,7 +301,7 @@
|
|||||||
- SES mail support (#2011)
|
- SES mail support (#2011)
|
||||||
- Backward compatibility layer for `Flarum\Mail\DriverInterface`, new methods from beta.12 are now required
|
- Backward compatibility layer for `Flarum\Mail\DriverInterface`, new methods from beta.12 are now required
|
||||||
- `Flarum\Util\Str` helper class
|
- `Flarum\Util\Str` helper class
|
||||||
- `Flarum\Event\ConfigureMiddleware` event
|
- `Flarum\Event\ConfigureMiddleware` event
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
- `Flarum\Event\AbstractConfigureRoutes` event class
|
- `Flarum\Event\AbstractConfigureRoutes` event class
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "flarum/core",
|
"name": "flarum/core",
|
||||||
"description": "Delightfully simple forum software.",
|
"description": "Delightfully simple forum software.",
|
||||||
"keywords": ["forum", "discussion"],
|
"keywords": [
|
||||||
|
"forum",
|
||||||
|
"discussion"
|
||||||
|
],
|
||||||
"homepage": "https://flarum.org/",
|
"homepage": "https://flarum.org/",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
@@ -17,27 +20,29 @@
|
|||||||
"docs": "https://flarum.org/docs/"
|
"docs": "https://flarum.org/docs/"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.2",
|
"php": ">=7.3",
|
||||||
"axy/sourcemap": "^0.1.4",
|
"axy/sourcemap": "^0.1.4",
|
||||||
"components/font-awesome": "^5.14.0",
|
"components/font-awesome": "^5.14.0",
|
||||||
"dflydev/fig-cookies": "^3.0.0",
|
"dflydev/fig-cookies": "^3.0.0",
|
||||||
"doctrine/dbal": "^2.7",
|
"doctrine/dbal": "^2.7",
|
||||||
|
"dragonmantank/cron-expression": "^3.1.0",
|
||||||
"franzl/whoops-middleware": "^2.0.0",
|
"franzl/whoops-middleware": "^2.0.0",
|
||||||
"illuminate/bus": "^6.0",
|
"illuminate/bus": "^8.0",
|
||||||
"illuminate/cache": "^6.0",
|
"illuminate/cache": "^8.0",
|
||||||
"illuminate/config": "^6.0",
|
"illuminate/config": "^8.0",
|
||||||
"illuminate/container": "^6.0",
|
"illuminate/console": "^8.0",
|
||||||
"illuminate/contracts": "^6.0",
|
"illuminate/container": "^8.0",
|
||||||
"illuminate/database": "^6.0",
|
"illuminate/contracts": "^8.0",
|
||||||
"illuminate/events": "^6.0",
|
"illuminate/database": "^8.0",
|
||||||
"illuminate/filesystem": "^6.0",
|
"illuminate/events": "^8.0",
|
||||||
"illuminate/hashing": "^6.0",
|
"illuminate/filesystem": "^8.0",
|
||||||
"illuminate/mail": "^6.0",
|
"illuminate/hashing": "^8.0",
|
||||||
"illuminate/queue": "^6.0",
|
"illuminate/mail": "^8.0",
|
||||||
"illuminate/session": "^6.0",
|
"illuminate/queue": "^8.0",
|
||||||
"illuminate/support": "^6.0",
|
"illuminate/session": "^8.0",
|
||||||
"illuminate/validation": "^6.0",
|
"illuminate/support": "^8.0",
|
||||||
"illuminate/view": "^6.0",
|
"illuminate/validation": "^8.0",
|
||||||
|
"illuminate/view": "^8.0",
|
||||||
"intervention/image": "^2.5.0",
|
"intervention/image": "^2.5.0",
|
||||||
"laminas/laminas-diactoros": "^2.4.1",
|
"laminas/laminas-diactoros": "^2.4.1",
|
||||||
"laminas/laminas-httphandlerrunner": "^1.2.0",
|
"laminas/laminas-httphandlerrunner": "^1.2.0",
|
||||||
@@ -54,18 +59,17 @@
|
|||||||
"psr/http-server-handler": "^1.0",
|
"psr/http-server-handler": "^1.0",
|
||||||
"psr/http-server-middleware": "^1.0",
|
"psr/http-server-middleware": "^1.0",
|
||||||
"s9e/text-formatter": "^2.3.6",
|
"s9e/text-formatter": "^2.3.6",
|
||||||
"symfony/config": "^4.3.4",
|
"symfony/config": "^5.2.2",
|
||||||
"symfony/console": "^4.3.4",
|
"symfony/console": "^5.2.2",
|
||||||
"symfony/event-dispatcher": "^4.3.4",
|
"symfony/event-dispatcher": "^5.2.2",
|
||||||
"symfony/mime": "^5.2.0",
|
"symfony/mime": "^5.2.0",
|
||||||
"symfony/translation": "^4.3.4",
|
"symfony/translation": "^5.1.5",
|
||||||
"symfony/yaml": "^4.3.4",
|
"symfony/yaml": "^5.2.2",
|
||||||
"tobscure/json-api": "^0.3.0",
|
"tobscure/json-api": "^0.3.0",
|
||||||
"wikimedia/less.php": "^3.0"
|
"wikimedia/less.php": "^3.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"mockery/mockery": "^1.3.3",
|
"flarum/testing": "^0.1.0-beta.16"
|
||||||
"phpunit/phpunit": "^8.0"
|
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
8
js/.bundlewatch.config.json
Normal file
8
js/.bundlewatch.config.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "./dist/*.js"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultCompression": "gzip"
|
||||||
|
}
|
16
js/dist/admin.js
vendored
16
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
18
js/dist/forum.js
vendored
18
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
10522
js/package-lock.json
generated
10522
js/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,32 +2,40 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"name": "@flarum/core",
|
"name": "@flarum/core",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/preset-typescript": "^7.10.1",
|
|
||||||
"@types/mithril": "^2.0.3",
|
|
||||||
"bootstrap": "^3.4.1",
|
"bootstrap": "^3.4.1",
|
||||||
"classnames": "^2.2.5",
|
"clsx": "^1.1.1",
|
||||||
"color-thief-browser": "^2.0.2",
|
"color-thief-browser": "^2.0.2",
|
||||||
"dayjs": "^1.8.28",
|
"dayjs": "^1.10.4",
|
||||||
"expose-loader": "^0.7.5",
|
"expose-loader": "^1.0.3",
|
||||||
"flarum-webpack-config": "0.1.0-beta.10",
|
"jquery": "^3.6.0",
|
||||||
"jquery": "^3.5.1",
|
|
||||||
"jquery.hotkeys": "^0.1.0",
|
"jquery.hotkeys": "^0.1.0",
|
||||||
"lodash-es": "^4.17.14",
|
"lodash-es": "^4.17.21",
|
||||||
"mithril": "^2.0.4",
|
"mithril": "^2.0.4",
|
||||||
"punycode": "^2.1.1",
|
"punycode": "^2.1.1",
|
||||||
"spin.js": "^3.1.0",
|
"spin.js": "^3.1.0",
|
||||||
"textarea-caret": "^3.1.0",
|
"textarea-caret": "^3.1.0"
|
||||||
"webpack": "^4.43.0",
|
|
||||||
"webpack-cli": "^3.3.11",
|
|
||||||
"webpack-merge": "^4.1.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"husky": "^4.2.5",
|
"@babel/preset-typescript": "^7.13.0",
|
||||||
"prettier": "2.0.2"
|
"@types/jquery": "^3.5.5",
|
||||||
|
"@types/lodash-es": "^4.17.4",
|
||||||
|
"@types/mithril": "^2.0.7",
|
||||||
|
"@types/punycode": "^2.1.0",
|
||||||
|
"@types/textarea-caret": "^3.0.0",
|
||||||
|
"bundlewatch": "^0.3.2",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"flarum-webpack-config": "0.1.0-beta.10",
|
||||||
|
"husky": "^4.3.8",
|
||||||
|
"prettier": "^2.2.1",
|
||||||
|
"webpack": "^4.46.0",
|
||||||
|
"webpack-bundle-analyzer": "^4.4.0",
|
||||||
|
"webpack-cli": "^3.3.12",
|
||||||
|
"webpack-merge": "^4.2.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "webpack --mode development --watch",
|
"dev": "webpack --mode development --watch",
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
|
"analyze": "cross-env ANALYZER=true npm run build",
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"format-check": "prettier --check src"
|
"format-check": "prettier --check src"
|
||||||
},
|
},
|
||||||
|
14
js/shims.d.ts
vendored
14
js/shims.d.ts
vendored
@@ -19,9 +19,21 @@ import Application from './src/common/Application';
|
|||||||
* to (and should not) bundle these themselves.
|
* to (and should not) bundle these themselves.
|
||||||
*/
|
*/
|
||||||
declare global {
|
declare global {
|
||||||
const $: typeof _$;
|
// $ is already defined by `@types/jquery`
|
||||||
const m: Mithril.Static;
|
const m: Mithril.Static;
|
||||||
const dayjs: typeof _dayjs;
|
const dayjs: typeof _dayjs;
|
||||||
|
|
||||||
|
// Extend JQuery with our custom functions, defined with $.fn
|
||||||
|
interface JQuery {
|
||||||
|
/**
|
||||||
|
* Creates a tooltip on a jQuery element reference.
|
||||||
|
*
|
||||||
|
* Optionally accepts placement and delay options.
|
||||||
|
*
|
||||||
|
* Returns the same reference to allow for method chaining.
|
||||||
|
*/
|
||||||
|
tooltip: (tooltipOptions?: { placement?: 'top' | 'bottom' | 'left' | 'right'; delay?: number }) => JQuery;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
8
js/src/admin/app.ts
Normal file
8
js/src/admin/app.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import Admin from './AdminApplication';
|
||||||
|
|
||||||
|
const app = new Admin();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
window.app = app;
|
||||||
|
|
||||||
|
export default app;
|
@@ -126,16 +126,17 @@ export default class AdminNav extends Component {
|
|||||||
|
|
||||||
categorizedExtensions[category].map((extension) => {
|
categorizedExtensions[category].map((extension) => {
|
||||||
const query = this.query().toUpperCase();
|
const query = this.query().toUpperCase();
|
||||||
const title = extension.extra['flarum-extension'].title;
|
const title = extension.extra['flarum-extension'].title || '';
|
||||||
|
const description = extension.description || '';
|
||||||
|
|
||||||
if (!query || title.toUpperCase().includes(query) || extension.description.toUpperCase().includes(query)) {
|
if (!query || title.toUpperCase().includes(query) || description.toUpperCase().includes(query)) {
|
||||||
items.add(
|
items.add(
|
||||||
`extension-${extension.id}`,
|
`extension-${extension.id}`,
|
||||||
<ExtensionLinkButton
|
<ExtensionLinkButton
|
||||||
href={app.route('extension', { id: extension.id })}
|
href={app.route('extension', { id: extension.id })}
|
||||||
extensionId={extension.id}
|
extensionId={extension.id}
|
||||||
className="ExtensionNavButton"
|
className="ExtensionNavButton"
|
||||||
title={extension.description}
|
title={description}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</ExtensionLinkButton>,
|
</ExtensionLinkButton>,
|
||||||
|
@@ -98,35 +98,41 @@ export default class AdminPage extends Page {
|
|||||||
return entry.call(this);
|
return entry.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
const setting = entry.setting;
|
const { setting, help, ...componentAttrs } = entry;
|
||||||
const help = entry.help;
|
|
||||||
delete entry.help;
|
delete componentAttrs.help;
|
||||||
|
|
||||||
const value = this.setting([setting])();
|
const value = this.setting([setting])();
|
||||||
if (['bool', 'checkbox', 'switch', 'boolean'].includes(entry.type)) {
|
if (['bool', 'checkbox', 'switch', 'boolean'].includes(componentAttrs.type)) {
|
||||||
return (
|
return (
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<Switch state={!!value && value !== '0'} onchange={this.settings[setting]} {...entry}>
|
<Switch state={!!value && value !== '0'} onchange={this.settings[setting]} {...componentAttrs}>
|
||||||
{entry.label}
|
{componentAttrs.label}
|
||||||
</Switch>
|
</Switch>
|
||||||
<div className="helpText">{help}</div>
|
<div className="helpText">{help}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (['select', 'dropdown', 'selectdropdown'].includes(entry.type)) {
|
} else if (['select', 'dropdown', 'selectdropdown'].includes(componentAttrs.type)) {
|
||||||
return (
|
return (
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
<label>{entry.label}</label>
|
<label>{componentAttrs.label}</label>
|
||||||
<div className="helpText">{help}</div>
|
<div className="helpText">{help}</div>
|
||||||
<Select value={value || entry.default} options={entry.options} buttonClassName="Button" onchange={this.settings[setting]} {...entry} />
|
<Select
|
||||||
|
value={value || componentAttrs.default}
|
||||||
|
options={componentAttrs.options}
|
||||||
|
buttonClassName="Button"
|
||||||
|
onchange={this.settings[setting]}
|
||||||
|
{...componentAttrs}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
entry.className = classList(['FormControl', entry.className]);
|
componentAttrs.className = classList(['FormControl', componentAttrs.className]);
|
||||||
return (
|
return (
|
||||||
<div className="Form-group">
|
<div className="Form-group">
|
||||||
{entry.label ? <label>{entry.label}</label> : ''}
|
{componentAttrs.label ? <label>{componentAttrs.label}</label> : ''}
|
||||||
<div className="helpText">{help}</div>
|
<div className="helpText">{help}</div>
|
||||||
<input type={entry.type} bidi={this.setting(setting)} {...entry} />
|
<input type={componentAttrs.type} bidi={this.setting(setting)} {...componentAttrs} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,4 @@
|
|||||||
import AdminApplication from './AdminApplication';
|
import app from './app';
|
||||||
|
|
||||||
const app = new AdminApplication();
|
|
||||||
|
|
||||||
// Backwards compatibility
|
|
||||||
window.app = app;
|
|
||||||
|
|
||||||
export { app };
|
export { app };
|
||||||
|
|
||||||
|
@@ -159,6 +159,8 @@ export default class Application {
|
|||||||
title = '';
|
title = '';
|
||||||
titleCount = 0;
|
titleCount = 0;
|
||||||
|
|
||||||
|
initialRoute;
|
||||||
|
|
||||||
load(payload) {
|
load(payload) {
|
||||||
this.data = payload;
|
this.data = payload;
|
||||||
this.translator.locale = payload.locale;
|
this.translator.locale = payload.locale;
|
||||||
@@ -174,6 +176,8 @@ export default class Application {
|
|||||||
this.session = new Session(this.store.getById('users', this.data.session.userId), this.data.session.csrfToken);
|
this.session = new Session(this.store.getById('users', this.data.session.userId), this.data.session.csrfToken);
|
||||||
|
|
||||||
this.mount();
|
this.mount();
|
||||||
|
|
||||||
|
this.initialRoute = window.location.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
bootExtensions(extensions) {
|
bootExtensions(extensions) {
|
||||||
@@ -226,7 +230,8 @@ export default class Application {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
preloadedApiDocument() {
|
preloadedApiDocument() {
|
||||||
if (this.data.apiDocument) {
|
// If the URL has changed, the preloaded Api document is invalid.
|
||||||
|
if (this.data.apiDocument && window.location.href === this.initialRoute) {
|
||||||
const results = this.store.pushPayload(this.data.apiDocument);
|
const results = this.store.pushPayload(this.data.apiDocument);
|
||||||
|
|
||||||
this.data.apiDocument = null;
|
this.data.apiDocument = null;
|
||||||
|
@@ -77,12 +77,12 @@ export default abstract class Component<T extends ComponentAttrs = ComponentAttr
|
|||||||
* containing all of the `li` elements inside the DOM element of this
|
* containing all of the `li` elements inside the DOM element of this
|
||||||
* component.
|
* component.
|
||||||
*
|
*
|
||||||
* @param {String} [selector] a jQuery-compatible selector string
|
* @param [selector] a jQuery-compatible selector string
|
||||||
* @returns {jQuery} the jQuery object for the DOM node
|
* @returns the jQuery object for the DOM node
|
||||||
* @final
|
* @final
|
||||||
*/
|
*/
|
||||||
protected $(selector) {
|
protected $(selector: string): JQuery {
|
||||||
const $element = $(this.element);
|
const $element = $(this.element) as JQuery<HTMLElement>;
|
||||||
|
|
||||||
return selector ? $element.find(selector) : $element;
|
return selector ? $element.find(selector) : $element;
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ export default abstract class Component<T extends ComponentAttrs = ComponentAttr
|
|||||||
* @see https://mithril.js.org/hyperscript.html#mselector,-attributes,-children
|
* @see https://mithril.js.org/hyperscript.html#mselector,-attributes,-children
|
||||||
*/
|
*/
|
||||||
static component(attrs = {}, children = null): Mithril.Vnode {
|
static component(attrs = {}, children = null): Mithril.Vnode {
|
||||||
const componentAttrs = Object.assign({}, attrs);
|
const componentAttrs = Object.assign({}, attrs) as Record<string, unknown>;
|
||||||
|
|
||||||
return m(this as any, componentAttrs, children);
|
return m(this as any, componentAttrs, children);
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import * as extend from './extend';
|
import * as extend from './extend';
|
||||||
import Session from './Session';
|
import Session from './Session';
|
||||||
import Store from './Store';
|
import Store from './Store';
|
||||||
|
import BasicEditorDriver from './utils/BasicEditorDriver';
|
||||||
import evented from './utils/evented';
|
import evented from './utils/evented';
|
||||||
import liveHumanTimes from './utils/liveHumanTimes';
|
import liveHumanTimes from './utils/liveHumanTimes';
|
||||||
import ItemList from './utils/ItemList';
|
import ItemList from './utils/ItemList';
|
||||||
@@ -56,6 +57,8 @@ import ModalManager from './components/ModalManager';
|
|||||||
import Button from './components/Button';
|
import Button from './components/Button';
|
||||||
import Modal from './components/Modal';
|
import Modal from './components/Modal';
|
||||||
import GroupBadge from './components/GroupBadge';
|
import GroupBadge from './components/GroupBadge';
|
||||||
|
import TextEditor from './components/TextEditor';
|
||||||
|
import TextEditorButton from './components/TextEditorButton';
|
||||||
import Model from './Model';
|
import Model from './Model';
|
||||||
import Application from './Application';
|
import Application from './Application';
|
||||||
import fullTime from './helpers/fullTime';
|
import fullTime from './helpers/fullTime';
|
||||||
@@ -74,6 +77,7 @@ export default {
|
|||||||
extend: extend,
|
extend: extend,
|
||||||
Session: Session,
|
Session: Session,
|
||||||
Store: Store,
|
Store: Store,
|
||||||
|
'utils/BasicEditorDriver': BasicEditorDriver,
|
||||||
'utils/evented': evented,
|
'utils/evented': evented,
|
||||||
'utils/liveHumanTimes': liveHumanTimes,
|
'utils/liveHumanTimes': liveHumanTimes,
|
||||||
'utils/ItemList': ItemList,
|
'utils/ItemList': ItemList,
|
||||||
@@ -130,6 +134,8 @@ export default {
|
|||||||
'components/Button': Button,
|
'components/Button': Button,
|
||||||
'components/Modal': Modal,
|
'components/Modal': Modal,
|
||||||
'components/GroupBadge': GroupBadge,
|
'components/GroupBadge': GroupBadge,
|
||||||
|
'components/TextEditor': TextEditor,
|
||||||
|
'components/TextEditorButton': TextEditorButton,
|
||||||
Model: Model,
|
Model: Model,
|
||||||
Application: Application,
|
Application: Application,
|
||||||
'helpers/fullTime': fullTime,
|
'helpers/fullTime': fullTime,
|
||||||
|
@@ -69,7 +69,7 @@ export default class Button extends Component {
|
|||||||
return [
|
return [
|
||||||
iconName && iconName !== true ? icon(iconName, { className: 'Button-icon' }) : '',
|
iconName && iconName !== true ? icon(iconName, { className: 'Button-icon' }) : '',
|
||||||
children ? <span className="Button-label">{children}</span> : '',
|
children ? <span className="Button-label">{children}</span> : '',
|
||||||
this.attrs.loading ? <LoadingIndicator size="tiny" className="LoadingIndicator--inline" /> : '',
|
this.attrs.loading ? <LoadingIndicator size="small" display="inline" /> : '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -46,7 +46,7 @@ export default class Checkbox extends Component {
|
|||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
getDisplay() {
|
getDisplay() {
|
||||||
return this.attrs.loading ? <LoadingIndicator size="tiny" /> : icon(this.attrs.state ? 'fas fa-check' : 'fas fa-times');
|
return this.attrs.loading ? <LoadingIndicator display="unset" size="small" /> : icon(this.attrs.state ? 'fas fa-check' : 'fas fa-times');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -13,6 +13,7 @@ import listItems from '../helpers/listItems';
|
|||||||
* - `icon` The name of an icon to show in the dropdown toggle button.
|
* - `icon` The name of an icon to show in the dropdown toggle button.
|
||||||
* - `caretIcon` The name of an icon to show on the right of the button.
|
* - `caretIcon` The name of an icon to show on the right of the button.
|
||||||
* - `label` The label of the dropdown toggle button. Defaults to 'Controls'.
|
* - `label` The label of the dropdown toggle button. Defaults to 'Controls'.
|
||||||
|
* - `accessibleToggleLabel` The label used to describe the dropdown toggle button to assistive readers. Defaults to 'Toggle dropdown menu'.
|
||||||
* - `onhide`
|
* - `onhide`
|
||||||
* - `onshow`
|
* - `onshow`
|
||||||
*
|
*
|
||||||
@@ -25,6 +26,7 @@ export default class Dropdown extends Component {
|
|||||||
attrs.menuClassName = attrs.menuClassName || '';
|
attrs.menuClassName = attrs.menuClassName || '';
|
||||||
attrs.label = attrs.label || '';
|
attrs.label = attrs.label || '';
|
||||||
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-caret-down';
|
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-caret-down';
|
||||||
|
attrs.accessibleToggleLabel = attrs.accessibleToggleLabel || app.translator.trans('core.lib.dropdown.toggle_dropdown_accessible_label');
|
||||||
}
|
}
|
||||||
|
|
||||||
oninit(vnode) {
|
oninit(vnode) {
|
||||||
@@ -92,7 +94,13 @@ export default class Dropdown extends Component {
|
|||||||
*/
|
*/
|
||||||
getButton(children) {
|
getButton(children) {
|
||||||
return (
|
return (
|
||||||
<button className={'Dropdown-toggle ' + this.attrs.buttonClassName} data-toggle="dropdown" onclick={this.attrs.onclick}>
|
<button
|
||||||
|
className={'Dropdown-toggle ' + this.attrs.buttonClassName}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-label={this.attrs.accessibleToggleLabel}
|
||||||
|
data-toggle="dropdown"
|
||||||
|
onclick={this.attrs.onclick}
|
||||||
|
>
|
||||||
{this.getButtonContent(children)}
|
{this.getButtonContent(children)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@@ -1,43 +0,0 @@
|
|||||||
import Component from '../Component';
|
|
||||||
import { Spinner } from 'spin.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `LoadingIndicator` component displays a loading spinner with spin.js.
|
|
||||||
*
|
|
||||||
* ### Attrs
|
|
||||||
*
|
|
||||||
* - `size` The spin.js size preset to use. Defaults to 'small'.
|
|
||||||
*
|
|
||||||
* All other attrs will be assigned as attributes on the DOM element.
|
|
||||||
*/
|
|
||||||
export default class LoadingIndicator extends Component {
|
|
||||||
view() {
|
|
||||||
const attrs = Object.assign({}, this.attrs);
|
|
||||||
|
|
||||||
attrs.className = 'LoadingIndicator ' + (attrs.className || '');
|
|
||||||
delete attrs.size;
|
|
||||||
|
|
||||||
return <div {...attrs}>{m.trust(' ')}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
oncreate(vnode) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
|
|
||||||
const options = { zIndex: 'auto', color: this.$().css('color') };
|
|
||||||
|
|
||||||
switch (this.attrs.size) {
|
|
||||||
case 'large':
|
|
||||||
Object.assign(options, { lines: 10, length: 8, width: 4, radius: 8 });
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'tiny':
|
|
||||||
Object.assign(options, { lines: 8, length: 2, width: 2, radius: 3 });
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Object.assign(options, { lines: 8, length: 4, width: 3, radius: 5 });
|
|
||||||
}
|
|
||||||
|
|
||||||
new Spinner(options).spin(this.element);
|
|
||||||
}
|
|
||||||
}
|
|
80
js/src/common/components/LoadingIndicator.tsx
Normal file
80
js/src/common/components/LoadingIndicator.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import Component, { ComponentAttrs } from '../Component';
|
||||||
|
import classList from '../utils/classList';
|
||||||
|
|
||||||
|
export interface LoadingIndicatorAttrs extends ComponentAttrs {
|
||||||
|
/**
|
||||||
|
* Custom classes fro the loading indicator's container.
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
/**
|
||||||
|
* Custom classes for the loading indicator's container.
|
||||||
|
*/
|
||||||
|
containerClassName?: string;
|
||||||
|
/**
|
||||||
|
* Optional size to specify for the loading indicator.
|
||||||
|
*/
|
||||||
|
size?: 'large' | 'medium' | 'small';
|
||||||
|
/**
|
||||||
|
* Optional attributes to apply to the loading indicator's container.
|
||||||
|
*/
|
||||||
|
containerAttrs?: Partial<ComponentAttrs>;
|
||||||
|
/**
|
||||||
|
* Display type of the spinner.
|
||||||
|
*
|
||||||
|
* @default 'block'
|
||||||
|
*/
|
||||||
|
display?: 'block' | 'inline' | 'unset';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `LoadingIndicator` component displays a simple CSS-based loading spinner.
|
||||||
|
*
|
||||||
|
* To set a custom color, use the CSS `color` property.
|
||||||
|
*
|
||||||
|
* To increase spacing around the spinner, use the CSS `height` property on the
|
||||||
|
* spinner's **container**. Setting the `display` attribute to `block` will set
|
||||||
|
* a height of `100px` by default.
|
||||||
|
*
|
||||||
|
* To apply a custom size to the loading indicator, set the `--size` and
|
||||||
|
* `--thickness` CSS custom properties on the loading indicator container.
|
||||||
|
*
|
||||||
|
* If you *really* want to change how this looks as part of your custom theme,
|
||||||
|
* you can override the `border-radius` and `border` then set either a
|
||||||
|
* background image, or use `content: "\<glyph>"` (e.g. `content: "\f1ce"`)
|
||||||
|
* and `font-family: 'Font Awesome 5 Free'` to set an FA icon if you'd rather.
|
||||||
|
*
|
||||||
|
* ### Attrs
|
||||||
|
*
|
||||||
|
* - `containerClassName` Class name(s) to apply to the indicator's parent
|
||||||
|
* - `className` Class name(s) to apply to the indicator itself
|
||||||
|
* - `display` Determines how the spinner should be displayed (`inline`, `block` (default) or `unset`)
|
||||||
|
* - `size` Size of the loading indicator (`small`, `medium` or `large`)
|
||||||
|
* - `containerAttrs` Optional attrs to be applied to the container DOM element
|
||||||
|
*
|
||||||
|
* All other attrs will be assigned as attributes on the DOM element.
|
||||||
|
*/
|
||||||
|
export default class LoadingIndicator extends Component<LoadingIndicatorAttrs> {
|
||||||
|
view() {
|
||||||
|
const { display = 'block', size = 'medium', containerClassName, className, ...attrs } = this.attrs;
|
||||||
|
|
||||||
|
const completeClassName = classList('LoadingIndicator', className);
|
||||||
|
const completeContainerClassName = classList(
|
||||||
|
'LoadingIndicator-container',
|
||||||
|
display !== 'unset' && `LoadingIndicator-container--${display}`,
|
||||||
|
size && `LoadingIndicator-container--${size}`,
|
||||||
|
containerClassName
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-label={app.translator.trans('core.lib.loading_indicator.accessible_label')}
|
||||||
|
role="status"
|
||||||
|
{...attrs.containerAttrs}
|
||||||
|
data-size={size}
|
||||||
|
className={completeContainerClassName}
|
||||||
|
>
|
||||||
|
<div aria-hidden className={completeClassName} {...attrs} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -24,7 +24,12 @@ export default class SplitDropdown extends Dropdown {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
Button.component(buttonAttrs, firstChild.children),
|
Button.component(buttonAttrs, firstChild.children),
|
||||||
<button className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName} data-toggle="dropdown">
|
<button
|
||||||
|
className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-label={this.attrs.accessibleToggleLabel}
|
||||||
|
data-toggle="dropdown"
|
||||||
|
>
|
||||||
{icon(this.attrs.icon, { className: 'Button-icon' })}
|
{icon(this.attrs.icon, { className: 'Button-icon' })}
|
||||||
{icon('fas fa-caret-down', { className: 'Button-caret' })}
|
{icon('fas fa-caret-down', { className: 'Button-caret' })}
|
||||||
</button>,
|
</button>,
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import Component from '../../common/Component';
|
import Component from '../Component';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../utils/ItemList';
|
||||||
import listItems from '../../common/helpers/listItems';
|
import listItems from '../helpers/listItems';
|
||||||
import Button from '../../common/components/Button';
|
import Button from './Button';
|
||||||
|
|
||||||
import BasicEditorDriver from '../utils/BasicEditorDriver';
|
import BasicEditorDriver from '../utils/BasicEditorDriver';
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
import Button from '../../common/components/Button';
|
import Button from './Button';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `TextEditorButton` component displays a button suitable for the text
|
* The `TextEditorButton` component displays a button suitable for the text
|
@@ -1,26 +1,28 @@
|
|||||||
|
import * as Mithril from 'mithril';
|
||||||
|
import User from '../models/User';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `avatar` helper displays a user's avatar.
|
* The `avatar` helper displays a user's avatar.
|
||||||
*
|
*
|
||||||
* @param {User} user
|
* @param user
|
||||||
* @param {Object} attrs Attributes to apply to the avatar element
|
* @param attrs Attributes to apply to the avatar element
|
||||||
* @return {Object}
|
|
||||||
*/
|
*/
|
||||||
export default function avatar(user, attrs = {}) {
|
export default function avatar(user: User, attrs: Object = {}): Mithril.Vnode {
|
||||||
attrs.className = 'Avatar ' + (attrs.className || '');
|
attrs.className = 'Avatar ' + (attrs.className || '');
|
||||||
let content = '';
|
let content: string = '';
|
||||||
|
|
||||||
// If the `title` attribute is set to null or false, we don't want to give the
|
// If the `title` attribute is set to null or false, we don't want to give the
|
||||||
// avatar a title. On the other hand, if it hasn't been given at all, we can
|
// avatar a title. On the other hand, if it hasn't been given at all, we can
|
||||||
// safely default it to the user's username.
|
// safely default it to the user's username.
|
||||||
const hasTitle = attrs.title === 'undefined' || attrs.title;
|
const hasTitle: boolean | string = attrs.title === 'undefined' || attrs.title;
|
||||||
if (!hasTitle) delete attrs.title;
|
if (!hasTitle) delete attrs.title;
|
||||||
|
|
||||||
// If a user has been passed, then we will set up an avatar using their
|
// If a user has been passed, then we will set up an avatar using their
|
||||||
// uploaded image, or the first letter of their username if they haven't
|
// uploaded image, or the first letter of their username if they haven't
|
||||||
// uploaded one.
|
// uploaded one.
|
||||||
if (user) {
|
if (user) {
|
||||||
const username = user.displayName() || '?';
|
const username: string = user.displayName() || '?';
|
||||||
const avatarUrl = user.avatarUrl();
|
const avatarUrl: string = user.avatarUrl();
|
||||||
|
|
||||||
if (hasTitle) attrs.title = attrs.title || username;
|
if (hasTitle) attrs.title = attrs.title || username;
|
||||||
|
|
@@ -1,15 +1,16 @@
|
|||||||
|
import * as Mithril from 'mithril';
|
||||||
import Separator from '../components/Separator';
|
import Separator from '../components/Separator';
|
||||||
import classList from '../utils/classList';
|
import classList from '../utils/classList';
|
||||||
|
|
||||||
function isSeparator(item) {
|
function isSeparator(item): boolean {
|
||||||
return item.tag === Separator;
|
return item.tag === Separator;
|
||||||
}
|
}
|
||||||
|
|
||||||
function withoutUnnecessarySeparators(items) {
|
function withoutUnnecessarySeparators(items: Array<Mithril.Vnode>): Array<Mithril.Vnode> {
|
||||||
const newItems = [];
|
const newItems = [];
|
||||||
let prevItem;
|
let prevItem;
|
||||||
|
|
||||||
items.filter(Boolean).forEach((item, i) => {
|
items.filter(Boolean).forEach((item: Mithril.Vnode, i: number) => {
|
||||||
if (!isSeparator(item) || (prevItem && !isSeparator(prevItem) && i !== items.length - 1)) {
|
if (!isSeparator(item) || (prevItem && !isSeparator(prevItem) && i !== items.length - 1)) {
|
||||||
prevItem = item;
|
prevItem = item;
|
||||||
newItems.push(item);
|
newItems.push(item);
|
||||||
@@ -22,14 +23,11 @@ function withoutUnnecessarySeparators(items) {
|
|||||||
/**
|
/**
|
||||||
* The `listItems` helper wraps a collection of components in <li> tags,
|
* The `listItems` helper wraps a collection of components in <li> tags,
|
||||||
* stripping out any unnecessary `Separator` components.
|
* stripping out any unnecessary `Separator` components.
|
||||||
*
|
|
||||||
* @param {*} items
|
|
||||||
* @return {Array}
|
|
||||||
*/
|
*/
|
||||||
export default function listItems(items) {
|
export default function listItems(items: Mithril.Vnode | Array<Mithril.Vnode>): Array<Mithril.Vnode> {
|
||||||
if (!(items instanceof Array)) items = [items];
|
if (!(items instanceof Array)) items = [items];
|
||||||
|
|
||||||
return withoutUnnecessarySeparators(items).map((item) => {
|
return withoutUnnecessarySeparators(items).map((item: Mithril.Vnode) => {
|
||||||
const isListItem = item.tag && item.tag.isListItem;
|
const isListItem = item.tag && item.tag.isListItem;
|
||||||
const active = item.tag && item.tag.isActive && item.tag.isActive(item.attrs);
|
const active = item.tag && item.tag.isActive && item.tag.isActive(item.attrs);
|
||||||
const className = (item.attrs && item.attrs.itemClassName) || item.itemClassName;
|
const className = (item.attrs && item.attrs.itemClassName) || item.itemClassName;
|
||||||
@@ -40,7 +38,7 @@ export default function listItems(items) {
|
|||||||
item.key = item.attrs.key;
|
item.key = item.attrs.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = isListItem ? (
|
const node: Mithril.Vnode = isListItem ? (
|
||||||
item
|
item
|
||||||
) : (
|
) : (
|
||||||
<li
|
<li
|
@@ -1,12 +1,11 @@
|
|||||||
|
import * as Mithril from 'mithril';
|
||||||
|
import User from '../models/User';
|
||||||
import icon from './icon';
|
import icon from './icon';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `useronline` helper displays a green circle if the user is online
|
* The `useronline` helper displays a green circle if the user is online
|
||||||
*
|
|
||||||
* @param {User} user
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
*/
|
||||||
export default function userOnline(user) {
|
export default function userOnline(user: User): Mithril.Vnode {
|
||||||
if (user.lastSeenAt() && user.isOnline()) {
|
if (user.lastSeenAt() && user.isOnline()) {
|
||||||
return <span className="UserOnline">{icon('fas fa-circle')}</span>;
|
return <span className="UserOnline">{icon('fas fa-circle')}</span>;
|
||||||
}
|
}
|
@@ -1,11 +1,11 @@
|
|||||||
|
import * as Mithril from 'mithril';
|
||||||
|
import User from '../models/User';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `username` helper displays a user's username in a <span class="username">
|
* The `username` helper displays a user's username in a <span class="username">
|
||||||
* tag. If the user doesn't exist, the username will be displayed as [deleted].
|
* tag. If the user doesn't exist, the username will be displayed as [deleted].
|
||||||
*
|
|
||||||
* @param {User} user
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
*/
|
||||||
export default function username(user) {
|
export default function username(user: User): Mithril.Vnode {
|
||||||
const name = (user && user.displayName()) || app.translator.trans('core.lib.username.deleted_text');
|
const name = (user && user.displayName()) || app.translator.trans('core.lib.username.deleted_text');
|
||||||
|
|
||||||
return <span className="username">{name}</span>;
|
return <span className="username">{name}</span>;
|
@@ -1,6 +1,8 @@
|
|||||||
import 'expose-loader?$!expose-loader?jQuery!jquery';
|
// Expose jQuery, mithril and dayjs to the window browser object
|
||||||
import 'expose-loader?m!mithril';
|
import 'expose-loader?exposes[]=$&exposes[]=jQuery!jquery';
|
||||||
import 'expose-loader?dayjs!dayjs';
|
import 'expose-loader?exposes=m!mithril';
|
||||||
|
import 'expose-loader?exposes=dayjs!dayjs';
|
||||||
|
|
||||||
import 'bootstrap/js/affix';
|
import 'bootstrap/js/affix';
|
||||||
import 'bootstrap/js/dropdown';
|
import 'bootstrap/js/dropdown';
|
||||||
import 'bootstrap/js/modal';
|
import 'bootstrap/js/modal';
|
||||||
|
@@ -31,7 +31,24 @@ export default class Drawer {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
hide() {
|
hide() {
|
||||||
$('#app').removeClass('drawerOpen');
|
/**
|
||||||
|
* As part of hiding the drawer, this function also ensures that the drawer
|
||||||
|
* correctly animates out, while ensuring it is not part of the navigation
|
||||||
|
* tree while off-screen.
|
||||||
|
*
|
||||||
|
* More info: https://github.com/flarum/core/pull/2666#discussion_r595381014
|
||||||
|
*/
|
||||||
|
|
||||||
|
const $app = $('#app');
|
||||||
|
|
||||||
|
if (!$app.hasClass('drawerOpen')) return;
|
||||||
|
|
||||||
|
const $drawer = $('#drawer');
|
||||||
|
|
||||||
|
// Used to prevent `visibility: hidden` from breaking the exit animation
|
||||||
|
$drawer.css('visibility', 'visible').one('transitionend', () => $drawer.css('visibility', ''));
|
||||||
|
|
||||||
|
$app.removeClass('drawerOpen');
|
||||||
|
|
||||||
if (this.$backdrop) this.$backdrop.remove();
|
if (this.$backdrop) this.$backdrop.remove();
|
||||||
}
|
}
|
||||||
|
@@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* The `classList` utility creates a list of class names by joining an object's
|
|
||||||
* keys, but only for values which are truthy.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* classList({ foo: true, bar: false, qux: 'qaz' });
|
|
||||||
* // "foo qux"
|
|
||||||
*
|
|
||||||
* @param {Object} classes
|
|
||||||
* @return {String}
|
|
||||||
*/
|
|
||||||
export default function classList(classes) {
|
|
||||||
let classNames;
|
|
||||||
|
|
||||||
if (classes instanceof Array) {
|
|
||||||
classNames = classes.filter((name) => name);
|
|
||||||
} else {
|
|
||||||
classNames = [];
|
|
||||||
|
|
||||||
for (const i in classes) {
|
|
||||||
if (classes[i]) classNames.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return classNames.join(' ');
|
|
||||||
}
|
|
12
js/src/common/utils/classList.ts
Normal file
12
js/src/common/utils/classList.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This util exposes `clsx` to core and extensions as a re-usable utility.
|
||||||
|
*
|
||||||
|
* For full documentation, see `clsx` on GitHub.
|
||||||
|
*
|
||||||
|
* @see https://github.com/lukeed/clsx
|
||||||
|
*/
|
||||||
|
const classList = clsx;
|
||||||
|
|
||||||
|
export default classList;
|
@@ -16,6 +16,7 @@ import NotificationListState from './states/NotificationListState';
|
|||||||
import GlobalSearchState from './states/GlobalSearchState';
|
import GlobalSearchState from './states/GlobalSearchState';
|
||||||
import DiscussionListState from './states/DiscussionListState';
|
import DiscussionListState from './states/DiscussionListState';
|
||||||
import ComposerState from './states/ComposerState';
|
import ComposerState from './states/ComposerState';
|
||||||
|
import isSafariMobile from './utils/isSafariMobile';
|
||||||
|
|
||||||
export default class ForumApplication extends Application {
|
export default class ForumApplication extends Application {
|
||||||
/**
|
/**
|
||||||
@@ -138,6 +139,12 @@ export default class ForumApplication extends Application {
|
|||||||
m.redraw();
|
m.redraw();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isSafariMobile()) {
|
||||||
|
$(() => {
|
||||||
|
$('.App').addClass('mobile-safari');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
8
js/src/forum/app.ts
Normal file
8
js/src/forum/app.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import Forum from './ForumApplication';
|
||||||
|
|
||||||
|
const app = new Forum();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
window.app = app;
|
||||||
|
|
||||||
|
export default app;
|
@@ -36,8 +36,6 @@ import HeaderSecondary from './components/HeaderSecondary';
|
|||||||
import ComposerButton from './components/ComposerButton';
|
import ComposerButton from './components/ComposerButton';
|
||||||
import DiscussionList from './components/DiscussionList';
|
import DiscussionList from './components/DiscussionList';
|
||||||
import ReplyPlaceholder from './components/ReplyPlaceholder';
|
import ReplyPlaceholder from './components/ReplyPlaceholder';
|
||||||
import TextEditor from './components/TextEditor';
|
|
||||||
import TextEditorButton from './components/TextEditorButton';
|
|
||||||
import AvatarEditor from './components/AvatarEditor';
|
import AvatarEditor from './components/AvatarEditor';
|
||||||
import Post from './components/Post';
|
import Post from './components/Post';
|
||||||
import SettingsPage from './components/SettingsPage';
|
import SettingsPage from './components/SettingsPage';
|
||||||
@@ -73,7 +71,7 @@ import DiscussionListItem from './components/DiscussionListItem';
|
|||||||
import LoadingPost from './components/LoadingPost';
|
import LoadingPost from './components/LoadingPost';
|
||||||
import PostsUserPage from './components/PostsUserPage';
|
import PostsUserPage from './components/PostsUserPage';
|
||||||
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
|
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
|
||||||
import BasicEditorDriver from './utils/BasicEditorDriver';
|
import BasicEditorDriver from '../common/utils/BasicEditorDriver';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
import ForumApplication from './ForumApplication';
|
import ForumApplication from './ForumApplication';
|
||||||
|
|
||||||
@@ -87,7 +85,6 @@ export default Object.assign(compat, {
|
|||||||
'utils/UserControls': UserControls,
|
'utils/UserControls': UserControls,
|
||||||
'utils/Pane': Pane,
|
'utils/Pane': Pane,
|
||||||
'utils/BasicEditorDriver': BasicEditorDriver,
|
'utils/BasicEditorDriver': BasicEditorDriver,
|
||||||
'utils/SuperTextarea': BasicEditorDriver, // @deprecated beta 16, remove beta 17
|
|
||||||
'states/ComposerState': ComposerState,
|
'states/ComposerState': ComposerState,
|
||||||
'states/DiscussionListState': DiscussionListState,
|
'states/DiscussionListState': DiscussionListState,
|
||||||
'states/GlobalSearchState': GlobalSearchState,
|
'states/GlobalSearchState': GlobalSearchState,
|
||||||
@@ -116,8 +113,6 @@ export default Object.assign(compat, {
|
|||||||
'components/ComposerButton': ComposerButton,
|
'components/ComposerButton': ComposerButton,
|
||||||
'components/DiscussionList': DiscussionList,
|
'components/DiscussionList': DiscussionList,
|
||||||
'components/ReplyPlaceholder': ReplyPlaceholder,
|
'components/ReplyPlaceholder': ReplyPlaceholder,
|
||||||
'components/TextEditor': TextEditor,
|
|
||||||
'components/TextEditorButton': TextEditorButton,
|
|
||||||
'components/AvatarEditor': AvatarEditor,
|
'components/AvatarEditor': AvatarEditor,
|
||||||
'components/Post': Post,
|
'components/Post': Post,
|
||||||
'components/SettingsPage': SettingsPage,
|
'components/SettingsPage': SettingsPage,
|
||||||
|
@@ -52,7 +52,13 @@ export default class AvatarEditor extends Component {
|
|||||||
ondragend={this.disableDragover.bind(this)}
|
ondragend={this.disableDragover.bind(this)}
|
||||||
ondrop={this.dropUpload.bind(this)}
|
ondrop={this.dropUpload.bind(this)}
|
||||||
>
|
>
|
||||||
{this.loading ? <LoadingIndicator /> : user.avatarUrl() ? icon('fas fa-pencil-alt') : icon('fas fa-plus-circle')}
|
{this.loading ? (
|
||||||
|
<LoadingIndicator display="unset" size="large" />
|
||||||
|
) : user.avatarUrl() ? (
|
||||||
|
icon('fas fa-pencil-alt')
|
||||||
|
) : (
|
||||||
|
icon('fas fa-plus-circle')
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
<ul className="Dropdown-menu Menu">{listItems(this.controlItems().toArray())}</ul>
|
<ul className="Dropdown-menu Menu">{listItems(this.controlItems().toArray())}</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -265,7 +265,17 @@ export default class Composer extends Component {
|
|||||||
this.animateHeightChange().then(() => this.focus());
|
this.animateHeightChange().then(() => this.focus());
|
||||||
|
|
||||||
if (app.screen() === 'phone') {
|
if (app.screen() === 'phone') {
|
||||||
this.$().css('top', 0);
|
// On safari fixed position doesn't properly work on mobile,
|
||||||
|
// So we use absolute and set the top value.
|
||||||
|
// https://github.com/flarum/core/issues/2652
|
||||||
|
|
||||||
|
// Due to another safari bug, `scrollTop` is unreliable when
|
||||||
|
// at the very bottom of the page AND opening the composer.
|
||||||
|
// So we fallback to a calculated version of scrollTop.
|
||||||
|
// https://github.com/flarum/core/issues/2683
|
||||||
|
const scrollElement = document.documentElement;
|
||||||
|
const topOfViewport = Math.min(scrollElement.scrollTop, scrollElement.scrollHeight - scrollElement.clientHeight);
|
||||||
|
this.$().css('top', $('.App').is('.mobile-safari') ? topOfViewport : 0);
|
||||||
this.showBackdrop();
|
this.showBackdrop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import Component from '../../common/Component';
|
import Component from '../../common/Component';
|
||||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||||
import ConfirmDocumentUnload from '../../common/components/ConfirmDocumentUnload';
|
import ConfirmDocumentUnload from '../../common/components/ConfirmDocumentUnload';
|
||||||
import TextEditor from './TextEditor';
|
import TextEditor from '../../common/components/TextEditor';
|
||||||
import avatar from '../../common/helpers/avatar';
|
import avatar from '../../common/helpers/avatar';
|
||||||
import listItems from '../../common/helpers/listItems';
|
import listItems from '../../common/helpers/listItems';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import classList from '../../common/utils/classList';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `ComposerBody` component handles the body, or the content, of the
|
* The `ComposerBody` component handles the body, or the content, of the
|
||||||
@@ -66,7 +67,7 @@ export default class ComposerBody extends Component {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{LoadingIndicator.component({ className: 'ComposerBody-loading' + (this.loading ? ' active' : '') })}
|
<LoadingIndicator display="unset" containerClassName={classList('ComposerBody-loading', this.loading && 'active')} size="large" />
|
||||||
</div>
|
</div>
|
||||||
</ConfirmDocumentUnload>
|
</ConfirmDocumentUnload>
|
||||||
);
|
);
|
||||||
|
@@ -19,7 +19,7 @@ export default class DiscussionList extends Component {
|
|||||||
let loading;
|
let loading;
|
||||||
|
|
||||||
if (state.isLoading()) {
|
if (state.isLoading()) {
|
||||||
loading = LoadingIndicator.component();
|
loading = <LoadingIndicator />;
|
||||||
} else if (state.moreResults) {
|
} else if (state.moreResults) {
|
||||||
loading = Button.component(
|
loading = Button.component(
|
||||||
{
|
{
|
||||||
|
@@ -87,6 +87,7 @@ export default class DiscussionListItem extends Component {
|
|||||||
icon: 'fas fa-ellipsis-v',
|
icon: 'fas fa-ellipsis-v',
|
||||||
className: 'DiscussionListItem-controls',
|
className: 'DiscussionListItem-controls',
|
||||||
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right',
|
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right',
|
||||||
|
accessibleToggleLabel: app.translator.trans('core.forum.discussion_controls.toggle_dropdown_accessible_label'),
|
||||||
},
|
},
|
||||||
controls
|
controls
|
||||||
)
|
)
|
||||||
|
@@ -73,23 +73,25 @@ export default class DiscussionPage extends Page {
|
|||||||
<div className="DiscussionPage">
|
<div className="DiscussionPage">
|
||||||
<DiscussionListPane state={app.discussions} />
|
<DiscussionListPane state={app.discussions} />
|
||||||
<div className="DiscussionPage-discussion">
|
<div className="DiscussionPage-discussion">
|
||||||
{discussion
|
{discussion ? (
|
||||||
? [
|
[
|
||||||
DiscussionHero.component({ discussion }),
|
DiscussionHero.component({ discussion }),
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<nav className="DiscussionPage-nav">
|
<nav className="DiscussionPage-nav">
|
||||||
<ul>{listItems(this.sidebarItems().toArray())}</ul>
|
<ul>{listItems(this.sidebarItems().toArray())}</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="DiscussionPage-stream">
|
<div className="DiscussionPage-stream">
|
||||||
{PostStream.component({
|
{PostStream.component({
|
||||||
discussion,
|
discussion,
|
||||||
stream: this.stream,
|
stream: this.stream,
|
||||||
onPositionChange: this.positionChanged.bind(this),
|
onPositionChange: this.positionChanged.bind(this),
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
]
|
]
|
||||||
: LoadingIndicator.component({ className: 'LoadingIndicator--block' })}
|
) : (
|
||||||
|
<LoadingIndicator />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -189,6 +191,7 @@ export default class DiscussionPage extends Page {
|
|||||||
icon: 'fas fa-ellipsis-v',
|
icon: 'fas fa-ellipsis-v',
|
||||||
className: 'App-primaryControl',
|
className: 'App-primaryControl',
|
||||||
buttonClassName: 'Button--primary',
|
buttonClassName: 'Button--primary',
|
||||||
|
accessibleToggleLabel: app.translator.trans('core.forum.discussion_controls.toggle_dropdown_accessible_label'),
|
||||||
},
|
},
|
||||||
DiscussionControls.controls(this.discussion, this).toArray()
|
DiscussionControls.controls(this.discussion, this).toArray()
|
||||||
)
|
)
|
||||||
|
@@ -237,7 +237,8 @@ export default class EditUserModal extends Modal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal @protected
|
* @internal
|
||||||
|
* @protected
|
||||||
*/
|
*/
|
||||||
userIsAdmin(user) {
|
userIsAdmin(user) {
|
||||||
return user.groups().some((g) => g.id() === Group.ADMINISTRATOR_ID);
|
return user.groups().some((g) => g.id() === Group.ADMINISTRATOR_ID);
|
||||||
|
@@ -57,6 +57,7 @@ export default class HeaderSecondary extends Component {
|
|||||||
SelectDropdown.component(
|
SelectDropdown.component(
|
||||||
{
|
{
|
||||||
buttonClassName: 'Button Button--link',
|
buttonClassName: 'Button Button--link',
|
||||||
|
accessibleToggleLabel: app.translator.trans('core.forum.header.locale_dropdown_accessible_label'),
|
||||||
},
|
},
|
||||||
locales
|
locales
|
||||||
),
|
),
|
||||||
|
@@ -172,6 +172,7 @@ export default class IndexPage extends Page {
|
|||||||
{
|
{
|
||||||
buttonClassName: 'Button',
|
buttonClassName: 'Button',
|
||||||
className: 'App-titleControl',
|
className: 'App-titleControl',
|
||||||
|
accessibleToggleLabel: app.translator.trans('core.forum.index.toggle_sidenav_dropdown_accessible_label'),
|
||||||
},
|
},
|
||||||
this.navItems(this).toArray()
|
this.navItems(this).toArray()
|
||||||
)
|
)
|
||||||
@@ -227,6 +228,7 @@ export default class IndexPage extends Page {
|
|||||||
{
|
{
|
||||||
buttonClassName: 'Button',
|
buttonClassName: 'Button',
|
||||||
label: sortOptions[app.search.params().sort] || Object.keys(sortMap).map((key) => sortOptions[key])[0],
|
label: sortOptions[app.search.params().sort] || Object.keys(sortMap).map((key) => sortOptions[key])[0],
|
||||||
|
accessibleToggleLabel: app.translator.trans('core.forum.index_sort.toggle_dropdown_accessible_label'),
|
||||||
},
|
},
|
||||||
Object.keys(sortOptions).map((value) => {
|
Object.keys(sortOptions).map((value) => {
|
||||||
const label = sortOptions[value];
|
const label = sortOptions[value];
|
||||||
|
@@ -84,7 +84,7 @@ export default class NotificationList extends Component {
|
|||||||
})
|
})
|
||||||
: ''}
|
: ''}
|
||||||
{state.isLoading() ? (
|
{state.isLoading() ? (
|
||||||
<LoadingIndicator className="LoadingIndicator--block" />
|
<LoadingIndicator />
|
||||||
) : pages.length ? (
|
) : pages.length ? (
|
||||||
''
|
''
|
||||||
) : (
|
) : (
|
||||||
|
@@ -9,6 +9,8 @@ export default class NotificationsDropdown extends Dropdown {
|
|||||||
attrs.menuClassName = attrs.menuClassName || 'Dropdown-menu--right';
|
attrs.menuClassName = attrs.menuClassName || 'Dropdown-menu--right';
|
||||||
attrs.label = attrs.label || app.translator.trans('core.forum.notifications.tooltip');
|
attrs.label = attrs.label || app.translator.trans('core.forum.notifications.tooltip');
|
||||||
attrs.icon = attrs.icon || 'fas fa-bell';
|
attrs.icon = attrs.icon || 'fas fa-bell';
|
||||||
|
// For best a11y support, both `title` and `aria-label` should be used
|
||||||
|
attrs.accessibleToggleLabel = attrs.accessibleToggleLabel || app.translator.trans('core.forum.notifications.toggle_dropdown_accessible_label');
|
||||||
|
|
||||||
super.initAttrs(attrs);
|
super.initAttrs(attrs);
|
||||||
}
|
}
|
||||||
|
@@ -61,6 +61,7 @@ export default class Post extends Component {
|
|||||||
icon="fas fa-ellipsis-h"
|
icon="fas fa-ellipsis-h"
|
||||||
onshow={() => this.$('.Post-actions').addClass('open')}
|
onshow={() => this.$('.Post-actions').addClass('open')}
|
||||||
onhide={() => this.$('.Post-actions').removeClass('open')}
|
onhide={() => this.$('.Post-actions').removeClass('open')}
|
||||||
|
accessibleToggleLabel={app.translator.trans('core.forum.post_controls.toggle_dropdown_accessible_label')}
|
||||||
>
|
>
|
||||||
{controls}
|
{controls}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
@@ -112,7 +112,6 @@ export default class PostStreamScrubber extends Component {
|
|||||||
|
|
||||||
// Now we want to make the scrollbar handle draggable. Let's start by
|
// Now we want to make the scrollbar handle draggable. Let's start by
|
||||||
// preventing default browser events from messing things up.
|
// preventing default browser events from messing things up.
|
||||||
.css({ cursor: 'pointer', 'user-select': 'none' })
|
|
||||||
.bind('dragstart mousedown touchstart', (e) => e.preventDefault());
|
.bind('dragstart mousedown touchstart', (e) => e.preventDefault());
|
||||||
|
|
||||||
// When the mouse is pressed on the scrollbar handle, we capture some
|
// When the mouse is pressed on the scrollbar handle, we capture some
|
||||||
@@ -124,7 +123,6 @@ export default class PostStreamScrubber extends Component {
|
|||||||
this.indexStart = 0;
|
this.indexStart = 0;
|
||||||
|
|
||||||
this.$('.Scrubber-handle')
|
this.$('.Scrubber-handle')
|
||||||
.css('cursor', 'move')
|
|
||||||
.bind('mousedown touchstart', this.onmousedown.bind(this))
|
.bind('mousedown touchstart', this.onmousedown.bind(this))
|
||||||
|
|
||||||
// Exempt the scrollbar handle from the 'jump to' click event.
|
// Exempt the scrollbar handle from the 'jump to' click event.
|
||||||
|
@@ -121,7 +121,7 @@ export default class PostsUserPage extends UserPage {
|
|||||||
loadResults(offset) {
|
loadResults(offset) {
|
||||||
return app.store.find('posts', {
|
return app.store.find('posts', {
|
||||||
filter: {
|
filter: {
|
||||||
user: this.user.id(),
|
author: this.user.username(),
|
||||||
type: 'comment',
|
type: 'comment',
|
||||||
},
|
},
|
||||||
page: { offset, limit: this.loadLimit },
|
page: { offset, limit: this.loadLimit },
|
||||||
|
@@ -49,7 +49,7 @@ export default class RenameDiscussionModal extends Modal {
|
|||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
const title = this.newTitle;
|
const title = this.newTitle();
|
||||||
const currentTitle = this.currentTitle;
|
const currentTitle = this.currentTitle;
|
||||||
|
|
||||||
// If the title is different to what it was before, then save it. After the
|
// If the title is different to what it was before, then save it. After the
|
||||||
|
@@ -71,8 +71,11 @@ export default class Search extends Component {
|
|||||||
// Hide the search view if no sources were loaded
|
// Hide the search view if no sources were loaded
|
||||||
if (!this.sources.length) return <div></div>;
|
if (!this.sources.length) return <div></div>;
|
||||||
|
|
||||||
|
const searchLabel = extractText(app.translator.trans('core.forum.header.search_placeholder'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
role="search"
|
||||||
className={
|
className={
|
||||||
'Search ' +
|
'Search ' +
|
||||||
classList({
|
classList({
|
||||||
@@ -85,16 +88,17 @@ export default class Search extends Component {
|
|||||||
>
|
>
|
||||||
<div className="Search-input">
|
<div className="Search-input">
|
||||||
<input
|
<input
|
||||||
|
aria-label={searchLabel}
|
||||||
className="FormControl"
|
className="FormControl"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder={extractText(app.translator.trans('core.forum.header.search_placeholder'))}
|
placeholder={searchLabel}
|
||||||
value={this.state.getValue()}
|
value={this.state.getValue()}
|
||||||
oninput={(e) => this.state.setValue(e.target.value)}
|
oninput={(e) => this.state.setValue(e.target.value)}
|
||||||
onfocus={() => (this.hasFocus = true)}
|
onfocus={() => (this.hasFocus = true)}
|
||||||
onblur={() => (this.hasFocus = false)}
|
onblur={() => (this.hasFocus = false)}
|
||||||
/>
|
/>
|
||||||
{this.loadingSources ? (
|
{this.loadingSources ? (
|
||||||
LoadingIndicator.component({ size: 'tiny', className: 'Button Button--icon Button--link' })
|
<LoadingIndicator size="small" display="inline" containerClassName="Button Button--icon Button--link" />
|
||||||
) : currentSearch ? (
|
) : currentSearch ? (
|
||||||
<button className="Search-clear Button Button--icon Button--link" onclick={this.clear.bind(this)}>
|
<button className="Search-clear Button Button--icon Button--link" onclick={this.clear.bind(this)}>
|
||||||
{icon('fas fa-times-circle')}
|
{icon('fas fa-times-circle')}
|
||||||
@@ -110,9 +114,23 @@ export default class Search extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateMaxHeight() {
|
||||||
|
// Since extensions might add elements above the search box on mobile,
|
||||||
|
// we need to calculate and set the max height dynamically.
|
||||||
|
const resultsElementMargin = 14;
|
||||||
|
const maxHeight =
|
||||||
|
window.innerHeight - this.element.querySelector('.Search-input>.FormControl').getBoundingClientRect().bottom - resultsElementMargin;
|
||||||
|
this.element.querySelector('.Search-results').style['max-height'] = `${maxHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
onupdate() {
|
onupdate() {
|
||||||
// Highlight the item that is currently selected.
|
// Highlight the item that is currently selected.
|
||||||
this.setIndex(this.getCurrentNumericIndex());
|
this.setIndex(this.getCurrentNumericIndex());
|
||||||
|
|
||||||
|
// If there are no sources, the search view is not shown.
|
||||||
|
if (!this.sources.length) return;
|
||||||
|
|
||||||
|
this.updateMaxHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
oncreate(vnode) {
|
oncreate(vnode) {
|
||||||
@@ -177,6 +195,13 @@ export default class Search extends Component {
|
|||||||
.one('mouseup', (e) => e.preventDefault())
|
.one('mouseup', (e) => e.preventDefault())
|
||||||
.select();
|
.select();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.updateMaxHeightHandler = this.updateMaxHeight.bind(this);
|
||||||
|
window.addEventListener('resize', this.updateMaxHeightHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
onremove() {
|
||||||
|
window.removeEventListener('resize', this.updateMaxHeightHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -17,6 +17,8 @@ export default class SessionDropdown extends Dropdown {
|
|||||||
attrs.className = 'SessionDropdown';
|
attrs.className = 'SessionDropdown';
|
||||||
attrs.buttonClassName = 'Button Button--user Button--flat';
|
attrs.buttonClassName = 'Button Button--user Button--flat';
|
||||||
attrs.menuClassName = 'Dropdown-menu--right';
|
attrs.menuClassName = 'Dropdown-menu--right';
|
||||||
|
|
||||||
|
attrs.accessibleToggleLabel = app.translator.trans('core.forum.header.session_dropdown_accessible_label');
|
||||||
}
|
}
|
||||||
|
|
||||||
view(vnode) {
|
view(vnode) {
|
||||||
|
@@ -40,6 +40,7 @@ export default class UserCard extends Component {
|
|||||||
menuClassName: 'Dropdown-menu--right',
|
menuClassName: 'Dropdown-menu--right',
|
||||||
buttonClassName: this.attrs.controlsButtonClassName,
|
buttonClassName: this.attrs.controlsButtonClassName,
|
||||||
label: app.translator.trans('core.forum.user_controls.button'),
|
label: app.translator.trans('core.forum.user_controls.button'),
|
||||||
|
accessibleToggleLabel: app.translator.trans('core.forum.user_controls.toggle_dropdown_accessible_label'),
|
||||||
icon: 'fas fa-ellipsis-v',
|
icon: 'fas fa-ellipsis-v',
|
||||||
},
|
},
|
||||||
controls
|
controls
|
||||||
|
@@ -51,7 +51,7 @@ export default class UserPage extends Page {
|
|||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
]
|
]
|
||||||
: [<LoadingIndicator className="LoadingIndicator--block" />]}
|
: [<LoadingIndicator display="block" />]}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,8 @@
|
|||||||
import 'expose-loader?punycode!punycode';
|
// Expose punycode and ColorThief to the window browser object
|
||||||
import 'expose-loader?ColorThief!color-thief-browser';
|
import 'expose-loader?exposes=punycode!punycode';
|
||||||
|
import 'expose-loader?exposes=ColorThief!color-thief-browser';
|
||||||
|
|
||||||
import ForumApplication from './ForumApplication';
|
import app from './app';
|
||||||
|
|
||||||
const app = new ForumApplication();
|
|
||||||
|
|
||||||
// Backwards compatibility
|
|
||||||
window.app = app;
|
|
||||||
|
|
||||||
export { app };
|
export { app };
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import subclassOf from '../../common/utils/subclassOf';
|
import subclassOf from '../../common/utils/subclassOf';
|
||||||
import Stream from '../../common/utils/Stream';
|
import Stream from '../../common/utils/Stream';
|
||||||
import ReplyComposer from '../components/ReplyComposer';
|
import ReplyComposer from '../components/ReplyComposer';
|
||||||
import EditorDriverInterface from '../utils/EditorDriverInterface';
|
import EditorDriverInterface from '../../common/utils/EditorDriverInterface';
|
||||||
|
|
||||||
class ComposerState {
|
class ComposerState {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
13
js/src/forum/utils/isSafariMobile.ts
Normal file
13
js/src/forum/utils/isSafariMobile.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* @see https://stackoverflow.com/a/31732310
|
||||||
|
*/
|
||||||
|
export default function isSafariMobile(): boolean {
|
||||||
|
return (
|
||||||
|
'ontouchstart' in window &&
|
||||||
|
navigator.vendor &&
|
||||||
|
navigator.vendor.includes('Apple') &&
|
||||||
|
navigator.userAgent &&
|
||||||
|
!navigator.userAgent.includes('CriOS') &&
|
||||||
|
!navigator.userAgent.includes('FxiOS')
|
||||||
|
);
|
||||||
|
}
|
@@ -1,15 +1,26 @@
|
|||||||
const config = require('flarum-webpack-config');
|
const config = require('flarum-webpack-config');
|
||||||
|
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||||
const merge = require('webpack-merge');
|
const merge = require('webpack-merge');
|
||||||
|
|
||||||
|
const useBundleAnalyzer = process.env.ANALYZER === 'true';
|
||||||
|
|
||||||
|
const plugins = [];
|
||||||
|
|
||||||
|
if (useBundleAnalyzer) {
|
||||||
|
plugins.push(new BundleAnalyzerPlugin());
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = merge(config(), {
|
module.exports = merge(config(), {
|
||||||
output: {
|
output: {
|
||||||
library: 'flarum.core'
|
library: 'flarum.core',
|
||||||
},
|
},
|
||||||
|
|
||||||
// temporary TS configuration
|
// temporary TS configuration
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.ts', '.tsx', '.js', '.json'],
|
extensions: ['.ts', '.tsx', '.js', '.json'],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
plugins,
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports['module'].rules[0].test = /\.(tsx?|js)$/;
|
module.exports['module'].rules[0].test = /\.(tsx?|js)$/;
|
||||||
|
@@ -123,6 +123,9 @@
|
|||||||
// the left side of the screen. On other devices, the drawer has no specific
|
// the left side of the screen. On other devices, the drawer has no specific
|
||||||
// appearance.
|
// appearance.
|
||||||
@media @phone {
|
@media @phone {
|
||||||
|
.App:not(.drawerOpen) .App-drawer {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
.drawerOpen {
|
.drawerOpen {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@@ -90,14 +90,11 @@
|
|||||||
.Button-label {
|
.Button-label {
|
||||||
.transition(margin-right 0.1s);
|
.transition(margin-right 0.1s);
|
||||||
}
|
}
|
||||||
.LoadingIndicator {
|
|
||||||
|
.LoadingIndicator-container {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
margin: 0 -5px 0 -15px;
|
margin-top: -0.175em;
|
||||||
}
|
margin-left: 4px;
|
||||||
&.loading {
|
|
||||||
.Button-label {
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,13 +15,9 @@
|
|||||||
float: left;
|
float: left;
|
||||||
margin-left: -65px;
|
margin-left: -65px;
|
||||||
margin-top: -4px;
|
margin-top: -4px;
|
||||||
|
|
||||||
.LoadingIndicator {
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Checkbox--switch .Checkbox-display {
|
.Checkbox--switch .Checkbox-display {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
@@ -31,8 +27,28 @@
|
|||||||
background: @control-bg;
|
background: @control-bg;
|
||||||
.transition(background-color 0.2s);
|
.transition(background-color 0.2s);
|
||||||
|
|
||||||
|
.LoadingIndicator {
|
||||||
|
--size: 22px !important;
|
||||||
|
|
||||||
|
&-container {
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.on& {
|
.on& {
|
||||||
background: #58a400;
|
background: #58a400;
|
||||||
|
|
||||||
|
.LoadingIndicator-container {
|
||||||
|
// Show loading indicator over the switch button
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.off& {
|
||||||
|
.LoadingIndicator-container {
|
||||||
|
// Show loading indicator over the switch button
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
@@ -2,13 +2,64 @@
|
|||||||
// Loading Indicators
|
// Loading Indicators
|
||||||
|
|
||||||
.LoadingIndicator {
|
.LoadingIndicator {
|
||||||
position: relative;
|
@spin-time: 750ms;
|
||||||
color: @muted-color;
|
|
||||||
|
// Use the value of `color` to maintain backwards compatibility
|
||||||
|
border-color: currentColor;
|
||||||
|
border-width: var(--thickness);
|
||||||
|
border-style: solid;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
|
||||||
|
animation: spin @spin-time linear infinite;
|
||||||
|
|
||||||
|
// <div> container around the spinner
|
||||||
|
// Used for positioning
|
||||||
|
&-container {
|
||||||
|
--size: 24px;
|
||||||
|
--thickness: 2px;
|
||||||
|
|
||||||
|
color: @muted-color;
|
||||||
|
|
||||||
|
// Center vertically and horizontally
|
||||||
|
// Allows people to set `height` and it'll stay centered within the new height
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
// Size
|
||||||
|
|
||||||
|
&--large {
|
||||||
|
--size: 32px;
|
||||||
|
--thickness: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--small {
|
||||||
|
--size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display types
|
||||||
|
|
||||||
|
&--block {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--inline {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.LoadingIndicator--inline {
|
|
||||||
display: inline-block;
|
@keyframes spin {
|
||||||
width: 25px;
|
from {
|
||||||
}
|
transform: rotate(0);
|
||||||
.LoadingIndicator--block {
|
}
|
||||||
height: 100px;
|
|
||||||
|
to {
|
||||||
|
transform: rotate(1turn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,14 +8,14 @@
|
|||||||
&.focused {
|
&.focused {
|
||||||
margin-left: -400px;
|
margin-left: -400px;
|
||||||
|
|
||||||
input, .Search-results {
|
input,
|
||||||
|
.Search-results {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.Search-results {
|
.Search-results {
|
||||||
max-height: 70vh;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
left: auto;
|
left: auto;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -62,11 +62,21 @@
|
|||||||
.transition(all 0.4s);
|
.transition(all 0.4s);
|
||||||
box-sizing: inherit !important;
|
box-sizing: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.LoadingIndicator-container {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
.Button {
|
.Button {
|
||||||
float: left;
|
float: left;
|
||||||
margin-left: -36px;
|
margin-left: -36px;
|
||||||
width: 36px !important;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
|
width: 36px !important;
|
||||||
|
|
||||||
|
&.LoadingIndicator {
|
||||||
|
width: var(--size) !important;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
49
less/common/TextEditor.less
Normal file
49
less/common/TextEditor.less
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
.TextEditor .TextEditor-editor {
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0 0 10px;
|
||||||
|
border: 0;
|
||||||
|
resize: none;
|
||||||
|
color: @text-color;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
|
||||||
|
&, &:focus, &[disabled] {
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media @phone {
|
||||||
|
font-size: 16px; // minimum font-size required to prevent page zoom on focus in iOS 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.TextEditor-controls {
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 0;
|
||||||
|
list-style-type: none;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.TextEditor-toolbar {
|
||||||
|
.Button--icon {
|
||||||
|
width: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media @tablet-up {
|
||||||
|
.TextEditor-controls {
|
||||||
|
margin: 0 -20px 0 -105px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-top: 1px solid @control-bg;
|
||||||
|
|
||||||
|
.fullScreen & {
|
||||||
|
margin: 0;
|
||||||
|
border-top: 0;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -25,5 +25,6 @@
|
|||||||
@import "Placeholder";
|
@import "Placeholder";
|
||||||
@import "Search";
|
@import "Search";
|
||||||
@import "Select";
|
@import "Select";
|
||||||
|
@import "TextEditor";
|
||||||
@import "Tooltip";
|
@import "Tooltip";
|
||||||
@import "ValidationError";
|
@import "ValidationError";
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
.header-background() {
|
.header-background() {
|
||||||
background: fade(@header-bg, 98%);
|
background: @header-bg;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@@ -1,227 +1,151 @@
|
|||||||
// Vendor Prefixes
|
// Vendor Prefixes
|
||||||
//
|
|
||||||
// All vendor mixins are deprecated as of v3.2.0 due to the introduction of
|
|
||||||
// Autoprefixer in our Gruntfile. They will be removed in v4.
|
|
||||||
|
|
||||||
// - Animations
|
|
||||||
// - Backface visibility
|
|
||||||
// - Box shadow
|
|
||||||
// - Box sizing
|
|
||||||
// - Content columns
|
|
||||||
// - Hyphens
|
|
||||||
// - Placeholder text
|
|
||||||
// - Transformations
|
|
||||||
// - Transitions
|
|
||||||
// - User Select
|
|
||||||
|
|
||||||
|
// These aim to ensure that Flarum remains compatible with most modern devices.
|
||||||
|
// The vendor presets below are to try to remain compatible with iOS 9+ and other
|
||||||
|
// major browsers (Chrome/Firefox/new Edge/Safari desktop).
|
||||||
|
|
||||||
// Animations
|
// Animations
|
||||||
|
// These remain for backwards compatibility with existing styles.
|
||||||
.animation(@animation) {
|
.animation(@animation) {
|
||||||
-webkit-animation: @animation;
|
animation: @animation;
|
||||||
-o-animation: @animation;
|
|
||||||
animation: @animation;
|
|
||||||
}
|
}
|
||||||
.animation-name(@name) {
|
.animation-name(@name) {
|
||||||
-webkit-animation-name: @name;
|
animation-name: @name;
|
||||||
animation-name: @name;
|
|
||||||
}
|
}
|
||||||
.animation-duration(@duration) {
|
.animation-duration(@duration) {
|
||||||
-webkit-animation-duration: @duration;
|
animation-duration: @duration;
|
||||||
animation-duration: @duration;
|
|
||||||
}
|
}
|
||||||
.animation-timing-function(@timing-function) {
|
.animation-timing-function(@timing-function) {
|
||||||
-webkit-animation-timing-function: @timing-function;
|
animation-timing-function: @timing-function;
|
||||||
animation-timing-function: @timing-function;
|
|
||||||
}
|
}
|
||||||
.animation-delay(@delay) {
|
.animation-delay(@delay) {
|
||||||
-webkit-animation-delay: @delay;
|
animation-delay: @delay;
|
||||||
animation-delay: @delay;
|
|
||||||
}
|
}
|
||||||
.animation-iteration-count(@iteration-count) {
|
.animation-iteration-count(@iteration-count) {
|
||||||
-webkit-animation-iteration-count: @iteration-count;
|
animation-iteration-count: @iteration-count;
|
||||||
animation-iteration-count: @iteration-count;
|
|
||||||
}
|
}
|
||||||
.animation-direction(@direction) {
|
.animation-direction(@direction) {
|
||||||
-webkit-animation-direction: @direction;
|
animation-direction: @direction;
|
||||||
animation-direction: @direction;
|
|
||||||
}
|
}
|
||||||
.animation-fill-mode(@fill-mode) {
|
.animation-fill-mode(@fill-mode) {
|
||||||
-webkit-animation-fill-mode: @fill-mode;
|
animation-fill-mode: @fill-mode;
|
||||||
animation-fill-mode: @fill-mode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backface visibility
|
// Backface visibility
|
||||||
// Prevent browsers from flickering when using CSS 3D transforms.
|
// Prevent browsers from flickering when using CSS 3D transforms.
|
||||||
// Default value is `visible`, but can be changed to `hidden`
|
// Default value is `visible`, but can be changed to `hidden`
|
||||||
|
.backface-visibility(@visibility) {
|
||||||
.backface-visibility(@visibility){
|
// Safari
|
||||||
-webkit-backface-visibility: @visibility;
|
-webkit-backface-visibility: @visibility;
|
||||||
-moz-backface-visibility: @visibility;
|
backface-visibility: @visibility;
|
||||||
backface-visibility: @visibility;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop shadows
|
// Drop shadows
|
||||||
//
|
// These remain for backwards compatibility with existing styles.
|
||||||
// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's
|
|
||||||
// supported browsers that have box shadow capabilities now support it.
|
|
||||||
|
|
||||||
.box-shadow(@shadow) {
|
.box-shadow(@shadow) {
|
||||||
-webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1
|
box-shadow: @shadow;
|
||||||
box-shadow: @shadow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Box sizing
|
// Box sizing
|
||||||
|
// These remain for backwards compatibility with existing styles.
|
||||||
.box-sizing(@boxmodel) {
|
.box-sizing(@boxmodel) {
|
||||||
-webkit-box-sizing: @boxmodel;
|
box-sizing: @boxmodel;
|
||||||
-moz-box-sizing: @boxmodel;
|
|
||||||
box-sizing: @boxmodel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSS3 Content Columns
|
// CSS3 Content Columns
|
||||||
.content-columns(@column-count; @column-gap: @grid-gutter-width) {
|
.content-columns(@column-count; @column-gap: @grid-gutter-width) {
|
||||||
|
// Safari
|
||||||
-webkit-column-count: @column-count;
|
-webkit-column-count: @column-count;
|
||||||
-moz-column-count: @column-count;
|
column-count: @column-count;
|
||||||
column-count: @column-count;
|
// Safari
|
||||||
-webkit-column-gap: @column-gap;
|
-webkit-column-gap: @column-gap;
|
||||||
-moz-column-gap: @column-gap;
|
column-gap: @column-gap;
|
||||||
column-gap: @column-gap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional hyphenation
|
// Optional hyphenation
|
||||||
.hyphens(@mode: auto) {
|
.hyphens(@mode: auto) {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
// Safari
|
||||||
-webkit-hyphens: @mode;
|
-webkit-hyphens: @mode;
|
||||||
-moz-hyphens: @mode;
|
hyphens: @mode;
|
||||||
-ms-hyphens: @mode; // IE10+
|
|
||||||
-o-hyphens: @mode;
|
|
||||||
hyphens: @mode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Placeholder text
|
// Placeholder text
|
||||||
.placeholder(@color) {
|
.placeholder(@color) {
|
||||||
// Firefox
|
// Safari
|
||||||
&::-moz-placeholder {
|
&::-webkit-input-placeholder,
|
||||||
|
&::placeholder {
|
||||||
color: @color;
|
color: @color;
|
||||||
opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526
|
|
||||||
}
|
}
|
||||||
&:-ms-input-placeholder { color: @color; } // Internet Explorer 10+
|
|
||||||
&::-webkit-input-placeholder { color: @color; } // Safari and Chrome
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transformations
|
// Transformations
|
||||||
|
// These remain for backwards compatibility with existing styles.
|
||||||
.scale(@ratio) {
|
.scale(@ratio) {
|
||||||
-webkit-transform: scale(@ratio);
|
transform: scale(@ratio);
|
||||||
-ms-transform: scale(@ratio); // IE9 only
|
|
||||||
-o-transform: scale(@ratio);
|
|
||||||
transform: scale(@ratio);
|
|
||||||
}
|
}
|
||||||
.scale(@ratioX; @ratioY) {
|
.scale(@ratioX; @ratioY) {
|
||||||
-webkit-transform: scale(@ratioX, @ratioY);
|
transform: scale(@ratioX, @ratioY);
|
||||||
-ms-transform: scale(@ratioX, @ratioY); // IE9 only
|
|
||||||
-o-transform: scale(@ratioX, @ratioY);
|
|
||||||
transform: scale(@ratioX, @ratioY);
|
|
||||||
}
|
}
|
||||||
.scaleX(@ratio) {
|
.scaleX(@ratio) {
|
||||||
-webkit-transform: scaleX(@ratio);
|
transform: scaleX(@ratio);
|
||||||
-ms-transform: scaleX(@ratio); // IE9 only
|
|
||||||
-o-transform: scaleX(@ratio);
|
|
||||||
transform: scaleX(@ratio);
|
|
||||||
}
|
}
|
||||||
.scaleY(@ratio) {
|
.scaleY(@ratio) {
|
||||||
-webkit-transform: scaleY(@ratio);
|
transform: scaleY(@ratio);
|
||||||
-ms-transform: scaleY(@ratio); // IE9 only
|
|
||||||
-o-transform: scaleY(@ratio);
|
|
||||||
transform: scaleY(@ratio);
|
|
||||||
}
|
}
|
||||||
.skew(@x; @y) {
|
.skew(@x; @y) {
|
||||||
-webkit-transform: skewX(@x) skewY(@y);
|
transform: skewX(@x) skewY(@y);
|
||||||
-ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+
|
|
||||||
-o-transform: skewX(@x) skewY(@y);
|
|
||||||
transform: skewX(@x) skewY(@y);
|
|
||||||
}
|
}
|
||||||
.translate(@x; @y) {
|
.translate(@x; @y) {
|
||||||
-webkit-transform: translate(@x, @y);
|
transform: translate(@x, @y);
|
||||||
-ms-transform: translate(@x, @y); // IE9 only
|
|
||||||
-o-transform: translate(@x, @y);
|
|
||||||
transform: translate(@x, @y);
|
|
||||||
}
|
}
|
||||||
.translate3d(@x; @y; @z) {
|
.translate3d(@x; @y; @z) {
|
||||||
-webkit-transform: translate3d(@x, @y, @z);
|
transform: translate3d(@x, @y, @z);
|
||||||
transform: translate3d(@x, @y, @z);
|
|
||||||
}
|
}
|
||||||
.rotate(@degrees) {
|
.rotate(@degrees) {
|
||||||
-webkit-transform: rotate(@degrees);
|
transform: rotate(@degrees);
|
||||||
-ms-transform: rotate(@degrees); // IE9 only
|
|
||||||
-o-transform: rotate(@degrees);
|
|
||||||
transform: rotate(@degrees);
|
|
||||||
}
|
}
|
||||||
.rotateX(@degrees) {
|
.rotateX(@degrees) {
|
||||||
-webkit-transform: rotateX(@degrees);
|
transform: rotateX(@degrees);
|
||||||
-ms-transform: rotateX(@degrees); // IE9 only
|
|
||||||
-o-transform: rotateX(@degrees);
|
|
||||||
transform: rotateX(@degrees);
|
|
||||||
}
|
}
|
||||||
.rotateY(@degrees) {
|
.rotateY(@degrees) {
|
||||||
-webkit-transform: rotateY(@degrees);
|
transform: rotateY(@degrees);
|
||||||
-ms-transform: rotateY(@degrees); // IE9 only
|
|
||||||
-o-transform: rotateY(@degrees);
|
|
||||||
transform: rotateY(@degrees);
|
|
||||||
}
|
}
|
||||||
.perspective(@perspective) {
|
.perspective(@perspective) {
|
||||||
-webkit-perspective: @perspective;
|
perspective: @perspective;
|
||||||
-moz-perspective: @perspective;
|
|
||||||
perspective: @perspective;
|
|
||||||
}
|
}
|
||||||
.perspective-origin(@perspective) {
|
.perspective-origin(@perspective) {
|
||||||
-webkit-perspective-origin: @perspective;
|
perspective-origin: @perspective;
|
||||||
-moz-perspective-origin: @perspective;
|
|
||||||
perspective-origin: @perspective;
|
|
||||||
}
|
}
|
||||||
.transform-origin(@origin) {
|
.transform-origin(@origin) {
|
||||||
-webkit-transform-origin: @origin;
|
transform-origin: @origin;
|
||||||
-moz-transform-origin: @origin;
|
|
||||||
-ms-transform-origin: @origin; // IE9 only
|
|
||||||
transform-origin: @origin;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Transitions
|
// Transitions
|
||||||
|
// These remain for backwards compatibility with existing styles.
|
||||||
.transition(@transition) {
|
.transition(@transition) {
|
||||||
-webkit-transition: @transition;
|
transition: @transition;
|
||||||
-o-transition: @transition;
|
|
||||||
transition: @transition;
|
|
||||||
}
|
}
|
||||||
.transition-property(@transition-property) {
|
.transition-property(@transition-property) {
|
||||||
-webkit-transition-property: @transition-property;
|
transition-property: @transition-property;
|
||||||
transition-property: @transition-property;
|
|
||||||
}
|
}
|
||||||
.transition-delay(@transition-delay) {
|
.transition-delay(@transition-delay) {
|
||||||
-webkit-transition-delay: @transition-delay;
|
transition-delay: @transition-delay;
|
||||||
transition-delay: @transition-delay;
|
|
||||||
}
|
}
|
||||||
.transition-duration(@transition-duration) {
|
.transition-duration(@transition-duration) {
|
||||||
-webkit-transition-duration: @transition-duration;
|
transition-duration: @transition-duration;
|
||||||
transition-duration: @transition-duration;
|
|
||||||
}
|
}
|
||||||
.transition-timing-function(@timing-function) {
|
.transition-timing-function(@timing-function) {
|
||||||
-webkit-transition-timing-function: @timing-function;
|
transition-timing-function: @timing-function;
|
||||||
transition-timing-function: @timing-function;
|
|
||||||
}
|
}
|
||||||
.transition-transform(@transition) {
|
.transition-transform(@transition) {
|
||||||
-webkit-transition: -webkit-transform @transition;
|
transition: transform @transition;
|
||||||
-moz-transition: -moz-transform @transition;
|
|
||||||
-o-transition: -o-transform @transition;
|
|
||||||
transition: transform @transition;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// User select
|
// User select
|
||||||
// For selecting text on the page
|
// For selecting text on the page
|
||||||
|
|
||||||
.user-select(@select) {
|
.user-select(@select) {
|
||||||
|
// Safari + MS Edge
|
||||||
-webkit-user-select: @select;
|
-webkit-user-select: @select;
|
||||||
-moz-user-select: @select;
|
user-select: @select;
|
||||||
-ms-user-select: @select; // IE10+
|
|
||||||
user-select: @select;
|
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|
||||||
.LoadingIndicator {
|
.LoadingIndicator-container {
|
||||||
height: 46px;
|
height: 46px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,7 +23,7 @@
|
|||||||
&.dragover .Dropdown-toggle {
|
&.dragover .Dropdown-toggle {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.LoadingIndicator {
|
.LoadingIndicator-container {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@@ -92,7 +92,7 @@
|
|||||||
border-radius: @border-radius @border-radius 0 0;
|
border-radius: @border-radius @border-radius 0 0;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
display: block;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ComposerBody-editor {
|
.ComposerBody-editor {
|
||||||
@@ -120,6 +120,12 @@
|
|||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
padding-top: @header-height-phone;
|
padding-top: @header-height-phone;
|
||||||
|
|
||||||
|
// Fixes a bug where fixed position doesn't properly work in Safari mobile
|
||||||
|
// https://github.com/flarum/core/issues/2652
|
||||||
|
.mobile-safari & {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: " ";
|
content: " ";
|
||||||
.header-background();
|
.header-background();
|
||||||
@@ -319,56 +325,3 @@
|
|||||||
left: @pane-width;
|
left: @pane-width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
|
||||||
// Text Editor
|
|
||||||
|
|
||||||
.TextEditor .TextEditor-editor {
|
|
||||||
border-radius: 0;
|
|
||||||
padding: 0 0 10px;
|
|
||||||
border: 0;
|
|
||||||
resize: none;
|
|
||||||
color: @text-color;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.7;
|
|
||||||
|
|
||||||
&, &:focus, &[disabled] {
|
|
||||||
background: none;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media @phone {
|
|
||||||
font-size: 16px; // minimum font-size required to prevent page zoom on focus in iOS 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.TextEditor-controls {
|
|
||||||
margin: 0;
|
|
||||||
padding: 10px 0;
|
|
||||||
list-style-type: none;
|
|
||||||
overflow-x: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.TextEditor-toolbar {
|
|
||||||
.Button--icon {
|
|
||||||
width: 28px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media @tablet-up {
|
|
||||||
.TextEditor-controls {
|
|
||||||
margin: 0 -20px 0 -105px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-top: 1px solid @control-bg;
|
|
||||||
|
|
||||||
.fullScreen & {
|
|
||||||
margin: 0;
|
|
||||||
border-top: 0;
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -10,9 +10,10 @@
|
|||||||
.DiscussionList-loadMore {
|
.DiscussionList-loadMore {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
|
||||||
.DiscussionList-loadMore .LoadingIndicator {
|
.LoadingIndicator-container {
|
||||||
height: 46px;
|
height: 46px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media @phone {
|
@media @phone {
|
||||||
|
@@ -18,6 +18,8 @@
|
|||||||
height: 300px;
|
height: 300px;
|
||||||
min-height: 50px; // JavaScript sets a max-height
|
min-height: 50px; // JavaScript sets a max-height
|
||||||
position: relative;
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
.user-select(none);
|
||||||
}
|
}
|
||||||
.Scrubber-before, .Scrubber-after {
|
.Scrubber-before, .Scrubber-after {
|
||||||
border-left: 1px solid @control-bg;
|
border-left: 1px solid @control-bg;
|
||||||
@@ -42,6 +44,7 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
|
cursor: move;
|
||||||
}
|
}
|
||||||
.Scrubber-bar {
|
.Scrubber-bar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@@ -278,6 +278,7 @@ core:
|
|||||||
rename_button: => core.ref.rename
|
rename_button: => core.ref.rename
|
||||||
reply_button: => core.ref.reply
|
reply_button: => core.ref.reply
|
||||||
restore_button: => core.ref.restore
|
restore_button: => core.ref.restore
|
||||||
|
toggle_dropdown_accessible_label: Toggle discussion actions dropdown menu
|
||||||
|
|
||||||
# These translations are used in the discussion list.
|
# These translations are used in the discussion list.
|
||||||
discussion_list:
|
discussion_list:
|
||||||
@@ -316,10 +317,12 @@ core:
|
|||||||
header:
|
header:
|
||||||
admin_button: Administration
|
admin_button: Administration
|
||||||
back_to_index_tooltip: Back to Discussion List
|
back_to_index_tooltip: Back to Discussion List
|
||||||
|
locale_dropdown_accessible_label: Change forum locale
|
||||||
log_in_link: => core.ref.log_in
|
log_in_link: => core.ref.log_in
|
||||||
log_out_button: => core.ref.log_out
|
log_out_button: => core.ref.log_out
|
||||||
profile_button: Profile
|
profile_button: Profile
|
||||||
search_placeholder: Search Forum
|
search_placeholder: Search Forum
|
||||||
|
session_dropdown_accessible_label: Toggle session options dropdown menu
|
||||||
settings_button: => core.ref.settings
|
settings_button: => core.ref.settings
|
||||||
sign_up_link: => core.ref.sign_up
|
sign_up_link: => core.ref.sign_up
|
||||||
|
|
||||||
@@ -332,6 +335,7 @@ core:
|
|||||||
meta_title_text: => core.ref.all_discussions
|
meta_title_text: => core.ref.all_discussions
|
||||||
refresh_tooltip: Refresh
|
refresh_tooltip: Refresh
|
||||||
start_discussion_button: => core.ref.start_a_discussion
|
start_discussion_button: => core.ref.start_a_discussion
|
||||||
|
toggle_sidenav_dropdown_accessible_label: Toggle navigation dropdown menu
|
||||||
|
|
||||||
# These translations are used by the sorting control above the discussion list.
|
# These translations are used by the sorting control above the discussion list.
|
||||||
index_sort:
|
index_sort:
|
||||||
@@ -339,6 +343,7 @@ core:
|
|||||||
newest_button: Newest
|
newest_button: Newest
|
||||||
oldest_button: Oldest
|
oldest_button: Oldest
|
||||||
relevance_button: Relevance
|
relevance_button: Relevance
|
||||||
|
toggle_dropdown_accessible_label: Change discussion list sorting
|
||||||
top_button: Top
|
top_button: Top
|
||||||
|
|
||||||
# These translations are used in the Log In modal dialog.
|
# These translations are used in the Log In modal dialog.
|
||||||
@@ -359,6 +364,7 @@ core:
|
|||||||
mark_all_as_read_tooltip: => core.ref.mark_all_as_read
|
mark_all_as_read_tooltip: => core.ref.mark_all_as_read
|
||||||
mark_as_read_tooltip: Mark as Read
|
mark_as_read_tooltip: Mark as Read
|
||||||
title: => core.ref.notifications
|
title: => core.ref.notifications
|
||||||
|
toggle_dropdown_accessible_label: View notifications
|
||||||
tooltip: => core.ref.notifications
|
tooltip: => core.ref.notifications
|
||||||
|
|
||||||
# These translations are used by tooltips displayed for individual posts.
|
# These translations are used by tooltips displayed for individual posts.
|
||||||
@@ -375,6 +381,7 @@ core:
|
|||||||
edit_button: => core.ref.edit
|
edit_button: => core.ref.edit
|
||||||
hide_confirmation: "Are you sure you want to delete this post?"
|
hide_confirmation: "Are you sure you want to delete this post?"
|
||||||
restore_button: => core.ref.restore
|
restore_button: => core.ref.restore
|
||||||
|
toggle_dropdown_accessible_label: Toggle post controls dropdown menu
|
||||||
|
|
||||||
# These translations are used in the scrubber to the right of the post stream.
|
# These translations are used in the scrubber to the right of the post stream.
|
||||||
post_scrubber:
|
post_scrubber:
|
||||||
@@ -448,6 +455,7 @@ core:
|
|||||||
delete_error_message: "Deletion of user <i>{username} ({email})</i> failed"
|
delete_error_message: "Deletion of user <i>{username} ({email})</i> failed"
|
||||||
delete_success_message: "User <i>{username} ({email})</i> was deleted"
|
delete_success_message: "User <i>{username} ({email})</i> was deleted"
|
||||||
edit_button: => core.ref.edit
|
edit_button: => core.ref.edit
|
||||||
|
toggle_dropdown_accessible_label: Toggle user controls dropdown menu
|
||||||
|
|
||||||
# These translations are used in the alert that is shown when a new user has not confirmed their email address.
|
# These translations are used in the alert that is shown when a new user has not confirmed their email address.
|
||||||
user_email_confirmation:
|
user_email_confirmation:
|
||||||
@@ -462,6 +470,10 @@ core:
|
|||||||
badge:
|
badge:
|
||||||
hidden_tooltip: Hidden
|
hidden_tooltip: Hidden
|
||||||
|
|
||||||
|
# These translations are used in the dropdown component.
|
||||||
|
dropdown:
|
||||||
|
toggle_dropdown_accessible_label: Toggle dropdown menu
|
||||||
|
|
||||||
# These translations are displayed as error messages.
|
# These translations are displayed as error messages.
|
||||||
error:
|
error:
|
||||||
dependent_extensions_message: "Cannot disable {extension} until the following dependent extensions are disabled: {extensions}"
|
dependent_extensions_message: "Cannot disable {extension} until the following dependent extensions are disabled: {extensions}"
|
||||||
@@ -471,6 +483,10 @@ core:
|
|||||||
permission_denied_message: You do not have permission to do that.
|
permission_denied_message: You do not have permission to do that.
|
||||||
rate_limit_exceeded_message: You're going a little too quickly. Please try again in a few seconds.
|
rate_limit_exceeded_message: You're going a little too quickly. Please try again in a few seconds.
|
||||||
|
|
||||||
|
# These translations are used in the loading indicator component.
|
||||||
|
loading_indicator:
|
||||||
|
accessible_label: => core.ref.loading
|
||||||
|
|
||||||
# These translations are used as suffixes when abbreviating numbers.
|
# These translations are used as suffixes when abbreviating numbers.
|
||||||
number_suffix:
|
number_suffix:
|
||||||
kilo_text: K
|
kilo_text: K
|
||||||
@@ -493,7 +509,7 @@ core:
|
|||||||
content:
|
content:
|
||||||
javascript_disabled_message: This site is best viewed in a modern browser with JavaScript enabled.
|
javascript_disabled_message: This site is best viewed in a modern browser with JavaScript enabled.
|
||||||
load_error_message: Something went wrong while trying to load the full version of this site. Try hard-refreshing this page to fix the error.
|
load_error_message: Something went wrong while trying to load the full version of this site. Try hard-refreshing this page to fix the error.
|
||||||
loading_text: Loading...
|
loading_text: => core.ref.loading
|
||||||
|
|
||||||
# Translations in this namespace are displayed in the basic HTML discussion view.
|
# Translations in this namespace are displayed in the basic HTML discussion view.
|
||||||
discussion:
|
discussion:
|
||||||
@@ -612,6 +628,7 @@ core:
|
|||||||
icon: Icon
|
icon: Icon
|
||||||
icon_text: "Enter the name of any <a>FontAwesome</a> icon class, <em>including</em> the <code>fas fa-</code> prefix."
|
icon_text: "Enter the name of any <a>FontAwesome</a> icon class, <em>including</em> the <code>fas fa-</code> prefix."
|
||||||
load_more: Load More
|
load_more: Load More
|
||||||
|
loading: Loading...
|
||||||
log_in: Log In
|
log_in: Log In
|
||||||
log_out: Log Out
|
log_out: Log Out
|
||||||
mark_all_as_read: Mark All as Read
|
mark_all_as_read: Mark All as Read
|
||||||
|
@@ -30,8 +30,6 @@ return [
|
|||||||
$table->integer('hide_user_id')->unsigned()->nullable();
|
$table->integer('hide_user_id')->unsigned()->nullable();
|
||||||
|
|
||||||
$table->unique(['discussion_id', 'number']);
|
$table->unique(['discussion_id', 'number']);
|
||||||
|
|
||||||
$table->engine = 'MyISAM';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$connection = $schema->getConnection();
|
$connection = $schema->getConnection();
|
||||||
|
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* For detailed copyright and license information, please view the
|
||||||
|
* LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Schema\Builder;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'up' => function (Builder $schema) {
|
||||||
|
$schema->table('access_tokens', function (Blueprint $table) {
|
||||||
|
$table->string('type', 100)->index();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Since all active sessions will stop working on update due to switching from user_id to access_token
|
||||||
|
// We can do things simple here by terminating all tokens that have the previously default lifetime
|
||||||
|
$schema->getConnection()->table('access_tokens')
|
||||||
|
->where('lifetime_seconds', 3600)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
// We will then assume that all remaining tokens are remember tokens
|
||||||
|
// This will include tokens that previously had a custom lifetime
|
||||||
|
$schema->getConnection()->table('access_tokens')
|
||||||
|
->update([
|
||||||
|
'type' => 'session_remember',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$schema->table('access_tokens', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('lifetime_seconds');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
'down' => function (Builder $schema) {
|
||||||
|
$schema->table('access_tokens', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('type');
|
||||||
|
$table->integer('lifetime_seconds');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
];
|
35
migrations/2021_03_02_040500_change_access_tokens_add_id.php
Normal file
35
migrations/2021_03_02_040500_change_access_tokens_add_id.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* For detailed copyright and license information, please view the
|
||||||
|
* LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Schema\Builder;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'up' => function (Builder $schema) {
|
||||||
|
$schema->table('access_tokens', function (Blueprint $table) {
|
||||||
|
// Replace primary key with unique index so we can create a new primary
|
||||||
|
$table->dropPrimary('token');
|
||||||
|
$table->unique('token');
|
||||||
|
});
|
||||||
|
|
||||||
|
// This needs to be done in a second statement because of the order Laravel runs operations in
|
||||||
|
$schema->table('access_tokens', function (Blueprint $table) {
|
||||||
|
// Introduce new increment-based ID
|
||||||
|
$table->increments('id')->first();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
'down' => function (Builder $schema) {
|
||||||
|
$schema->table('access_tokens', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('id');
|
||||||
|
$table->dropIndex('token');
|
||||||
|
$table->primary('token');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
];
|
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* For detailed copyright and license information, please view the
|
||||||
|
* LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Flarum\Database\Migration;
|
||||||
|
|
||||||
|
return Migration::addColumns('access_tokens', [
|
||||||
|
'title' => ['string', 'length' => 150, 'nullable' => true],
|
||||||
|
// Accommodates both IPv4 and IPv6 as strings
|
||||||
|
'last_ip_address' => ['string', 'length' => 45, 'nullable' => true],
|
||||||
|
// Technically, there's no limit to a user agent length
|
||||||
|
// Most are around 150 in length, and the general recommendation seems to be below 200
|
||||||
|
// We're going to use the longest string possible to be safe
|
||||||
|
// There will still be exceptions, we'll just truncate them
|
||||||
|
'last_user_agent' => ['string', 'length' => 255, 'nullable' => true],
|
||||||
|
]);
|
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* For detailed copyright and license information, please view the
|
||||||
|
* LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Schema\Builder;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'up' => function (Builder $schema) {
|
||||||
|
if (! $schema->hasColumn('migrations', 'id')) {
|
||||||
|
$schema->table('migrations', function (Blueprint $table) {
|
||||||
|
$table->increments('id')->first();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'down' => function (Builder $schema) {
|
||||||
|
$schema->table('migrations', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
];
|
@@ -36,19 +36,20 @@ class AdminServiceProvider extends AbstractServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function register()
|
public function register()
|
||||||
{
|
{
|
||||||
$this->app->extend(UrlGenerator::class, function (UrlGenerator $url) {
|
$this->container->extend(UrlGenerator::class, function (UrlGenerator $url) {
|
||||||
return $url->addCollection('admin', $this->app->make('flarum.admin.routes'), 'admin');
|
return $url->addCollection('admin', $this->container->make('flarum.admin.routes'), 'admin');
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->singleton('flarum.admin.routes', function () {
|
$this->container->singleton('flarum.admin.routes', function () {
|
||||||
$routes = new RouteCollection;
|
$routes = new RouteCollection;
|
||||||
$this->populateRoutes($routes);
|
$this->populateRoutes($routes);
|
||||||
|
|
||||||
return $routes;
|
return $routes;
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->singleton('flarum.admin.middleware', function () {
|
$this->container->singleton('flarum.admin.middleware', function () {
|
||||||
return [
|
return [
|
||||||
|
HttpMiddleware\InjectActorReference::class,
|
||||||
'flarum.admin.error_handler',
|
'flarum.admin.error_handler',
|
||||||
HttpMiddleware\ParseJsonBody::class,
|
HttpMiddleware\ParseJsonBody::class,
|
||||||
HttpMiddleware\StartSession::class,
|
HttpMiddleware\StartSession::class,
|
||||||
@@ -61,23 +62,23 @@ class AdminServiceProvider extends AbstractServiceProvider
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->bind('flarum.admin.error_handler', function () {
|
$this->container->bind('flarum.admin.error_handler', function () {
|
||||||
return new HttpMiddleware\HandleErrors(
|
return new HttpMiddleware\HandleErrors(
|
||||||
$this->app->make(Registry::class),
|
$this->container->make(Registry::class),
|
||||||
$this->app['flarum.config']->inDebugMode() ? $this->app->make(WhoopsFormatter::class) : $this->app->make(ViewFormatter::class),
|
$this->container['flarum.config']->inDebugMode() ? $this->container->make(WhoopsFormatter::class) : $this->container->make(ViewFormatter::class),
|
||||||
$this->app->tagged(Reporter::class)
|
$this->container->tagged(Reporter::class)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->bind('flarum.admin.route_resolver', function () {
|
$this->container->bind('flarum.admin.route_resolver', function () {
|
||||||
return new HttpMiddleware\ResolveRoute($this->app->make('flarum.admin.routes'));
|
return new HttpMiddleware\ResolveRoute($this->container->make('flarum.admin.routes'));
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->singleton('flarum.admin.handler', function () {
|
$this->container->singleton('flarum.admin.handler', function () {
|
||||||
$pipe = new MiddlewarePipe;
|
$pipe = new MiddlewarePipe;
|
||||||
|
|
||||||
foreach ($this->app->make('flarum.admin.middleware') as $middleware) {
|
foreach ($this->container->make('flarum.admin.middleware') as $middleware) {
|
||||||
$pipe->pipe($this->app->make($middleware));
|
$pipe->pipe($this->container->make($middleware));
|
||||||
}
|
}
|
||||||
|
|
||||||
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
|
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
|
||||||
@@ -85,9 +86,9 @@ class AdminServiceProvider extends AbstractServiceProvider
|
|||||||
return $pipe;
|
return $pipe;
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->bind('flarum.assets.admin', function () {
|
$this->container->bind('flarum.assets.admin', function () {
|
||||||
/** @var \Flarum\Frontend\Assets $assets */
|
/** @var \Flarum\Frontend\Assets $assets */
|
||||||
$assets = $this->app->make('flarum.assets.factory')('admin');
|
$assets = $this->container->make('flarum.assets.factory')('admin');
|
||||||
|
|
||||||
$assets->js(function (SourceCollector $sources) {
|
$assets->js(function (SourceCollector $sources) {
|
||||||
$sources->addFile(__DIR__.'/../../js/dist/admin.js');
|
$sources->addFile(__DIR__.'/../../js/dist/admin.js');
|
||||||
@@ -97,17 +98,17 @@ class AdminServiceProvider extends AbstractServiceProvider
|
|||||||
$sources->addFile(__DIR__.'/../../less/admin.less');
|
$sources->addFile(__DIR__.'/../../less/admin.less');
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->make(AddTranslations::class)->forFrontend('admin')->to($assets);
|
$this->container->make(AddTranslations::class)->forFrontend('admin')->to($assets);
|
||||||
$this->app->make(AddLocaleAssets::class)->to($assets);
|
$this->container->make(AddLocaleAssets::class)->to($assets);
|
||||||
|
|
||||||
return $assets;
|
return $assets;
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->bind('flarum.frontend.admin', function () {
|
$this->container->bind('flarum.frontend.admin', function () {
|
||||||
/** @var \Flarum\Frontend\Frontend $frontend */
|
/** @var \Flarum\Frontend\Frontend $frontend */
|
||||||
$frontend = $this->app->make('flarum.frontend.factory')('admin');
|
$frontend = $this->container->make('flarum.frontend.factory')('admin');
|
||||||
|
|
||||||
$frontend->content($this->app->make(Content\AdminPayload::class));
|
$frontend->content($this->container->make(Content\AdminPayload::class));
|
||||||
|
|
||||||
return $frontend;
|
return $frontend;
|
||||||
});
|
});
|
||||||
@@ -120,14 +121,14 @@ class AdminServiceProvider extends AbstractServiceProvider
|
|||||||
{
|
{
|
||||||
$this->loadViewsFrom(__DIR__.'/../../views', 'flarum.admin');
|
$this->loadViewsFrom(__DIR__.'/../../views', 'flarum.admin');
|
||||||
|
|
||||||
$events = $this->app->make('events');
|
$events = $this->container->make('events');
|
||||||
|
|
||||||
$events->listen(
|
$events->listen(
|
||||||
[Enabled::class, Disabled::class, ClearingCache::class],
|
[Enabled::class, Disabled::class, ClearingCache::class],
|
||||||
function () {
|
function () {
|
||||||
$recompile = new RecompileFrontendAssets(
|
$recompile = new RecompileFrontendAssets(
|
||||||
$this->app->make('flarum.assets.admin'),
|
$this->container->make('flarum.assets.admin'),
|
||||||
$this->app->make(LocaleManager::class)
|
$this->container->make(LocaleManager::class)
|
||||||
);
|
);
|
||||||
$recompile->flush();
|
$recompile->flush();
|
||||||
}
|
}
|
||||||
@@ -137,8 +138,8 @@ class AdminServiceProvider extends AbstractServiceProvider
|
|||||||
Saved::class,
|
Saved::class,
|
||||||
function (Saved $event) {
|
function (Saved $event) {
|
||||||
$recompile = new RecompileFrontendAssets(
|
$recompile = new RecompileFrontendAssets(
|
||||||
$this->app->make('flarum.assets.admin'),
|
$this->container->make('flarum.assets.admin'),
|
||||||
$this->app->make(LocaleManager::class)
|
$this->container->make(LocaleManager::class)
|
||||||
);
|
);
|
||||||
$recompile->whenSettingsSaved($event);
|
$recompile->whenSettingsSaved($event);
|
||||||
}
|
}
|
||||||
@@ -150,7 +151,7 @@ class AdminServiceProvider extends AbstractServiceProvider
|
|||||||
*/
|
*/
|
||||||
protected function populateRoutes(RouteCollection $routes)
|
protected function populateRoutes(RouteCollection $routes)
|
||||||
{
|
{
|
||||||
$factory = $this->app->make(RouteHandlerFactory::class);
|
$factory = $this->container->make(RouteHandlerFactory::class);
|
||||||
|
|
||||||
$callback = include __DIR__.'/routes.php';
|
$callback = include __DIR__.'/routes.php';
|
||||||
$callback($routes, $factory);
|
$callback($routes, $factory);
|
||||||
|
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace Flarum\Admin\Middleware;
|
namespace Flarum\Admin\Middleware;
|
||||||
|
|
||||||
|
use Flarum\Http\RequestUtil;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Psr\Http\Server\MiddlewareInterface as Middleware;
|
use Psr\Http\Server\MiddlewareInterface as Middleware;
|
||||||
@@ -18,7 +19,7 @@ class RequireAdministrateAbility implements Middleware
|
|||||||
{
|
{
|
||||||
public function process(Request $request, Handler $handler): Response
|
public function process(Request $request, Handler $handler): Response
|
||||||
{
|
{
|
||||||
$request->getAttribute('actor')->assertAdmin();
|
RequestUtil::getActor($request)->assertAdmin();
|
||||||
|
|
||||||
return $handler->handle($request);
|
return $handler->handle($request);
|
||||||
}
|
}
|
||||||
|
@@ -30,18 +30,18 @@ class ApiServiceProvider extends AbstractServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function register()
|
public function register()
|
||||||
{
|
{
|
||||||
$this->app->extend(UrlGenerator::class, function (UrlGenerator $url) {
|
$this->container->extend(UrlGenerator::class, function (UrlGenerator $url) {
|
||||||
return $url->addCollection('api', $this->app->make('flarum.api.routes'), 'api');
|
return $url->addCollection('api', $this->container->make('flarum.api.routes'), 'api');
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->singleton('flarum.api.routes', function () {
|
$this->container->singleton('flarum.api.routes', function () {
|
||||||
$routes = new RouteCollection;
|
$routes = new RouteCollection;
|
||||||
$this->populateRoutes($routes);
|
$this->populateRoutes($routes);
|
||||||
|
|
||||||
return $routes;
|
return $routes;
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->singleton('flarum.api.throttlers', function () {
|
$this->container->singleton('flarum.api.throttlers', function () {
|
||||||
return [
|
return [
|
||||||
'bypassThrottlingAttribute' => function ($request) {
|
'bypassThrottlingAttribute' => function ($request) {
|
||||||
if ($request->getAttribute('bypassThrottling')) {
|
if ($request->getAttribute('bypassThrottling')) {
|
||||||
@@ -51,12 +51,13 @@ class ApiServiceProvider extends AbstractServiceProvider
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->bind(Middleware\ThrottleApi::class, function ($app) {
|
$this->container->bind(Middleware\ThrottleApi::class, function ($container) {
|
||||||
return new Middleware\ThrottleApi($app->make('flarum.api.throttlers'));
|
return new Middleware\ThrottleApi($container->make('flarum.api.throttlers'));
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->singleton('flarum.api.middleware', function () {
|
$this->container->singleton('flarum.api.middleware', function () {
|
||||||
return [
|
return [
|
||||||
|
HttpMiddleware\InjectActorReference::class,
|
||||||
'flarum.api.error_handler',
|
'flarum.api.error_handler',
|
||||||
HttpMiddleware\ParseJsonBody::class,
|
HttpMiddleware\ParseJsonBody::class,
|
||||||
Middleware\FakeHttpMethods::class,
|
Middleware\FakeHttpMethods::class,
|
||||||
@@ -71,23 +72,23 @@ class ApiServiceProvider extends AbstractServiceProvider
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->bind('flarum.api.error_handler', function () {
|
$this->container->bind('flarum.api.error_handler', function () {
|
||||||
return new HttpMiddleware\HandleErrors(
|
return new HttpMiddleware\HandleErrors(
|
||||||
$this->app->make(Registry::class),
|
$this->container->make(Registry::class),
|
||||||
new JsonApiFormatter($this->app['flarum.config']->inDebugMode()),
|
new JsonApiFormatter($this->container['flarum.config']->inDebugMode()),
|
||||||
$this->app->tagged(Reporter::class)
|
$this->container->tagged(Reporter::class)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->bind('flarum.api.route_resolver', function () {
|
$this->container->bind('flarum.api.route_resolver', function () {
|
||||||
return new HttpMiddleware\ResolveRoute($this->app->make('flarum.api.routes'));
|
return new HttpMiddleware\ResolveRoute($this->container->make('flarum.api.routes'));
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->singleton('flarum.api.handler', function () {
|
$this->container->singleton('flarum.api.handler', function () {
|
||||||
$pipe = new MiddlewarePipe;
|
$pipe = new MiddlewarePipe;
|
||||||
|
|
||||||
foreach ($this->app->make('flarum.api.middleware') as $middleware) {
|
foreach ($this->container->make('flarum.api.middleware') as $middleware) {
|
||||||
$pipe->pipe($this->app->make($middleware));
|
$pipe->pipe($this->container->make($middleware));
|
||||||
}
|
}
|
||||||
|
|
||||||
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
|
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
|
||||||
@@ -95,7 +96,7 @@ class ApiServiceProvider extends AbstractServiceProvider
|
|||||||
return $pipe;
|
return $pipe;
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->singleton('flarum.api.notification_serializers', function () {
|
$this->container->singleton('flarum.api.notification_serializers', function () {
|
||||||
return [
|
return [
|
||||||
'discussionRenamed' => BasicDiscussionSerializer::class
|
'discussionRenamed' => BasicDiscussionSerializer::class
|
||||||
];
|
];
|
||||||
@@ -109,9 +110,9 @@ class ApiServiceProvider extends AbstractServiceProvider
|
|||||||
{
|
{
|
||||||
$this->setNotificationSerializers();
|
$this->setNotificationSerializers();
|
||||||
|
|
||||||
AbstractSerializeController::setContainer($this->app);
|
AbstractSerializeController::setContainer($this->container);
|
||||||
|
|
||||||
AbstractSerializer::setContainer($this->app);
|
AbstractSerializer::setContainer($this->container);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,7 +120,7 @@ class ApiServiceProvider extends AbstractServiceProvider
|
|||||||
*/
|
*/
|
||||||
protected function setNotificationSerializers()
|
protected function setNotificationSerializers()
|
||||||
{
|
{
|
||||||
$serializers = $this->app->make('flarum.api.notification_serializers');
|
$serializers = $this->container->make('flarum.api.notification_serializers');
|
||||||
|
|
||||||
foreach ($serializers as $type => $serializer) {
|
foreach ($serializers as $type => $serializer) {
|
||||||
NotificationSerializer::setSubjectSerializer($type, $serializer);
|
NotificationSerializer::setSubjectSerializer($type, $serializer);
|
||||||
@@ -133,7 +134,7 @@ class ApiServiceProvider extends AbstractServiceProvider
|
|||||||
*/
|
*/
|
||||||
protected function populateRoutes(RouteCollection $routes)
|
protected function populateRoutes(RouteCollection $routes)
|
||||||
{
|
{
|
||||||
$factory = $this->app->make(RouteHandlerFactory::class);
|
$factory = $this->container->make(RouteHandlerFactory::class);
|
||||||
|
|
||||||
$callback = include __DIR__.'/routes.php';
|
$callback = include __DIR__.'/routes.php';
|
||||||
$callback($routes, $factory);
|
$callback($routes, $factory);
|
||||||
|
@@ -12,6 +12,7 @@ namespace Flarum\Api;
|
|||||||
use Exception;
|
use Exception;
|
||||||
use Flarum\Foundation\ErrorHandling\JsonApiFormatter;
|
use Flarum\Foundation\ErrorHandling\JsonApiFormatter;
|
||||||
use Flarum\Foundation\ErrorHandling\Registry;
|
use Flarum\Foundation\ErrorHandling\Registry;
|
||||||
|
use Flarum\Http\RequestUtil;
|
||||||
use Flarum\User\User;
|
use Flarum\User\User;
|
||||||
use Illuminate\Contracts\Container\Container;
|
use Illuminate\Contracts\Container\Container;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
@@ -56,7 +57,7 @@ class Client
|
|||||||
{
|
{
|
||||||
$request = ServerRequestFactory::fromGlobals(null, $queryParams, $body);
|
$request = ServerRequestFactory::fromGlobals(null, $queryParams, $body);
|
||||||
|
|
||||||
$request = $request->withAttribute('actor', $actor);
|
$request = RequestUtil::withActor($request, $actor);
|
||||||
|
|
||||||
if (is_string($controller)) {
|
if (is_string($controller)) {
|
||||||
$controller = $this->container->make($controller);
|
$controller = $this->container->make($controller);
|
||||||
|
@@ -11,6 +11,9 @@ namespace Flarum\Api\Controller;
|
|||||||
|
|
||||||
use Flarum\Api\JsonApiResponse;
|
use Flarum\Api\JsonApiResponse;
|
||||||
use Illuminate\Contracts\Container\Container;
|
use Illuminate\Contracts\Container\Container;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
@@ -84,6 +87,11 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
|
|||||||
*/
|
*/
|
||||||
protected static $beforeSerializationCallbacks = [];
|
protected static $beforeSerializationCallbacks = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected static $loadRelations = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
@@ -139,6 +147,47 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
|
|||||||
*/
|
*/
|
||||||
abstract protected function createElement($data, SerializerInterface $serializer);
|
abstract protected function createElement($data, SerializerInterface $serializer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eager loads the required relationships.
|
||||||
|
*
|
||||||
|
* @param Collection $models
|
||||||
|
* @param array $relations
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function loadRelations(Collection $models, array $relations): void
|
||||||
|
{
|
||||||
|
$addedRelations = [];
|
||||||
|
|
||||||
|
foreach (array_reverse(array_merge([static::class], class_parents($this))) as $class) {
|
||||||
|
if (isset(static::$loadRelations[$class])) {
|
||||||
|
$addedRelations = array_merge($addedRelations, static::$loadRelations[$class]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($addedRelations)) {
|
||||||
|
usort($addedRelations, function ($a, $b) {
|
||||||
|
return substr_count($a, '.') - substr_count($b, '.');
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach ($addedRelations as $relation) {
|
||||||
|
if (strpos($relation, '.') !== false) {
|
||||||
|
$parentRelation = Str::beforeLast($relation, '.');
|
||||||
|
|
||||||
|
if (! in_array($parentRelation, $relations, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$relations[] = $relation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($relations)) {
|
||||||
|
$relations = array_unique($relations);
|
||||||
|
$models->loadMissing($relations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ServerRequestInterface $request
|
* @param ServerRequestInterface $request
|
||||||
* @return array
|
* @return array
|
||||||
@@ -207,6 +256,11 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
|
|||||||
return new Parameters($request->getQueryParams());
|
return new Parameters($request->getQueryParams());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function sortIsDefault(ServerRequestInterface $request): bool
|
||||||
|
{
|
||||||
|
return ! Arr::get($request->getQueryParams(), 'sort');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the serializer that will serialize data for the endpoint.
|
* Set the serializer that will serialize data for the endpoint.
|
||||||
*
|
*
|
||||||
@@ -348,4 +402,13 @@ abstract class AbstractSerializeController implements RequestHandlerInterface
|
|||||||
|
|
||||||
static::$beforeSerializationCallbacks[$controllerClass][] = $callback;
|
static::$beforeSerializationCallbacks[$controllerClass][] = $callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function setLoadRelations(string $controllerClass, array $relations)
|
||||||
|
{
|
||||||
|
if (! isset(static::$loadRelations[$controllerClass])) {
|
||||||
|
static::$loadRelations[$controllerClass] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static::$loadRelations[$controllerClass] = array_merge(static::$loadRelations[$controllerClass], $relations);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,7 @@
|
|||||||
namespace Flarum\Api\Controller;
|
namespace Flarum\Api\Controller;
|
||||||
|
|
||||||
use Flarum\Foundation\Console\CacheClearCommand;
|
use Flarum\Foundation\Console\CacheClearCommand;
|
||||||
|
use Flarum\Http\RequestUtil;
|
||||||
use Laminas\Diactoros\Response\EmptyResponse;
|
use Laminas\Diactoros\Response\EmptyResponse;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Symfony\Component\Console\Input\ArrayInput;
|
use Symfony\Component\Console\Input\ArrayInput;
|
||||||
@@ -35,7 +36,7 @@ class ClearCacheController extends AbstractDeleteController
|
|||||||
*/
|
*/
|
||||||
protected function delete(ServerRequestInterface $request)
|
protected function delete(ServerRequestInterface $request)
|
||||||
{
|
{
|
||||||
$request->getAttribute('actor')->assertAdmin();
|
RequestUtil::getActor($request)->assertAdmin();
|
||||||
|
|
||||||
$this->command->run(
|
$this->command->run(
|
||||||
new ArrayInput([]),
|
new ArrayInput([]),
|
||||||
|
@@ -12,6 +12,7 @@ namespace Flarum\Api\Controller;
|
|||||||
use Flarum\Api\Serializer\DiscussionSerializer;
|
use Flarum\Api\Serializer\DiscussionSerializer;
|
||||||
use Flarum\Discussion\Command\ReadDiscussion;
|
use Flarum\Discussion\Command\ReadDiscussion;
|
||||||
use Flarum\Discussion\Command\StartDiscussion;
|
use Flarum\Discussion\Command\StartDiscussion;
|
||||||
|
use Flarum\Http\RequestUtil;
|
||||||
use Illuminate\Contracts\Bus\Dispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
@@ -53,7 +54,7 @@ class CreateDiscussionController extends AbstractCreateController
|
|||||||
*/
|
*/
|
||||||
protected function data(ServerRequestInterface $request, Document $document)
|
protected function data(ServerRequestInterface $request, Document $document)
|
||||||
{
|
{
|
||||||
$actor = $request->getAttribute('actor');
|
$actor = RequestUtil::getActor($request);
|
||||||
$ipAddress = $request->getAttribute('ipAddress');
|
$ipAddress = $request->getAttribute('ipAddress');
|
||||||
|
|
||||||
$discussion = $this->bus->dispatch(
|
$discussion = $this->bus->dispatch(
|
||||||
|
@@ -11,6 +11,7 @@ namespace Flarum\Api\Controller;
|
|||||||
|
|
||||||
use Flarum\Api\Serializer\GroupSerializer;
|
use Flarum\Api\Serializer\GroupSerializer;
|
||||||
use Flarum\Group\Command\CreateGroup;
|
use Flarum\Group\Command\CreateGroup;
|
||||||
|
use Flarum\Http\RequestUtil;
|
||||||
use Illuminate\Contracts\Bus\Dispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
@@ -42,7 +43,7 @@ class CreateGroupController extends AbstractCreateController
|
|||||||
protected function data(ServerRequestInterface $request, Document $document)
|
protected function data(ServerRequestInterface $request, Document $document)
|
||||||
{
|
{
|
||||||
return $this->bus->dispatch(
|
return $this->bus->dispatch(
|
||||||
new CreateGroup($request->getAttribute('actor'), Arr::get($request->getParsedBody(), 'data', []))
|
new CreateGroup(RequestUtil::getActor($request), Arr::get($request->getParsedBody(), 'data', []))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,6 +11,7 @@ namespace Flarum\Api\Controller;
|
|||||||
|
|
||||||
use Flarum\Api\Serializer\PostSerializer;
|
use Flarum\Api\Serializer\PostSerializer;
|
||||||
use Flarum\Discussion\Command\ReadDiscussion;
|
use Flarum\Discussion\Command\ReadDiscussion;
|
||||||
|
use Flarum\Http\RequestUtil;
|
||||||
use Flarum\Post\Command\PostReply;
|
use Flarum\Post\Command\PostReply;
|
||||||
use Illuminate\Contracts\Bus\Dispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
@@ -52,7 +53,7 @@ class CreatePostController extends AbstractCreateController
|
|||||||
*/
|
*/
|
||||||
protected function data(ServerRequestInterface $request, Document $document)
|
protected function data(ServerRequestInterface $request, Document $document)
|
||||||
{
|
{
|
||||||
$actor = $request->getAttribute('actor');
|
$actor = RequestUtil::getActor($request);
|
||||||
$data = Arr::get($request->getParsedBody(), 'data', []);
|
$data = Arr::get($request->getParsedBody(), 'data', []);
|
||||||
$discussionId = Arr::get($data, 'relationships.discussion.data.id');
|
$discussionId = Arr::get($data, 'relationships.discussion.data.id');
|
||||||
$ipAddress = $request->getAttribute('ipAddress');
|
$ipAddress = $request->getAttribute('ipAddress');
|
||||||
|
@@ -9,7 +9,8 @@
|
|||||||
|
|
||||||
namespace Flarum\Api\Controller;
|
namespace Flarum\Api\Controller;
|
||||||
|
|
||||||
use Flarum\Http\AccessToken;
|
use Flarum\Http\RememberAccessToken;
|
||||||
|
use Flarum\Http\SessionAccessToken;
|
||||||
use Flarum\User\Exception\NotAuthenticatedException;
|
use Flarum\User\Exception\NotAuthenticatedException;
|
||||||
use Flarum\User\UserRepository;
|
use Flarum\User\UserRepository;
|
||||||
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
|
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher;
|
||||||
@@ -58,7 +59,6 @@ class CreateTokenController implements RequestHandlerInterface
|
|||||||
|
|
||||||
$identification = Arr::get($body, 'identification');
|
$identification = Arr::get($body, 'identification');
|
||||||
$password = Arr::get($body, 'password');
|
$password = Arr::get($body, 'password');
|
||||||
$lifetime = Arr::get($body, 'lifetime', 3600);
|
|
||||||
|
|
||||||
$user = $this->users->findByIdentification($identification);
|
$user = $this->users->findByIdentification($identification);
|
||||||
|
|
||||||
@@ -66,8 +66,14 @@ class CreateTokenController implements RequestHandlerInterface
|
|||||||
throw new NotAuthenticatedException;
|
throw new NotAuthenticatedException;
|
||||||
}
|
}
|
||||||
|
|
||||||
$token = AccessToken::generate($user->id, $lifetime);
|
if (Arr::get($body, 'remember')) {
|
||||||
$token->save();
|
$token = RememberAccessToken::generate($user->id);
|
||||||
|
} else {
|
||||||
|
$token = SessionAccessToken::generate($user->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do a first update here to log the IP/agent of the token creator, even if the token is never used afterwards
|
||||||
|
$token->touch($request);
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'token' => $token->token,
|
'token' => $token->token,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user