mirror of
https://github.com/flarum/core.git
synced 2025-08-21 23:56:33 +02:00
Compare commits
265 Commits
v0.1.0-bet
...
v0.1.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
d1c25a4bad | ||
|
4b2f0c2d1a | ||
|
48be5ac2eb | ||
|
0b3a4264a3 | ||
|
76ea6f3695 | ||
|
7120ba2050 | ||
|
ff77912dc6 | ||
|
53b32eda12 | ||
|
6d69e90662 | ||
|
589e903c71 | ||
|
4fe7acfddf | ||
|
685d5f1517 | ||
|
a5c8ef0566 | ||
|
cb428f1e4a | ||
|
3d11309b35 | ||
|
b13adfec84 | ||
|
b2b5789c25 | ||
|
673a78a203 | ||
|
31caced04c | ||
|
5d88ad2431 | ||
|
96a40fd6ea | ||
|
77086c9be6 | ||
|
3c629f091d | ||
|
820752f61c | ||
|
67f3a4a5bf | ||
|
cd4d669127 | ||
|
238f2fca73 | ||
|
7e33690660 | ||
|
eef895c16f | ||
|
2be964f8e2 | ||
|
2f05a2d80b | ||
|
e6a001335d | ||
|
4c03f13fef | ||
|
588dd7b213 | ||
|
1ca1639139 | ||
|
476c1a5691 | ||
|
3b19fe3a33 | ||
|
65f2d84d55 | ||
|
cf63e063ba | ||
|
cd6e6addf7 | ||
|
1395ce6c30 | ||
|
05732be929 | ||
|
5097d7f9a4 | ||
|
0b3bc9f2ba | ||
|
8087d9ea47 | ||
|
d1c436c4d5 | ||
|
e37c7a9b06 | ||
|
dc757fae5f | ||
|
3b236dd66e | ||
|
e2e5ac8c0c | ||
|
beb2f91fef | ||
|
2391471937 | ||
|
f631b98df6 | ||
|
01cb5c4478 | ||
|
fc517ca94d | ||
|
393fa67d2d | ||
|
cb6ac9e9e2 | ||
|
7d2f24bb47 | ||
|
5a7b57df96 | ||
|
a75a76e95b | ||
|
639f5c0114 | ||
|
15c0a8c2db | ||
|
1b5b91c85b | ||
|
5d5f47aab2 | ||
|
24713733fc | ||
|
56b39f9fba | ||
|
cdbc4b9717 | ||
|
594a2ba8cc | ||
|
445517ee84 | ||
|
b4cf197cc6 | ||
|
102db3c913 | ||
|
0ccfad3931 | ||
|
a6cf10f854 | ||
|
83c22d73a4 | ||
|
952b4693da | ||
|
c7b6426fd4 | ||
|
acdb1ff749 | ||
|
50e56ac0a1 | ||
|
82fc4dd483 | ||
|
5390187a4f | ||
|
e4412178b1 | ||
|
2b5dab73f9 | ||
|
db7a03fbe5 | ||
|
ad95a44e7d | ||
|
59613910b1 | ||
|
13fe162db3 | ||
|
51955504aa | ||
|
05fe4446bf | ||
|
71d2e71908 | ||
|
93f3f22623 | ||
|
ff69dade15 | ||
|
17851c4dfe | ||
|
46dfdf2deb | ||
|
d944a9e618 | ||
|
2143a96c19 | ||
|
d7fe3ca35b | ||
|
48e29ed168 | ||
|
0ad4c0ac61 | ||
|
458f4f811c | ||
|
e90dfe04fd | ||
|
191589e2b1 | ||
|
96c4e6b147 | ||
|
d15a9dc0f0 | ||
|
08312568ba | ||
|
31be2f8f86 | ||
|
89598646c1 | ||
|
b3035c18b6 | ||
|
235c265c06 | ||
|
f1a1a7a806 | ||
|
dfef3c1ff1 | ||
|
fb09cef540 | ||
|
24ed2c0d8f | ||
|
173f88da92 | ||
|
9ecb5f437a | ||
|
97979b2189 | ||
|
efff4c1801 | ||
|
2018e424ec | ||
|
36ad4a8554 | ||
|
3581fe8d1e | ||
|
90ce0fa521 | ||
|
63b5cd0812 | ||
|
2a3240b9d1 | ||
|
e0790de2e5 | ||
|
c99c83435b | ||
|
c8f2d94558 | ||
|
c842fa0184 | ||
|
ad2bbdd115 | ||
|
db06b8c71a | ||
|
3cec7e8b46 | ||
|
60d78cedef | ||
|
2980c94247 | ||
|
9b5ec9d7ba | ||
|
f17f0b5278 | ||
|
be924c4fa0 | ||
|
285e397d05 | ||
|
2e27d5938a | ||
|
be013c6db0 | ||
|
dfc0cf53b0 | ||
|
09ad4a180b | ||
|
194f304752 | ||
|
aaab2cc86e | ||
|
ba7fba9015 | ||
|
4ec108f28a | ||
|
e5a7013c2c | ||
|
df2a199b48 | ||
|
b123e435ff | ||
|
17da649d0a | ||
|
1e33ca4111 | ||
|
8506d095db | ||
|
94a62293eb | ||
|
02bcb0f898 | ||
|
98ea4d1e71 | ||
|
5120d9577e | ||
|
23eaee6b16 | ||
|
15398fcc6d | ||
|
bd1d05ee2c | ||
|
4a6137fdb1 | ||
|
537ab6e41f | ||
|
ace4bcf7d8 | ||
|
159810c335 | ||
|
b7120fb176 | ||
|
1f5219f2a2 | ||
|
e8a6fe2f7b | ||
|
1a2cc6a603 | ||
|
417b7f7972 | ||
|
9e3771cac3 | ||
|
819728d8dd | ||
|
e3c7f5379b | ||
|
41ccade385 | ||
|
6d42bcb5ce | ||
|
096aae7919 | ||
|
5bbcba6332 | ||
|
b671c3ccfa | ||
|
9d89d8a127 | ||
|
6dfe455fd6 | ||
|
1f2eaea960 | ||
|
b2ec380d4c | ||
|
08dbc246dd | ||
|
3767ee4bf6 | ||
|
248de34242 | ||
|
8d671f4de4 | ||
|
6de7038f83 | ||
|
07a20a10fd | ||
|
c8027d344a | ||
|
f7709aff95 | ||
|
46818ccd94 | ||
|
f6f9e45085 | ||
|
ff0ce09620 | ||
|
e86cc39f5b | ||
|
a719d4109f | ||
|
1aaf588341 | ||
|
0fcc8dca46 | ||
|
5a4e3b09cf | ||
|
bf87518161 | ||
|
08dae7b530 | ||
|
aa516fb5c3 | ||
|
1cac48f90a | ||
|
5e476fae16 | ||
|
341ffaced5 | ||
|
595d715b1d | ||
|
8c8de8eb22 | ||
|
5431a90dbd | ||
|
7a8c7518bd | ||
|
08f0425c43 | ||
|
ffb76715f6 | ||
|
9cb45c98d8 | ||
|
e0db5823ee | ||
|
46f7f6b3fe | ||
|
fbcd2cf88c | ||
|
e55b7a14e5 | ||
|
32601d2c98 | ||
|
d9d52dab3c | ||
|
d743e56bc1 | ||
|
0cf000122f | ||
|
973ca16eee | ||
|
262dc70fe1 | ||
|
3efd5fbcb0 | ||
|
c97b01a445 | ||
|
b0b3af0305 | ||
|
387109002e | ||
|
1d9e7b0262 | ||
|
094ad74abc | ||
|
67e9e23df1 | ||
|
1cfae4ad14 | ||
|
9896378b59 | ||
|
287ce2fddd | ||
|
cea1cbc2d6 | ||
|
b9148364fa | ||
|
2ba890c239 | ||
|
55e80f135d | ||
|
81a1c0955b | ||
|
05386b1259 | ||
|
d96e57eabb | ||
|
173de809b8 | ||
|
c432ed7d5c | ||
|
172fffd1ed | ||
|
4bfbf68bca | ||
|
cd411a0c6b | ||
|
7f05d9dce3 | ||
|
b3a5822ddb | ||
|
a1e1635019 | ||
|
1cc5e1cb26 | ||
|
a80d72d165 | ||
|
153a82e937 | ||
|
262a934747 | ||
|
a61929730e | ||
|
ce02387ee4 | ||
|
2c4fae60bc | ||
|
7eab206f91 | ||
|
599958354c | ||
|
2088fceb8b | ||
|
5b25a77e82 | ||
|
59c534a882 | ||
|
c79bda6279 | ||
|
6374f92676 | ||
|
1f4e03d1fa | ||
|
acf67ca416 | ||
|
bd750ca154 | ||
|
61b09ac982 | ||
|
6d895e6d77 | ||
|
e199997231 | ||
|
095e8164e8 | ||
|
0bdf873e65 | ||
|
439b867dde | ||
|
63d00e8b34 |
@@ -1,5 +0,0 @@
|
||||
**/bower_components/**/*
|
||||
**/node_modules/**/*
|
||||
vendor/**/*
|
||||
**/Gulpfile.js
|
||||
**/dist/**/*
|
175
.eslintrc
175
.eslintrc
@@ -1,175 +0,0 @@
|
||||
{
|
||||
"parser": "babel-eslint", // https://github.com/babel/babel-eslint
|
||||
"env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments
|
||||
"browser": true // browser global variables
|
||||
},
|
||||
"ecmaFeatures": {
|
||||
"arrowFunctions": true,
|
||||
"blockBindings": true,
|
||||
"classes": true,
|
||||
"defaultParams": true,
|
||||
"destructuring": true,
|
||||
"forOf": true,
|
||||
"generators": false,
|
||||
"modules": true,
|
||||
"objectLiteralComputedProperties": true,
|
||||
"objectLiteralDuplicateProperties": false,
|
||||
"objectLiteralShorthandMethods": true,
|
||||
"objectLiteralShorthandProperties": true,
|
||||
"spread": true,
|
||||
"superInFunctions": true,
|
||||
"templateStrings": true,
|
||||
"jsx": true
|
||||
},
|
||||
"globals": {
|
||||
"m": true,
|
||||
"app": true,
|
||||
"$": true,
|
||||
"moment": true
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-vars": 1,
|
||||
|
||||
/**
|
||||
* Strict mode
|
||||
*/
|
||||
// babel inserts "use strict"; for us
|
||||
"strict": [2, "never"], // http://eslint.org/docs/rules/strict
|
||||
|
||||
/**
|
||||
* ES6
|
||||
*/
|
||||
"no-var": 2, // http://eslint.org/docs/rules/no-var
|
||||
"prefer-const": 2, // http://eslint.org/docs/rules/prefer-const
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*/
|
||||
"no-shadow": 2, // http://eslint.org/docs/rules/no-shadow
|
||||
"no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names
|
||||
"no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars
|
||||
"vars": "local",
|
||||
"args": "after-used"
|
||||
}],
|
||||
"no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define
|
||||
|
||||
/**
|
||||
* Possible errors
|
||||
*/
|
||||
"comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle
|
||||
"no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign
|
||||
"no-console": 1, // http://eslint.org/docs/rules/no-console
|
||||
"no-debugger": 1, // http://eslint.org/docs/rules/no-debugger
|
||||
"no-alert": 1, // http://eslint.org/docs/rules/no-alert
|
||||
"no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition
|
||||
"no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys
|
||||
"no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case
|
||||
"no-empty": 2, // http://eslint.org/docs/rules/no-empty
|
||||
"no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign
|
||||
"no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast
|
||||
"no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi
|
||||
"no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign
|
||||
"no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations
|
||||
"no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp
|
||||
"no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace
|
||||
"no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls
|
||||
"no-reserved-keys": 2, // http://eslint.org/docs/rules/no-reserved-keys
|
||||
"no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays
|
||||
"no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable
|
||||
"use-isnan": 2, // http://eslint.org/docs/rules/use-isnan
|
||||
"block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var
|
||||
|
||||
/**
|
||||
* Best practices
|
||||
*/
|
||||
"consistent-return": 2, // http://eslint.org/docs/rules/consistent-return
|
||||
"curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly
|
||||
"default-case": 2, // http://eslint.org/docs/rules/default-case
|
||||
"dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation
|
||||
"allowKeywords": true
|
||||
}],
|
||||
"eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq
|
||||
"no-caller": 2, // http://eslint.org/docs/rules/no-caller
|
||||
"no-else-return": 2, // http://eslint.org/docs/rules/no-else-return
|
||||
"no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null
|
||||
"no-eval": 2, // http://eslint.org/docs/rules/no-eval
|
||||
"no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native
|
||||
"no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind
|
||||
"no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough
|
||||
"no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal
|
||||
"no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval
|
||||
"no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks
|
||||
"no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func
|
||||
"no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str
|
||||
"no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign
|
||||
"no-new": 2, // http://eslint.org/docs/rules/no-new
|
||||
"no-new-func": 2, // http://eslint.org/docs/rules/no-new-func
|
||||
"no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers
|
||||
"no-octal": 2, // http://eslint.org/docs/rules/no-octal
|
||||
"no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape
|
||||
"no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign
|
||||
"no-proto": 2, // http://eslint.org/docs/rules/no-proto
|
||||
"no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare
|
||||
"no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign
|
||||
"no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare
|
||||
"no-sequences": 2, // http://eslint.org/docs/rules/no-sequences
|
||||
"no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal
|
||||
"no-with": 2, // http://eslint.org/docs/rules/no-with
|
||||
"radix": 2, // http://eslint.org/docs/rules/radix
|
||||
"vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top
|
||||
"wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife
|
||||
"yoda": 2, // http://eslint.org/docs/rules/yoda
|
||||
|
||||
/**
|
||||
* Style
|
||||
*/
|
||||
"indent": [2, 2], // http://eslint.org/docs/rules/indent
|
||||
"brace-style": [2, // http://eslint.org/docs/rules/brace-style
|
||||
"1tbs", {
|
||||
"allowSingleLine": true
|
||||
}],
|
||||
"quotes": [
|
||||
2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes
|
||||
],
|
||||
"camelcase": [2, { // http://eslint.org/docs/rules/camelcase
|
||||
"properties": "never"
|
||||
}],
|
||||
"comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing
|
||||
"before": false,
|
||||
"after": true
|
||||
}],
|
||||
"comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style
|
||||
"eol-last": 2, // http://eslint.org/docs/rules/eol-last
|
||||
"key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing
|
||||
"beforeColon": false,
|
||||
"afterColon": true
|
||||
}],
|
||||
"new-cap": [2, { // http://eslint.org/docs/rules/new-cap
|
||||
"newIsCap": true
|
||||
}],
|
||||
"no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines
|
||||
"max": 2
|
||||
}],
|
||||
"no-new-object": 2, // http://eslint.org/docs/rules/no-new-object
|
||||
"no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func
|
||||
"no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces
|
||||
"no-wrap-func": 2, // http://eslint.org/docs/rules/no-wrap-func
|
||||
"no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle
|
||||
"one-var": [2, "never"], // http://eslint.org/docs/rules/one-var
|
||||
"padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks
|
||||
"semi": [2, "always"], // http://eslint.org/docs/rules/semi
|
||||
"semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing
|
||||
"before": false,
|
||||
"after": true
|
||||
}],
|
||||
"space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords
|
||||
"space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks
|
||||
"space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren
|
||||
"space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops
|
||||
"space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case
|
||||
"spaced-line-comment": 2, // http://eslint.org/docs/rules/spaced-line-comment
|
||||
}
|
||||
}
|
26
.php_cs
26
.php_cs
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
$header = <<<EOF
|
||||
This file is part of Flarum.
|
||||
|
||||
(c) Toby Zerner <toby.zerner@gmail.com>
|
||||
|
||||
For the full copyright and license information, please view the LICENSE
|
||||
file that was distributed with this source code.
|
||||
EOF;
|
||||
|
||||
Symfony\CS\Fixer\Contrib\HeaderCommentFixer::setHeader($header);
|
||||
|
||||
$finder = Symfony\CS\Finder\DefaultFinder::create()
|
||||
->exclude('stubs')
|
||||
->in(__DIR__);
|
||||
|
||||
return Symfony\CS\Config\Config::create()
|
||||
->setUsingCache(true)
|
||||
->level(Symfony\CS\FixerInterface::PSR2_LEVEL)
|
||||
->fixers([
|
||||
'short_array_syntax',
|
||||
'header_comment',
|
||||
'-psr0'
|
||||
])
|
||||
->finder($finder);
|
17
.styleci.yml
Normal file
17
.styleci.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
preset: recommended
|
||||
|
||||
enabled:
|
||||
- logical_not_operators_with_successor_space
|
||||
|
||||
disabled:
|
||||
- align_double_arrow
|
||||
- multiline_array_trailing_comma
|
||||
- new_with_braces
|
||||
- phpdoc_align
|
||||
- phpdoc_order
|
||||
- phpdoc_separation
|
||||
- phpdoc_types
|
||||
|
||||
finder:
|
||||
exclude:
|
||||
- "stubs"
|
11
.travis.yml
11
.travis.yml
@@ -12,12 +12,12 @@ matrix:
|
||||
fast_finish: true
|
||||
|
||||
before_script:
|
||||
- curl -s http://getcomposer.org/installer | php
|
||||
- php composer.phar install
|
||||
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then phpenv config-rm xdebug.ini; fi;
|
||||
- composer self-update
|
||||
- composer install
|
||||
|
||||
script:
|
||||
- php composer.phar style
|
||||
- php composer.phar test
|
||||
- vendor/bin/phpunit -c tests/phpunit.xml --coverage-clover=coverage.xml
|
||||
|
||||
notifications:
|
||||
email:
|
||||
@@ -29,4 +29,7 @@ notifications:
|
||||
on_failure: always
|
||||
on_start: false
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
sudo: false
|
||||
|
108
CHANGELOG.md
108
CHANGELOG.md
@@ -1,108 +0,0 @@
|
||||
# Change Log
|
||||
All notable changes to Flarum and its bundled extensions will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [0.1.0-beta.4] - 2015-11-05
|
||||
### Added
|
||||
- Add an icon/label to the back button to indicate where it leads
|
||||
- Add "Loading..." text while the JavaScript payload is loading
|
||||
|
||||
### Fixed
|
||||
- Fix some admin actions resulting in "You do not have permission to do that"
|
||||
- Fix translation keys persisting after enabling an initial language pack
|
||||
- Fix translation `=>` references not being parsed in some cases
|
||||
|
||||
## [0.1.0-beta.3] - 2015-11-03
|
||||
### Architecture improvements
|
||||
- **Composer-driven extension architecture.** All extensions are Composer packages installable via Packagist.
|
||||
- **Backend codebase & API refactoring.** Classes, namespaces, and events systematically tidied up.
|
||||
|
||||
### Improved internationalization
|
||||
> A huge thanks to @dcsjapan for the countless hours he put in to make this stuff happen. You're amazing!
|
||||
|
||||
- New systematic translation key naming scheme.
|
||||
- Make many hardcoded strings translatable, including administration UI and validation messages.
|
||||
- More powerful pluralization via use of Symfony's Translation component instead of a proprietary one.
|
||||
|
||||
### New moderation tools
|
||||
- **Hide/restore discussions.** Discussions can be soft-deleted by moderators or by the OP if no one has replied.
|
||||
- **Flags.** New bundled extension that allows posts to be flagged for moderator review.
|
||||
- **Approval.** New bundled extension that hides/flags new posts to be approved by the moderation team.
|
||||
- **Akismet.** New bundled extension that checks new posts for spam with Akismet.
|
||||
- **IP address logging.** IP addresses are stored with posts for use by extensions (e.g. Akismet).
|
||||
- **Flood control.** Users must wait at least ten seconds between consecutive posts.
|
||||
|
||||
### Other features
|
||||
- **Social login.** New bundled extensions that allow users to log in with Facebook, Twitter, and GitHub.
|
||||
- **More compact post layout.** All controls are grouped over to the right.
|
||||
- **Improved permissions.** The admin Permissions page has been improved with icons and other tweaks.
|
||||
- **Improved extension management.** The admin Extensions page has a new look and is easier to use.
|
||||
- **Easier debugging.** The "oops" error message has a Debug button to inspect a failed AJAX request.
|
||||
- **Improved JavaScript minification.** Minification is done by ClosureCompiler only when debug mode is off, resulting in easier debugging and smaller production assets.
|
||||
|
||||
### Added
|
||||
- Allow HTML tag syntax in translations (#574)
|
||||
- Add gzip/caching directives to webserver configuration (#514)
|
||||
- API to set the asset compiler's filename
|
||||
- Migration generator, available via generate:migration console command
|
||||
- Tags: Ability to set the tags page as the home page
|
||||
- `bidi` attribute for Mithril elements as a shortcut to set up bidirectional bindings
|
||||
- `route` attribute for Mithril elements as a shortcut to link to a route
|
||||
- Abstract SettingsModal component for quickly building admin config modals
|
||||
- `Model::afterSave()` API to run callback after a model instance is saved
|
||||
- Sticky: Allow permission to be configured
|
||||
- Lock: Allow permission to be configured
|
||||
- Add a third state to header icons (#500)
|
||||
- Allow faking of PATCH/DELETE methods (#502)
|
||||
- More reliable form validation and error handling
|
||||
|
||||
### Changed
|
||||
- Rename `notification_read_time` column in discussions table to `notifications_read_time`.
|
||||
- Update to FontAwesome 4.4.0.
|
||||
|
||||
### Fixed
|
||||
- Output forum description in meta description tag (#506)
|
||||
- Allow users to edit their last post in a discussion even if it's hidden
|
||||
- Allow users to rename their discussion even if their first post is hidden
|
||||
- API links correctly include the `/api` path (#579)
|
||||
- Tags: Fix sub-tag ordering algorithm in Chrome (#325)
|
||||
- Fix several design bugs
|
||||
|
||||
## [0.1.0-beta.2] - 2015-09-15
|
||||
### Added
|
||||
- Check prerequisites (PHP version, extensions, etc.) before installation (#364)
|
||||
- Enforce maximum title and post length through validation (#53, #338)
|
||||
- Ctrl+Enter submits posts (#276)
|
||||
- Syntax highlighting for code blocks (#248)
|
||||
- All links open in new window, receive rel=nofollow attribute (#247)
|
||||
- Default build script for extensions (#438)
|
||||
- Input validation in installer
|
||||
|
||||
### Changed
|
||||
- Ask for admin password confirmation in installer (#405)
|
||||
- Increased some text contrasts for accessibility (#390)
|
||||
|
||||
### Fixed
|
||||
- Discussion list did not work with non-empty database prefix (#269, #380)
|
||||
- Non-admins could not reset their password (#229)
|
||||
- Requests ending with a slash resulted in a 404 (#334)
|
||||
- In rare cases, posts did not load correctly (#295)
|
||||
- Avatars did not show up when installed in a subfolder (#291)
|
||||
- Installer crashed when views directory was not writable (#376)
|
||||
- Table prefix could not be set in web installer (#269)
|
||||
- Enabling an extension disabled all other extensions (#402)
|
||||
- Invalid custom CSS could crash the application (#400)
|
||||
- First posts could not be restored or deleted
|
||||
- Several design bugs
|
||||
- Set cookies to be HTTP-only
|
||||
- Tags: Sometimes, tags could not be dragged for reordering in the admin panel (#341)
|
||||
- Suspend: Use correct column name in when migrating database
|
||||
- Lock: Check for correct permission when displaying lock control
|
||||
- Likes: Allow liking permissions to be configured
|
||||
|
||||
## 0.1.0-beta - 2015-08-27
|
||||
First Version
|
||||
|
||||
[0.1.0-beta.4]: https://github.com/flarum/core/compare/v0.1.0-beta.3...v0.1.0-beta.4
|
||||
[0.1.0-beta.3]: https://github.com/flarum/core/compare/v0.1.0-beta.2...v0.1.0-beta.3
|
||||
[0.1.0-beta.2]: https://github.com/flarum/core/compare/v0.1.0-beta...v0.1.0-beta.2
|
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2015 Toby Zerner
|
||||
Copyright (c) 2014-2016 Toby Zerner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@@ -21,12 +21,15 @@
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.5.9",
|
||||
"dflydev/fig-cookies": "^1.0",
|
||||
"doctrine/dbal": "^2.5",
|
||||
"franzl/whoops-middleware": "^0.2.0",
|
||||
"illuminate/bus": "5.1.*",
|
||||
"illuminate/cache": "5.1.*",
|
||||
"illuminate/config": "5.1.*",
|
||||
"illuminate/container": "5.1.*",
|
||||
"illuminate/contracts": "5.1.*",
|
||||
"illuminate/database": "5.1.*",
|
||||
"illuminate/database": "^5.1.31",
|
||||
"illuminate/events": "5.1.*",
|
||||
"illuminate/filesystem": "5.1.*",
|
||||
"illuminate/hashing": "5.1.*",
|
||||
@@ -34,27 +37,25 @@
|
||||
"illuminate/support": "5.1.*",
|
||||
"illuminate/validation": "5.1.*",
|
||||
"illuminate/view": "5.1.*",
|
||||
"league/flysystem": "^1.0.11",
|
||||
"tobscure/json-api": "^0.2.0",
|
||||
"oyejorge/less.php": "~1.5",
|
||||
"intervention/image": "^2.3.0",
|
||||
"s9e/text-formatter": "^0.4.0",
|
||||
"psr/http-message": "^1.0",
|
||||
"zendframework/zend-diactoros": "^1.1",
|
||||
"zendframework/zend-stratigility": "^1.1",
|
||||
"nikic/fast-route": "^0.6",
|
||||
"dflydev/fig-cookies": "^1.0",
|
||||
"symfony/console": "^2.7",
|
||||
"symfony/yaml": "^2.7",
|
||||
"symfony/translation": "^2.7",
|
||||
"doctrine/dbal": "^2.5",
|
||||
"league/flysystem": "^1.0.11",
|
||||
"league/oauth2-client": "~1.0",
|
||||
"matthiasmullie/minify": "^1.3",
|
||||
"monolog/monolog": "^1.16.0",
|
||||
"franzl/whoops-middleware": "^0.2.0",
|
||||
"matthiasmullie/minify": "^1.3"
|
||||
"nikic/fast-route": "^0.6",
|
||||
"oyejorge/less.php": "~1.5",
|
||||
"psr/http-message": "^1.0",
|
||||
"symfony/console": "^2.7",
|
||||
"symfony/http-foundation": "^2.7",
|
||||
"symfony/translation": "^2.7",
|
||||
"symfony/yaml": "^2.7",
|
||||
"s9e/text-formatter": "^0.4.12",
|
||||
"tobscure/json-api": "^0.2.0",
|
||||
"zendframework/zend-diactoros": "^1.1",
|
||||
"zendframework/zend-stratigility": "^1.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^0.9.4",
|
||||
"squizlabs/php_codesniffer": "2.*",
|
||||
"phpunit/phpunit": "^4.8"
|
||||
},
|
||||
"autoload": {
|
||||
@@ -71,7 +72,11 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vendor/bin/phpunit -c tests/phpunit.xml",
|
||||
"style": "vendor/bin/phpcs --standard=PSR2 -np src"
|
||||
"test": "vendor/bin/phpunit -c tests/phpunit.xml"
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "0.1.x-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
13
error/403.html
Normal file
13
error/403.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>403 Forbidden</h1>
|
||||
<p>You do not have permissions to access this page.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1,12 +1,10 @@
|
||||
var gulp = require('flarum-gulp');
|
||||
|
||||
var nodeDir = 'node_modules';
|
||||
var bowerDir = '../bower_components';
|
||||
|
||||
gulp({
|
||||
includeHelpers: true,
|
||||
files: [
|
||||
nodeDir + '/babel-core/external-helpers.js',
|
||||
|
||||
bowerDir + '/es6-micro-loader/dist/system-polyfill.js',
|
||||
|
||||
bowerDir + '/mithril/mithril.js',
|
||||
|
6479
js/admin/dist/app.js
vendored
6479
js/admin/dist/app.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"gulp": "^3.8.11",
|
||||
"flarum-gulp": "git+https://github.com/flarum/gulp.git",
|
||||
"babel-core": "^5.0.0"
|
||||
"gulp": "^3.9.1",
|
||||
"flarum-gulp": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
@@ -21,9 +21,9 @@ export default class AddExtensionModal extends Modal {
|
||||
content() {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<p>One day in the not-too-distant future, this dialog will allow you to add an extension to your forum with ease. We're building an ecosystem as we speak!</p>
|
||||
<p>In the meantime, if you manage to get your hands on a new extension, simply drop it in your forum's <code>extensions</code> directory.</p>
|
||||
<p>If you're a developer, you can <a href="http://flarum.org/docs/extend">read the docs</a> and have a go at building your own.</p>
|
||||
<p>{app.translator.trans('core.admin.add_extension.temporary_text')}</p>
|
||||
<p>{app.translator.trans('core.admin.add_extension.install_text', {a: <a href="https://discuss.flarum.org/t/extensions" target="_blank"/>})}</p>
|
||||
<p>{app.translator.trans('core.admin.add_extension.developer_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -1,11 +1,13 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Page from 'flarum/components/Page';
|
||||
import Button from 'flarum/components/Button';
|
||||
import Switch from 'flarum/components/Switch';
|
||||
import EditCustomCssModal from 'flarum/components/EditCustomCssModal';
|
||||
import saveSettings from 'flarum/utils/saveSettings';
|
||||
|
||||
export default class AppearancePage extends Component {
|
||||
export default class AppearancePage extends Page {
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.primaryColor = m.prop(app.settings.theme_primary_color);
|
||||
this.secondaryColor = m.prop(app.settings.theme_secondary_color);
|
||||
this.darkMode = m.prop(app.settings.theme_dark_mode === '1');
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Page from 'flarum/components/Page';
|
||||
import FieldSet from 'flarum/components/FieldSet';
|
||||
import Select from 'flarum/components/Select';
|
||||
import Button from 'flarum/components/Button';
|
||||
@@ -6,8 +6,10 @@ import Alert from 'flarum/components/Alert';
|
||||
import saveSettings from 'flarum/utils/saveSettings';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
|
||||
export default class BasicsPage extends Component {
|
||||
export default class BasicsPage extends Page {
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.loading = false;
|
||||
|
||||
this.fields = [
|
||||
@@ -145,7 +147,8 @@ export default class BasicsPage extends Component {
|
||||
.then(() => {
|
||||
app.alerts.show(this.successAlert = new Alert({type: 'success', children: app.translator.trans('core.admin.basics.saved_message')}));
|
||||
})
|
||||
.finally(() => {
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Page from 'flarum/components/Page';
|
||||
|
||||
export default class DashboardPage extends Component {
|
||||
export default class DashboardPage extends Page {
|
||||
view() {
|
||||
return (
|
||||
<div className="DashboardPage">
|
||||
<div className="container">
|
||||
<h2>Welcome to Flarum Beta</h2>
|
||||
<h2>{app.translator.trans('core.admin.dashboard.welcome_text')}</h2>
|
||||
<p>{app.translator.trans('core.admin.dashboard.version_text', {version: <strong>{app.forum.attribute('version')}</strong>})}</p>
|
||||
<p>{app.translator.trans('core.admin.dashboard.beta_warning_text', {strong: <strong/>})}</p>
|
||||
<ul>
|
||||
|
@@ -12,13 +12,13 @@ export default class EditCustomCssModal extends Modal {
|
||||
}
|
||||
|
||||
title() {
|
||||
return 'Edit Custom CSS';
|
||||
return app.translator.trans('core.admin.edit_css.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<p>Customize your forum's appearance by adding your own LESS/CSS code to be applied on top of Flarum's default styles. <a href="http://flarum.org/docs/extend/themes/">Read the documentation</a> for more information.</p>
|
||||
<p>{app.translator.trans('core.admin.edit_css.customize_text', {a: <a href="https://github.com/flarum/core/tree/master/less" target="_blank"/>})}</p>
|
||||
|
||||
<div className="Form">
|
||||
<div className="Form-group">
|
||||
@@ -29,7 +29,7 @@ export default class EditCustomCssModal extends Modal {
|
||||
{Button.component({
|
||||
className: 'Button Button--primary',
|
||||
type: 'submit',
|
||||
children: 'Save Changes',
|
||||
children: app.translator.trans('core.admin.edit_css.submit_button'),
|
||||
loading: this.loading
|
||||
})}
|
||||
</div>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Page from 'flarum/components/Page';
|
||||
import LinkButton from 'flarum/components/LinkButton';
|
||||
import Button from 'flarum/components/Button';
|
||||
import Dropdown from 'flarum/components/Dropdown';
|
||||
@@ -9,10 +9,8 @@ import ItemList from 'flarum/utils/ItemList';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
|
||||
export default class ExtensionsPage extends Component {
|
||||
export default class ExtensionsPage extends Page {
|
||||
view() {
|
||||
const extensions = Object.keys(app.extensions).map(id => app.extensions[id]);
|
||||
|
||||
return (
|
||||
<div className="ExtensionsPage">
|
||||
<div className="ExtensionsPage-header">
|
||||
@@ -29,15 +27,15 @@ export default class ExtensionsPage extends Component {
|
||||
<div className="ExtensionsPage-list">
|
||||
<div className="container">
|
||||
<ul className="ExtensionList">
|
||||
{extensions
|
||||
.sort((a, b) => a.extra['flarum-extension'].title.localeCompare(b.extra['flarum-extension'].title))
|
||||
.map(extension => {
|
||||
{Object.keys(app.extensions)
|
||||
.map(id => {
|
||||
const extension = app.extensions[id];
|
||||
const controls = this.controlItems(extension.id).toArray();
|
||||
|
||||
return <li className={'ExtensionListItem ' + (!this.isEnabled(extension.id) ? 'disabled' : '')}>
|
||||
<div className="ExtensionListItem-content">
|
||||
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.extra['flarum-extension'].icon}>
|
||||
{extension.extra['flarum-extension'].icon ? icon(extension.extra['flarum-extension'].icon.name) : ''}
|
||||
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
|
||||
{extension.icon ? icon(extension.icon.name) : ''}
|
||||
</span>
|
||||
{controls.length ? (
|
||||
<Dropdown
|
||||
|
32
js/admin/src/components/Page.js
Normal file
32
js/admin/src/components/Page.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import Component from 'flarum/Component';
|
||||
|
||||
/**
|
||||
* The `Page` component
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class Page extends Component {
|
||||
init() {
|
||||
app.previous = app.current;
|
||||
app.current = this;
|
||||
|
||||
app.modal.close();
|
||||
|
||||
/**
|
||||
* A class name to apply to the body while the route is active.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.bodyClass = '';
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
if (isInitialized) return;
|
||||
|
||||
if (this.bodyClass) {
|
||||
$('#app').addClass(this.bodyClass);
|
||||
|
||||
context.onunload = () => $('#app').removeClass(this.bodyClass);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,11 +1,11 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Page from 'flarum/components/Page';
|
||||
import GroupBadge from 'flarum/components/GroupBadge';
|
||||
import EditGroupModal from 'flarum/components/EditGroupModal';
|
||||
import Group from 'flarum/models/Group';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import PermissionGrid from 'flarum/components/PermissionGrid';
|
||||
|
||||
export default class PermissionsPage extends Component {
|
||||
export default class PermissionsPage extends Page {
|
||||
view() {
|
||||
return (
|
||||
<div className="PermissionsPage">
|
||||
|
@@ -33,7 +33,7 @@ export default class SettingsModal extends Modal {
|
||||
className="Button Button--primary"
|
||||
loading={this.loading}
|
||||
disabled={!this.changed()}>
|
||||
Save Changes
|
||||
{app.translator.trans('core.admin.settings.submit_button')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@@ -7,10 +7,10 @@
|
||||
"spin.js": "~2.0.1",
|
||||
"moment": "~2.8.4",
|
||||
"color-thief": "v2.0",
|
||||
"mithril": "lhorie/mithril.js#next",
|
||||
"mithril": "lhorie/mithril.js#v0.2.3",
|
||||
"es6-micro-loader": "caridy/es6-micro-loader#v0.2.1",
|
||||
"fastclick": "~1.0.6",
|
||||
"autolink": "*",
|
||||
"autolink": "~1.0.0",
|
||||
"m.attrs.bidi": "tobscure/m.attrs.bidi"
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,10 @@
|
||||
var gulp = require('flarum-gulp');
|
||||
|
||||
var nodeDir = 'node_modules';
|
||||
var bowerDir = '../bower_components';
|
||||
|
||||
gulp({
|
||||
includeHelpers: true,
|
||||
files: [
|
||||
nodeDir + '/babel-core/external-helpers.js',
|
||||
|
||||
bowerDir + '/es6-micro-loader/dist/system-polyfill.js',
|
||||
|
||||
bowerDir + '/mithril/mithril.js',
|
||||
|
10194
js/forum/dist/app.js
vendored
10194
js/forum/dist/app.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"gulp": "^3.8.11",
|
||||
"flarum-gulp": "git+https://github.com/flarum/gulp.git",
|
||||
"babel-core": "^5.0.0"
|
||||
"gulp": "^3.9.1",
|
||||
"flarum-gulp": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import routes from 'flarum/initializers/routes';
|
||||
import components from 'flarum/initializers/components';
|
||||
import humanTime from 'flarum/initializers/humanTime';
|
||||
import boot from 'flarum/initializers/boot';
|
||||
import alertEmailConfirmation from 'flarum/initializers/alertEmailConfirmation';
|
||||
|
||||
const app = new ForumApp();
|
||||
|
||||
@@ -15,5 +16,6 @@ app.initializers.add('humanTime', humanTime);
|
||||
|
||||
app.initializers.add('preload', preload, -100);
|
||||
app.initializers.add('boot', boot, -100);
|
||||
app.initializers.add('alertEmailConfirmation', alertEmailConfirmation, -100);
|
||||
|
||||
export default app;
|
||||
|
@@ -22,6 +22,13 @@ export default class ChangeEmailModal extends Modal {
|
||||
* @type {function}
|
||||
*/
|
||||
this.email = m.prop(app.session.user.email());
|
||||
|
||||
/**
|
||||
* The value of the password input.
|
||||
*
|
||||
* @type {function}
|
||||
*/
|
||||
this.password = m.prop('');
|
||||
}
|
||||
|
||||
className() {
|
||||
@@ -54,8 +61,13 @@ export default class ChangeEmailModal extends Modal {
|
||||
<div className="Form-group">
|
||||
<input type="email" name="email" className="FormControl"
|
||||
placeholder={app.session.user.email()}
|
||||
value={this.email()}
|
||||
onchange={m.withAttr('value', this.email)}
|
||||
bidi={this.email}
|
||||
disabled={this.loading}/>
|
||||
</div>
|
||||
<div className="Form-group">
|
||||
<input type="password" name="password" className="FormControl"
|
||||
placeholder={app.translator.trans('core.forum.change_email.confirm_password_label')}
|
||||
bidi={this.password}
|
||||
disabled={this.loading}/>
|
||||
</div>
|
||||
<div className="Form-group">
|
||||
@@ -81,10 +93,24 @@ export default class ChangeEmailModal extends Modal {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldEmail = app.session.user.email();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
app.session.user.save({email: this.email()}, {errorHandler: this.onerror.bind(this)})
|
||||
app.session.user.save({email: this.email()}, {
|
||||
errorHandler: this.onerror.bind(this),
|
||||
meta: {password: this.password()}
|
||||
})
|
||||
.then(() => this.success = true)
|
||||
.finally(this.loaded.bind(this));
|
||||
.catch(() => {})
|
||||
.then(this.loaded.bind(this));
|
||||
}
|
||||
|
||||
onerror(error) {
|
||||
if (error.status === 401) {
|
||||
error.alert.props.children = app.translator.trans('core.forum.change_email.incorrect_password_message');
|
||||
}
|
||||
|
||||
super.onerror(error);
|
||||
}
|
||||
}
|
||||
|
@@ -71,8 +71,7 @@ export default class CommentPost extends Post {
|
||||
|
||||
isEditing() {
|
||||
return app.composer.component instanceof EditPostComposer &&
|
||||
app.composer.component.props.post === this.props.post &&
|
||||
app.composer.position !== Composer.PositionEnum.MINIMIZED;
|
||||
app.composer.component.props.post === this.props.post;
|
||||
}
|
||||
|
||||
attrs() {
|
||||
|
@@ -19,13 +19,6 @@ class Composer extends Component {
|
||||
*/
|
||||
this.position = Composer.PositionEnum.HIDDEN;
|
||||
|
||||
/**
|
||||
* The composer's previous position.
|
||||
*
|
||||
* @type {Composer.PositionEnum}
|
||||
*/
|
||||
this.oldPosition = null;
|
||||
|
||||
/**
|
||||
* The composer's intended height, which can be modified by the user
|
||||
* (by dragging the composer handle).
|
||||
@@ -66,21 +59,19 @@ class Composer extends Component {
|
||||
|
||||
view() {
|
||||
const classes = {
|
||||
'normal': this.position === Composer.PositionEnum.NORMAL,
|
||||
'minimized': this.position === Composer.PositionEnum.MINIMIZED,
|
||||
'fullScreen': this.position === Composer.PositionEnum.FULLSCREEN,
|
||||
'active': this.active
|
||||
};
|
||||
classes.visible = this.position === Composer.PositionEnum.NORMAL || classes.minimized || classes.fullScreen;
|
||||
classes.visible = classes.normal || classes.minimized || classes.fullScreen;
|
||||
|
||||
// If the composer is minimized, tell the composer's content component that
|
||||
// it shouldn't let the user interact with it. Set up a handler so that if
|
||||
// the content IS clicked, the composer will be shown.
|
||||
if (this.component) this.component.props.disabled = classes.minimized;
|
||||
|
||||
const showIfMinimized = () => {
|
||||
if (this.position === Composer.PositionEnum.MINIMIZED) this.show();
|
||||
m.redraw.strategy('none');
|
||||
};
|
||||
const showIfMinimized = this.position === Composer.PositionEnum.MINIMIZED ? this.show.bind(this) : undefined;
|
||||
|
||||
return (
|
||||
<div className={'Composer ' + classList(classes)}>
|
||||
@@ -100,8 +91,6 @@ class Composer extends Component {
|
||||
defaultHeight = this.$().height();
|
||||
}
|
||||
|
||||
this.updateHeight();
|
||||
|
||||
if (isInitialized) return;
|
||||
|
||||
// Since this component is a part of the global UI that persists between
|
||||
@@ -213,19 +202,17 @@ class Composer extends Component {
|
||||
* of any flexible elements inside the composer's body.
|
||||
*/
|
||||
updateHeight() {
|
||||
// TODO: update this in a way that is independent of the TextEditor being
|
||||
// present.
|
||||
const height = this.computedHeight();
|
||||
const $flexible = this.$('.TextEditor-flexible');
|
||||
const $flexible = this.$('.Composer-flexible');
|
||||
|
||||
this.$().height(height);
|
||||
|
||||
if ($flexible.length) {
|
||||
const headerHeight = $flexible.offset().top - this.$().offset().top;
|
||||
const paddingBottom = parseInt($flexible.css('padding-bottom'), 10);
|
||||
const footerHeight = this.$('.TextEditor-controls').outerHeight(true);
|
||||
const footerHeight = this.$('.Composer-footer').outerHeight(true);
|
||||
|
||||
$flexible.height(height - headerHeight - paddingBottom - footerHeight);
|
||||
$flexible.height(this.$().outerHeight() - headerHeight - paddingBottom - footerHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,99 +223,27 @@ class Composer extends Component {
|
||||
*/
|
||||
updateBodyPadding() {
|
||||
const visible = this.position !== Composer.PositionEnum.HIDDEN &&
|
||||
this.position !== Composer.PositionEnum.MINIMIZED;
|
||||
this.position !== Composer.PositionEnum.MINIMIZED &&
|
||||
this.$().css('position') !== 'absolute';
|
||||
|
||||
const paddingBottom = visible
|
||||
? this.computedHeight() - parseInt($('#app').css('padding-bottom'), 10)
|
||||
: 0;
|
||||
|
||||
$('#content').css({paddingBottom});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update (and animate) the DOM to reflect the composer's current state.
|
||||
* Determine whether or not the Composer is covering the screen.
|
||||
*
|
||||
* This will be true if the Composer is in full-screen mode on desktop, or
|
||||
* if the Composer is positioned absolutely as on mobile devices.
|
||||
*
|
||||
* @return {Boolean}
|
||||
* @public
|
||||
*/
|
||||
update() {
|
||||
// Before we redraw the composer to its new state, we need to save the
|
||||
// current height of the composer, as well as the page's scroll position, so
|
||||
// that we can smoothly transition from the old to the new state.
|
||||
const $composer = this.$().stop(true);
|
||||
const oldHeight = $composer.is(':visible') ? $composer.outerHeight() : 0;
|
||||
const scrollTop = $(window).scrollTop();
|
||||
|
||||
m.redraw(true);
|
||||
|
||||
// Now that we've redrawn and the composer's DOM has been updated, we want
|
||||
// to update the composer's height. Once we've done that, we'll capture the
|
||||
// real value to use as the end point for our animation later on.
|
||||
$composer.show();
|
||||
this.updateHeight();
|
||||
|
||||
const newHeight = $composer.outerHeight();
|
||||
|
||||
switch (this.position) {
|
||||
case Composer.PositionEnum.NORMAL:
|
||||
// If the composer is being opened, we will make it visible and animate
|
||||
// it growing/sliding up from the bottom of the viewport. Or if the user
|
||||
// has just exited fullscreen mode, we will simply tell the content to
|
||||
// take focus.
|
||||
if (this.oldPosition !== Composer.PositionEnum.FULLSCREEN) {
|
||||
$composer.show()
|
||||
.css({height: oldHeight})
|
||||
.animate({bottom: 0, height: newHeight}, 'fast', this.component.focus.bind(this.component));
|
||||
|
||||
if ($composer.css('position') === 'absolute') {
|
||||
$composer.css('top', $(window).scrollTop());
|
||||
|
||||
this.$backdrop = $('<div/>')
|
||||
.addClass('composer-backdrop')
|
||||
.appendTo('body');
|
||||
}
|
||||
} else {
|
||||
this.component.focus();
|
||||
}
|
||||
break;
|
||||
|
||||
case Composer.PositionEnum.MINIMIZED:
|
||||
// If the composer has been minimized, we will animate it shrinking down
|
||||
// to its new smaller size.
|
||||
$composer.css({top: 'auto', height: oldHeight})
|
||||
.animate({height: newHeight}, 'fast');
|
||||
|
||||
if (this.$backdrop) this.$backdrop.remove();
|
||||
break;
|
||||
|
||||
case Composer.PositionEnum.HIDDEN:
|
||||
// If the composer has been hidden, then we will animate it sliding down
|
||||
// beyond the edge of the viewport. Once the animation is complete, we
|
||||
// un-draw the composer's component.
|
||||
$composer.css({top: 'auto', height: oldHeight})
|
||||
.animate({bottom: -newHeight}, 'fast', () => {
|
||||
$composer.hide();
|
||||
this.clear();
|
||||
m.redraw();
|
||||
});
|
||||
|
||||
if (this.$backdrop) this.$backdrop.remove();
|
||||
break;
|
||||
|
||||
case Composer.PositionEnum.FULLSCREEN:
|
||||
this.component.focus();
|
||||
break;
|
||||
|
||||
default:
|
||||
// no default
|
||||
}
|
||||
|
||||
// Provided the composer isn't in fullscreen mode, we'll want to update the
|
||||
// body's padding to make sure all of the page's content can still be seen.
|
||||
// Plus, we'll scroll back to where we were before the composer was opened,
|
||||
// as its opening may have changed the content of the page.
|
||||
if (this.position !== Composer.PositionEnum.FULLSCREEN) {
|
||||
this.updateBodyPadding();
|
||||
$('html, body').scrollTop(scrollTop);
|
||||
}
|
||||
|
||||
this.oldPosition = this.position;
|
||||
isFullScreen() {
|
||||
return this.position === Composer.PositionEnum.FULLSCREEN || this.$().css('position') === 'absolute';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,20 +293,76 @@ class Composer extends Component {
|
||||
this.component = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate the Composer into the given position.
|
||||
*
|
||||
* @param {Composer.PositionEnum} position
|
||||
*/
|
||||
animateToPosition(position) {
|
||||
// Before we redraw the composer to its new state, we need to save the
|
||||
// current height of the composer, as well as the page's scroll position, so
|
||||
// that we can smoothly transition from the old to the new state.
|
||||
const oldPosition = this.position;
|
||||
const $composer = this.$().stop(true);
|
||||
const oldHeight = $composer.outerHeight();
|
||||
const scrollTop = $(window).scrollTop();
|
||||
|
||||
this.position = position;
|
||||
|
||||
m.redraw(true);
|
||||
|
||||
// Now that we've redrawn and the composer's DOM has been updated, we want
|
||||
// to update the composer's height. Once we've done that, we'll capture the
|
||||
// real value to use as the end point for our animation later on.
|
||||
$composer.show();
|
||||
this.updateHeight();
|
||||
|
||||
const newHeight = $composer.outerHeight();
|
||||
|
||||
if (oldPosition === Composer.PositionEnum.HIDDEN) {
|
||||
$composer.css({bottom: -newHeight, height: newHeight});
|
||||
} else {
|
||||
$composer.css({height: oldHeight});
|
||||
}
|
||||
|
||||
$composer.animate({bottom: 0, height: newHeight}, 'fast', () => this.component.focus());
|
||||
|
||||
this.updateBodyPadding();
|
||||
$(window).scrollTop(scrollTop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Composer backdrop.
|
||||
*/
|
||||
showBackdrop() {
|
||||
this.$backdrop = $('<div/>')
|
||||
.addClass('composer-backdrop')
|
||||
.appendTo('body');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the Composer backdrop.
|
||||
*/
|
||||
hideBackdrop() {
|
||||
if (this.$backdrop) this.$backdrop.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the composer.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
show() {
|
||||
// If the composer is hidden or minimized, we'll need to update its
|
||||
// position. Otherwise, if the composer is already showing (whether it's
|
||||
// fullscreen or not), we can leave it as is.
|
||||
if ([Composer.PositionEnum.MINIMIZED, Composer.PositionEnum.HIDDEN].indexOf(this.position) !== -1) {
|
||||
this.position = Composer.PositionEnum.NORMAL;
|
||||
if (this.position === Composer.PositionEnum.NORMAL || this.position === Composer.PositionEnum.FULLSCREEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.update();
|
||||
this.animateToPosition(Composer.PositionEnum.NORMAL);
|
||||
|
||||
if (this.isFullScreen()) {
|
||||
this.$().css('top', $(window).scrollTop());
|
||||
this.showBackdrop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -400,8 +371,20 @@ class Composer extends Component {
|
||||
* @public
|
||||
*/
|
||||
hide() {
|
||||
this.position = Composer.PositionEnum.HIDDEN;
|
||||
this.update();
|
||||
const $composer = this.$();
|
||||
|
||||
// Animate the composer sliding down off the bottom edge of the viewport.
|
||||
// Only when the animation is completed, update the Composer state flag and
|
||||
// other elements on the page.
|
||||
$composer.stop(true).animate({bottom: -$composer.height()}, 'fast', () => {
|
||||
this.position = Composer.PositionEnum.HIDDEN;
|
||||
this.clear();
|
||||
m.redraw();
|
||||
|
||||
$composer.hide();
|
||||
this.hideBackdrop();
|
||||
this.updateBodyPadding();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -422,10 +405,12 @@ class Composer extends Component {
|
||||
* @public
|
||||
*/
|
||||
minimize() {
|
||||
if (this.position !== Composer.PositionEnum.HIDDEN) {
|
||||
this.position = Composer.PositionEnum.MINIMIZED;
|
||||
this.update();
|
||||
}
|
||||
if (this.position === Composer.PositionEnum.HIDDEN) return;
|
||||
|
||||
this.animateToPosition(Composer.PositionEnum.MINIMIZED);
|
||||
|
||||
this.$().css('top', 'auto');
|
||||
this.hideBackdrop();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -437,7 +422,9 @@ class Composer extends Component {
|
||||
fullScreen() {
|
||||
if (this.position !== Composer.PositionEnum.HIDDEN) {
|
||||
this.position = Composer.PositionEnum.FULLSCREEN;
|
||||
this.update();
|
||||
m.redraw();
|
||||
this.updateHeight();
|
||||
this.component.focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,7 +436,9 @@ class Composer extends Component {
|
||||
exitFullScreen() {
|
||||
if (this.position === Composer.PositionEnum.FULLSCREEN) {
|
||||
this.position = Composer.PositionEnum.NORMAL;
|
||||
this.update();
|
||||
m.redraw();
|
||||
this.updateHeight();
|
||||
this.component.focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -56,7 +56,7 @@ export default class ComposerBody extends Component {
|
||||
this.editor.props.disabled = this.loading;
|
||||
|
||||
return (
|
||||
<div className="ComposerBody">
|
||||
<div className={'ComposerBody ' + (this.props.className || '')}>
|
||||
{avatar(this.props.user, {className: 'ComposerBody-avatar'})}
|
||||
<div className="ComposerBody-content">
|
||||
<ul className="ComposerBody-header">{listItems(this.headerItems().toArray())}</ul>
|
||||
|
@@ -31,12 +31,15 @@ export default class DiscussionComposer extends ComposerBody {
|
||||
props.submitLabel = props.submitLabel || app.translator.trans('core.forum.composer_discussion.submit_button');
|
||||
props.confirmExit = props.confirmExit || extractText(app.translator.trans('core.forum.composer_discussion.discard_confirmation'));
|
||||
props.titlePlaceholder = props.titlePlaceholder || extractText(app.translator.trans('core.forum.composer_discussion.title_placeholder'));
|
||||
props.className = 'ComposerBody--discussion';
|
||||
}
|
||||
|
||||
headerItems() {
|
||||
const items = super.headerItems();
|
||||
|
||||
items.add('title', (
|
||||
items.add('title', <h3>{app.translator.trans('core.forum.composer_discussion.title')}</h3>, 100);
|
||||
|
||||
items.add('discussionTitle', (
|
||||
<h3>
|
||||
<input className="FormControl"
|
||||
value={this.title()}
|
||||
|
@@ -115,7 +115,7 @@ export default class DiscussionList extends Component {
|
||||
map.latest = '-lastTime';
|
||||
map.top = '-commentsCount';
|
||||
map.newest = '-startTime';
|
||||
map.oldest = '+startTime';
|
||||
map.oldest = 'startTime';
|
||||
|
||||
return map;
|
||||
}
|
||||
|
@@ -184,7 +184,7 @@ export default class DiscussionPage extends Page {
|
||||
// the specific post that was routed to.
|
||||
this.stream = new PostStream({discussion, includedPosts});
|
||||
this.stream.on('positionChanged', this.positionChanged.bind(this));
|
||||
this.stream.goToNumber(m.route.param('near') || includedPosts[0].number(), true);
|
||||
this.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,6 +1,13 @@
|
||||
import ComposerBody from 'flarum/components/ComposerBody';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
function minimizeComposerIfFullScreen(e) {
|
||||
if (app.composer.isFullScreen()) {
|
||||
app.composer.minimize();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `EditPostComposer` component displays the composer content for editing a
|
||||
* post. It sets the initial content to the content of the post that is being
|
||||
@@ -15,7 +22,9 @@ export default class EditPostComposer extends ComposerBody {
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.editor.props.preview = () => {
|
||||
this.editor.props.preview = e => {
|
||||
minimizeComposerIfFullScreen(e);
|
||||
|
||||
m.route(app.route.post(this.props.post));
|
||||
};
|
||||
}
|
||||
@@ -35,10 +44,16 @@ export default class EditPostComposer extends ComposerBody {
|
||||
const items = super.headerItems();
|
||||
const post = this.props.post;
|
||||
|
||||
const routeAndMinimize = function(element, isInitialized) {
|
||||
if (isInitialized) return;
|
||||
$(element).on('click', minimizeComposerIfFullScreen);
|
||||
m.route.apply(this, arguments);
|
||||
};
|
||||
|
||||
items.add('title', (
|
||||
<h3>
|
||||
{icon('pencil')} {' '}
|
||||
<a href={app.route.discussion(post.discussion(), post.number())} config={m.route}>
|
||||
<a href={app.route.discussion(post.discussion(), post.number())} config={routeAndMinimize}>
|
||||
{app.translator.trans('core.forum.composer_edit.post_link', {number: post.number(), discussion: post.discussion().title()})}
|
||||
</a>
|
||||
</h3>
|
||||
|
@@ -85,12 +85,22 @@ export default class ForgotPasswordModal extends Modal {
|
||||
app.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/forgot',
|
||||
data: {email: this.email()}
|
||||
data: {email: this.email()},
|
||||
errorHandler: this.onerror.bind(this)
|
||||
})
|
||||
.then(() => {
|
||||
this.success = true;
|
||||
this.alert = null;
|
||||
})
|
||||
.finally(this.loaded.bind(this));
|
||||
.catch(() => {})
|
||||
.then(this.loaded.bind(this));
|
||||
}
|
||||
|
||||
onerror(error) {
|
||||
if (error.status === 404) {
|
||||
error.alert.props.children = app.translator.trans('core.forum.forgot_password.not_found_message');
|
||||
}
|
||||
|
||||
super.onerror(error);
|
||||
}
|
||||
}
|
||||
|
@@ -98,14 +98,19 @@ export default class IndexPage extends Page {
|
||||
|
||||
// Work out the difference between the height of this hero and that of the
|
||||
// previous hero. Maintain the same scroll position relative to the bottom
|
||||
// of the hero so that the 'fixed' sidebar doesn't jump around.
|
||||
const heroHeight = this.$('.Hero').outerHeight();
|
||||
// of the hero so that the sidebar doesn't jump around.
|
||||
const oldHeroHeight = app.cache.heroHeight;
|
||||
const heroHeight = app.cache.heroHeight = this.$('.Hero').outerHeight();
|
||||
const scrollTop = app.cache.scrollTop;
|
||||
|
||||
$('#app').css('min-height', $(window).height() + heroHeight);
|
||||
$(window).scrollTop(scrollTop - (app.cache.heroHeight - heroHeight));
|
||||
|
||||
app.cache.heroHeight = heroHeight;
|
||||
// Scroll to the remembered position. We do this after a short delay so that
|
||||
// it happens after the browser has done its own "back button" scrolling,
|
||||
// which isn't right. https://github.com/flarum/core/issues/835
|
||||
const scroll = () => $(window).scrollTop(scrollTop - oldHeroHeight + heroHeight);
|
||||
scroll();
|
||||
setTimeout(scroll, 1);
|
||||
|
||||
// If we've just returned from a discussion page, then the constructor will
|
||||
// have set the `lastDiscussion` property. If this is the case, we want to
|
||||
@@ -199,16 +204,17 @@ export default class IndexPage extends Page {
|
||||
*/
|
||||
viewItems() {
|
||||
const items = new ItemList();
|
||||
const sortMap = app.cache.discussionList.sortMap();
|
||||
|
||||
const sortOptions = {};
|
||||
for (const i in app.cache.discussionList.sortMap()) {
|
||||
for (const i in sortMap) {
|
||||
sortOptions[i] = app.translator.trans('core.forum.index_sort.' + i + '_button');
|
||||
}
|
||||
|
||||
items.add('sort',
|
||||
Select.component({
|
||||
options: sortOptions,
|
||||
value: this.params().sort,
|
||||
value: this.params().sort || Object.keys(sortMap)[0],
|
||||
onchange: this.changeSort.bind(this)
|
||||
})
|
||||
);
|
||||
@@ -358,6 +364,10 @@ export default class IndexPage extends Page {
|
||||
* @return void
|
||||
*/
|
||||
markAllAsRead() {
|
||||
app.session.user.save({readTime: new Date()});
|
||||
const confirmation = confirm(app.translator.trans('core.forum.index.mark_all_as_read_confirmation'));
|
||||
|
||||
if (confirmation) {
|
||||
app.session.user.save({readTime: new Date()});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -13,8 +13,8 @@ export default class LogInButton extends Button {
|
||||
props.className = (props.className || '') + ' LogInButton';
|
||||
|
||||
props.onclick = function() {
|
||||
const width = 1000;
|
||||
const height = 500;
|
||||
const width = 600;
|
||||
const height = 400;
|
||||
const $window = $(window);
|
||||
|
||||
window.open(app.forum.attribute('baseUrl') + props.path, 'logInPopup',
|
||||
|
@@ -48,16 +48,14 @@ export default class LogInModal extends Modal {
|
||||
|
||||
<div className="Form Form--centered">
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="email" placeholder={extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'))}
|
||||
value={this.email()}
|
||||
onchange={m.withAttr('value', this.email)}
|
||||
<input className="FormControl" name="email" type="text" placeholder={extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'))}
|
||||
bidi={this.email}
|
||||
disabled={this.loading} />
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="password" type="password" placeholder={extractText(app.translator.trans('core.forum.log_in.password_placeholder'))}
|
||||
value={this.password()}
|
||||
onchange={m.withAttr('value', this.password)}
|
||||
bidi={this.password}
|
||||
disabled={this.loading} />
|
||||
</div>
|
||||
|
||||
@@ -124,18 +122,15 @@ export default class LogInModal extends Modal {
|
||||
const email = this.email();
|
||||
const password = this.password();
|
||||
|
||||
app.session.login(email, password, {errorHandler: this.onerror.bind(this)})
|
||||
.catch(this.loaded.bind(this));
|
||||
app.session.login(email, password, {errorHandler: this.onerror.bind(this)}).then(
|
||||
() => window.location.reload(),
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
onerror(error) {
|
||||
if (error.status === 401) {
|
||||
if (error.response.emailConfirmationRequired) {
|
||||
error.alert.props.children = app.translator.trans('core.forum.log_in.confirmation_required_message', {email: error.response.emailConfirmationRequired});
|
||||
delete error.alert.props.type;
|
||||
} else {
|
||||
error.alert.props.children = app.translator.trans('core.forum.log_in.invalid_login_message');
|
||||
}
|
||||
error.alert.props.children = app.translator.trans('core.forum.log_in.invalid_login_message');
|
||||
}
|
||||
|
||||
super.onerror(error);
|
||||
|
@@ -120,7 +120,8 @@ export default class NotificationList extends Component {
|
||||
app.session.user.pushAttributes({newNotificationsCount: 0});
|
||||
app.cache.notifications = notifications.sort((a, b) => b.time() - a.time());
|
||||
})
|
||||
.finally(() => {
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
|
@@ -18,6 +18,8 @@ import ItemList from 'flarum/utils/ItemList';
|
||||
*/
|
||||
export default class Post extends Component {
|
||||
init() {
|
||||
this.loading = false;
|
||||
|
||||
/**
|
||||
* Set up a subtree retainer so that the post will not be redrawn
|
||||
* unless new data comes in.
|
||||
@@ -37,7 +39,7 @@ export default class Post extends Component {
|
||||
view() {
|
||||
const attrs = this.attrs();
|
||||
|
||||
attrs.className = 'Post ' + (attrs.className || '');
|
||||
attrs.className = 'Post ' + (this.loading ? 'Post--loading ' : '') + (attrs.className || '');
|
||||
|
||||
return (
|
||||
<article {...attrs}>
|
||||
|
@@ -5,6 +5,7 @@ import anchorScroll from 'flarum/utils/anchorScroll';
|
||||
import mixin from 'flarum/utils/mixin';
|
||||
import evented from 'flarum/utils/evented';
|
||||
import ReplyPlaceholder from 'flarum/components/ReplyPlaceholder';
|
||||
import Button from 'flarum/components/Button';
|
||||
|
||||
/**
|
||||
* The `PostStream` component displays an infinitely-scrollable wall of posts in
|
||||
@@ -54,7 +55,9 @@ class PostStream extends Component {
|
||||
return this.goToLast().then(() => {
|
||||
$('html,body').stop(true).animate({
|
||||
scrollTop: $(document).height() - $(window).height()
|
||||
}, 'fast');
|
||||
}, 'fast', () => {
|
||||
this.flashItem(this.$('.PostStream-item:last-child'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -194,60 +197,72 @@ class PostStream extends Component {
|
||||
this.visibleEnd = this.sanitizeIndex(this.visibleEnd);
|
||||
this.viewingEnd = this.visibleEnd === this.count();
|
||||
|
||||
const posts = this.posts();
|
||||
const postIds = this.discussion.postIds();
|
||||
|
||||
const items = posts.map((post, i) => {
|
||||
let content;
|
||||
const attrs = {'data-index': this.visibleStart + i};
|
||||
|
||||
if (post) {
|
||||
const time = post.time();
|
||||
const PostComponent = app.postComponents[post.contentType()];
|
||||
content = PostComponent ? PostComponent.component({post}) : '';
|
||||
|
||||
attrs.key = 'post' + post.id();
|
||||
attrs.config = fadeIn;
|
||||
attrs['data-time'] = time.toISOString();
|
||||
attrs['data-number'] = post.number();
|
||||
attrs['data-id'] = post.id();
|
||||
attrs['data-type'] = post.contentType();
|
||||
|
||||
// If the post before this one was more than 4 hours ago, we will
|
||||
// display a 'time gap' indicating how long it has been in between
|
||||
// the posts.
|
||||
const dt = time - lastTime;
|
||||
|
||||
if (dt > 1000 * 60 * 60 * 24 * 4) {
|
||||
content = [
|
||||
<div className="PostStream-timeGap">
|
||||
<span>{app.translator.trans('core.forum.post_stream.time_lapsed_text', {period: moment.duration(dt).humanize()})}</span>
|
||||
</div>,
|
||||
content
|
||||
];
|
||||
}
|
||||
|
||||
lastTime = time;
|
||||
} else {
|
||||
attrs.key = 'post' + postIds[this.visibleStart + i];
|
||||
|
||||
content = PostLoading.component();
|
||||
}
|
||||
|
||||
return <div className="PostStream-item" {...attrs}>{content}</div>;
|
||||
});
|
||||
|
||||
if (!this.viewingEnd && posts[this.visibleEnd - this.visibleStart - 1]) {
|
||||
items.push(
|
||||
<div className="PostStream-loadMore" key="loadMore">
|
||||
<Button className="Button" onclick={this.loadNext.bind(this)}>
|
||||
{app.translator.trans('core.forum.post_stream.load_more_button')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If we're viewing the end of the discussion, the user can reply, and
|
||||
// is not already doing so, then show a 'write a reply' placeholder.
|
||||
if (this.viewingEnd && (!app.session.user || this.discussion.canReply())) {
|
||||
items.push(
|
||||
<div className="PostStream-item" key="reply">
|
||||
{ReplyPlaceholder.component({discussion: this.discussion})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="PostStream">
|
||||
{this.posts().map((post, i) => {
|
||||
let content;
|
||||
const attrs = {'data-index': this.visibleStart + i};
|
||||
|
||||
if (post) {
|
||||
const time = post.time();
|
||||
const PostComponent = app.postComponents[post.contentType()];
|
||||
content = PostComponent ? PostComponent.component({post}) : '';
|
||||
|
||||
attrs.key = 'post' + post.id();
|
||||
attrs.config = fadeIn;
|
||||
attrs['data-time'] = time.toISOString();
|
||||
attrs['data-number'] = post.number();
|
||||
attrs['data-id'] = post.id();
|
||||
|
||||
// If the post before this one was more than 4 hours ago, we will
|
||||
// display a 'time gap' indicating how long it has been in between
|
||||
// the posts.
|
||||
const dt = time - lastTime;
|
||||
|
||||
if (dt > 1000 * 60 * 60 * 24 * 4) {
|
||||
content = [
|
||||
<div className="PostStream-timeGap">
|
||||
<span>{app.translator.trans('core.forum.post_stream.time_lapsed_text', {period: moment.duration(dt).humanize()})}</span>
|
||||
</div>,
|
||||
content
|
||||
];
|
||||
}
|
||||
|
||||
lastTime = time;
|
||||
} else {
|
||||
attrs.key = 'post' + postIds[this.visibleStart + i];
|
||||
|
||||
content = PostLoading.component();
|
||||
}
|
||||
|
||||
return <div className="PostStream-item" {...attrs}>{content}</div>;
|
||||
})}
|
||||
|
||||
{
|
||||
// If we're viewing the end of the discussion, the user can reply, and
|
||||
// is not already doing so, then show a 'write a reply' placeholder.
|
||||
this.viewingEnd &&
|
||||
(!app.session.user || this.discussion.canReply())
|
||||
? (
|
||||
<div className="PostStream-item" key="reply">
|
||||
{ReplyPlaceholder.component({discussion: this.discussion})}
|
||||
</div>
|
||||
) : ''
|
||||
}
|
||||
{items}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -39,16 +39,6 @@ export default class PostStreamScrubber extends Component {
|
||||
*/
|
||||
this.description = '';
|
||||
|
||||
/**
|
||||
* The integer index of the last item that is visible in the viewport. This
|
||||
* is displayed on the scrubber (i.e. X of 100 posts).
|
||||
*
|
||||
* @return {Integer}
|
||||
*/
|
||||
this.visibleIndex = computed('index', 'visible', 'count', function(index, visible, count) {
|
||||
return Math.min(count, Math.ceil(Math.max(0, index) + visible));
|
||||
});
|
||||
|
||||
// When the post stream begins loading posts at a certain index, we want our
|
||||
// scrubber scrollbar to jump to that position.
|
||||
this.props.stream.on('unpaused', this.handlers.streamWasUnpaused = this.streamWasUnpaused.bind(this));
|
||||
@@ -68,10 +58,10 @@ export default class PostStreamScrubber extends Component {
|
||||
const retain = this.subtree.retain();
|
||||
const count = this.count();
|
||||
const unreadCount = this.props.stream.discussion.unreadCount();
|
||||
const unreadPercent = Math.min(count - this.index, unreadCount) / count;
|
||||
const unreadPercent = count ? Math.min(count - this.index, unreadCount) / count : 0;
|
||||
|
||||
const viewing = app.translator.transChoice('core.forum.post_scrubber.viewing_text', count, {
|
||||
index: <span className="Scrubber-index">{retain || formatNumber(this.visibleIndex())}</span>,
|
||||
index: <span className="Scrubber-index">{retain || formatNumber(Math.ceil(this.index + this.visible))}</span>,
|
||||
count: <span className="Scrubber-count">{formatNumber(count)}</span>
|
||||
});
|
||||
|
||||
@@ -200,6 +190,7 @@ export default class PostStreamScrubber extends Component {
|
||||
const marginTop = stream.getMarginTop();
|
||||
const viewportTop = scrollTop + marginTop;
|
||||
const viewportHeight = $(window).height() - marginTop;
|
||||
const viewportBottom = viewportTop + viewportHeight;
|
||||
|
||||
// Before looping through all of the posts, we reset the scrollbar
|
||||
// properties to a 'default' state. These values reflect what would be
|
||||
@@ -219,32 +210,28 @@ export default class PostStreamScrubber extends Component {
|
||||
const height = $this.outerHeight(true);
|
||||
|
||||
// If this item is above the top of the viewport, skip to the next
|
||||
// post. If it's below the bottom of the viewport, break out of the
|
||||
// one. If it's below the bottom of the viewport, break out of the
|
||||
// loop.
|
||||
if (top + height < viewportTop) {
|
||||
visible = (top + height - viewportTop) / height;
|
||||
index = parseFloat($this.data('index')) + 1 - visible;
|
||||
return true;
|
||||
}
|
||||
if (top > viewportTop + viewportHeight) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the bottom half of this item is visible at the top of the
|
||||
// viewport, then set the start of the visible proportion as our index.
|
||||
if (top <= viewportTop && top + height > viewportTop) {
|
||||
visible = (top + height - viewportTop) / height;
|
||||
index = parseFloat($this.data('index')) + 1 - visible;
|
||||
//
|
||||
// If the top half of this item is visible at the bottom of the
|
||||
// viewport, then add the visible proportion to the visible
|
||||
// counter.
|
||||
} else if (top + height >= viewportTop + viewportHeight) {
|
||||
visible += (viewportTop + viewportHeight - top) / height;
|
||||
//
|
||||
// If the whole item is visible in the viewport, then increment the
|
||||
// visible counter.
|
||||
} else visible++;
|
||||
// Work out how many pixels of this item are visible inside the viewport.
|
||||
// Then add the proportion of this item's total height to the index.
|
||||
const visibleTop = Math.max(0, viewportTop - top);
|
||||
const visibleBottom = Math.min(height, viewportTop + viewportHeight - top);
|
||||
const visiblePost = visibleBottom - visibleTop;
|
||||
|
||||
if (top <= viewportTop) {
|
||||
index = parseFloat($this.data('index')) + visibleTop / height;
|
||||
}
|
||||
|
||||
if (visiblePost > 0) {
|
||||
visible += visiblePost / height;
|
||||
}
|
||||
|
||||
// If this item has a time associated with it, then set the
|
||||
// scrollbar's current period to a formatted version of this time.
|
||||
@@ -328,7 +315,7 @@ export default class PostStreamScrubber extends Component {
|
||||
const visible = this.visible || 1;
|
||||
|
||||
const $scrubber = this.$();
|
||||
$scrubber.find('.Scrubber-index').text(formatNumber(this.visibleIndex()));
|
||||
$scrubber.find('.Scrubber-index').text(formatNumber(Math.ceil(index + visible)));
|
||||
$scrubber.find('.Scrubber-description').text(this.description);
|
||||
$scrubber.toggleClass('disabled', this.disabled());
|
||||
|
||||
|
@@ -2,6 +2,7 @@ import Component from 'flarum/Component';
|
||||
import UserCard from 'flarum/components/UserCard';
|
||||
import avatar from 'flarum/helpers/avatar';
|
||||
import username from 'flarum/helpers/username';
|
||||
import userOnline from 'flarum/helpers/userOnline';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
|
||||
/**
|
||||
@@ -45,6 +46,7 @@ export default class PostUser extends Component {
|
||||
|
||||
return (
|
||||
<div className="PostUser">
|
||||
{userOnline(user)}
|
||||
<h3>
|
||||
<a href={app.route.user(user)} config={m.route}>
|
||||
{avatar(user, {className: 'PostUser-avatar'})}{' '}{username(user)}
|
||||
|
@@ -65,9 +65,9 @@ export default class PostsUserPage extends UserPage {
|
||||
{this.posts.map(post => (
|
||||
<li>
|
||||
<div className="PostsUserPage-discussion">
|
||||
In <a href={app.route.post(post)} config={m.route}>{post.discussion().title()}</a>
|
||||
{app.translator.trans('core.forum.user.in_discussion_text', {discussion: <a href={app.route.post(post)} config={m.route}>{post.discussion().title()}</a>})}
|
||||
</div>
|
||||
{CommentPost.component({post, showDiscussionTitle: true})}
|
||||
{CommentPost.component({post})}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
@@ -4,6 +4,13 @@ import Button from 'flarum/components/Button';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
|
||||
function minimizeComposerIfFullScreen(e) {
|
||||
if (app.composer.isFullScreen()) {
|
||||
app.composer.minimize();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `ReplyComposer` component displays the composer content for replying to a
|
||||
* discussion.
|
||||
@@ -17,7 +24,9 @@ export default class ReplyComposer extends ComposerBody {
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.editor.props.preview = () => {
|
||||
this.editor.props.preview = e => {
|
||||
minimizeComposerIfFullScreen(e);
|
||||
|
||||
m.route(app.route.discussion(this.props.discussion, 'reply'));
|
||||
};
|
||||
}
|
||||
@@ -34,10 +43,16 @@ export default class ReplyComposer extends ComposerBody {
|
||||
const items = super.headerItems();
|
||||
const discussion = this.props.discussion;
|
||||
|
||||
const routeAndMinimize = function(element, isInitialized) {
|
||||
if (isInitialized) return;
|
||||
$(element).on('click', minimizeComposerIfFullScreen);
|
||||
m.route.apply(this, arguments);
|
||||
};
|
||||
|
||||
items.add('title', (
|
||||
<h3>
|
||||
{icon('reply')} {' '}
|
||||
<a href={app.route.discussion(discussion)} config={m.route}>{discussion.title()}</a>
|
||||
<a href={app.route.discussion(discussion)} config={routeAndMinimize}>{discussion.title()}</a>
|
||||
</h3>
|
||||
));
|
||||
|
||||
|
@@ -23,7 +23,7 @@ export default class Search extends Component {
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.value = m.prop();
|
||||
this.value = m.prop('');
|
||||
|
||||
/**
|
||||
* Whether or not the search input has focus.
|
||||
@@ -131,7 +131,11 @@ export default class Search extends Component {
|
||||
break;
|
||||
|
||||
case 13: // Return
|
||||
m.route(this.getItem(this.index).find('a').attr('href'));
|
||||
if (this.value()) {
|
||||
m.route(this.getItem(this.index).find('a').attr('href'));
|
||||
} else {
|
||||
this.clear();
|
||||
}
|
||||
this.$('input').blur();
|
||||
break;
|
||||
|
||||
|
@@ -48,7 +48,7 @@ export default class SettingsPage extends UserPage {
|
||||
FieldSet.component({
|
||||
label: app.translator.trans('core.forum.settings.notifications_heading'),
|
||||
className: 'Settings-notifications',
|
||||
children: [NotificationGrid.component({user: this.user})]
|
||||
children: this.notificationsItems().toArray()
|
||||
})
|
||||
);
|
||||
|
||||
@@ -90,6 +90,19 @@ export default class SettingsPage extends UserPage {
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the user's notification settings.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
notificationsItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('notificationGrid', NotificationGrid.component({user: this.user}));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a callback that will save a value to the given preference.
|
||||
*
|
||||
|
@@ -39,13 +39,6 @@ export default class SignUpModal extends Modal {
|
||||
* @type {Function}
|
||||
*/
|
||||
this.password = m.prop(this.props.password || '');
|
||||
|
||||
/**
|
||||
* The user that has been signed up and that should be welcomed.
|
||||
*
|
||||
* @type {null|User}
|
||||
*/
|
||||
this.welcomeUser = null;
|
||||
}
|
||||
|
||||
className() {
|
||||
@@ -68,12 +61,12 @@ export default class SignUpModal extends Modal {
|
||||
}
|
||||
|
||||
body() {
|
||||
const body = [
|
||||
return [
|
||||
this.props.token ? '' : <LogInButtons/>,
|
||||
|
||||
<div className="Form Form--centered">
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="username" placeholder={extractText(app.translator.trans('core.forum.sign_up.username_placeholder'))}
|
||||
<input className="FormControl" name="username" type="text" placeholder={extractText(app.translator.trans('core.forum.sign_up.username_placeholder'))}
|
||||
value={this.username()}
|
||||
onchange={m.withAttr('value', this.username)}
|
||||
disabled={this.loading} />
|
||||
@@ -105,36 +98,6 @@ export default class SignUpModal extends Modal {
|
||||
</div>
|
||||
</div>
|
||||
];
|
||||
|
||||
if (this.welcomeUser) {
|
||||
const user = this.welcomeUser;
|
||||
|
||||
const fadeIn = (element, isInitialized) => {
|
||||
if (isInitialized) return;
|
||||
$(element).hide().fadeIn();
|
||||
};
|
||||
|
||||
body.push(
|
||||
<div className="SignUpModal-welcome" style={{background: user.color()}} config={fadeIn}>
|
||||
<div className="darkenBackground">
|
||||
<div className="container">
|
||||
{avatar(user)}
|
||||
<h3>{app.translator.trans('core.forum.sign_up.welcome_text', {user})}</h3>
|
||||
|
||||
<p>{app.translator.trans('core.forum.sign_up.confirmation_message', {email: <strong>{user.email()}</strong>})}</p>
|
||||
|
||||
<p>
|
||||
<Button className="Button Button--primary" onclick={this.hide.bind(this)}>
|
||||
{app.translator.trans('core.forum.sign_up.dismiss_button')}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
footer() {
|
||||
@@ -181,19 +144,7 @@ export default class SignUpModal extends Modal {
|
||||
data,
|
||||
errorHandler: this.onerror.bind(this)
|
||||
}).then(
|
||||
payload => {
|
||||
const user = app.store.pushPayload(payload);
|
||||
|
||||
// If the user's new account has been activated, then we can assume
|
||||
// that they have been logged in too. Thus, we will reload the page.
|
||||
// Otherwise, we will show a message asking them to check their email.
|
||||
if (user.isActivated()) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
this.welcomeUser = user;
|
||||
this.loaded();
|
||||
}
|
||||
},
|
||||
() => window.location.reload(),
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
}
|
||||
@@ -216,6 +167,10 @@ export default class SignUpModal extends Modal {
|
||||
data.password = this.password();
|
||||
}
|
||||
|
||||
if (this.props.avatarUrl) {
|
||||
data.avatarUrl = this.props.avatarUrl;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ export default class TextEditor extends Component {
|
||||
/**
|
||||
* The value of the textarea.
|
||||
*
|
||||
* @type {[type]}
|
||||
* @type {String}
|
||||
*/
|
||||
this.value = m.prop(this.props.value || '');
|
||||
}
|
||||
@@ -27,14 +27,14 @@ export default class TextEditor extends Component {
|
||||
view() {
|
||||
return (
|
||||
<div className="TextEditor">
|
||||
<textarea className="FormControl TextEditor-flexible"
|
||||
<textarea className="FormControl Composer-flexible"
|
||||
config={this.configTextarea.bind(this)}
|
||||
oninput={m.withAttr('value', this.oninput.bind(this))}
|
||||
placeholder={this.props.placeholder || ''}
|
||||
disabled={!!this.props.disabled}
|
||||
value={this.value()}/>
|
||||
|
||||
<ul className="TextEditor-controls">
|
||||
<ul className="TextEditor-controls Composer-footer">
|
||||
{listItems(this.controlItems().toArray())}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -72,6 +72,7 @@ export default class TextEditor extends Component {
|
||||
children: this.props.submitLabel,
|
||||
icon: 'check',
|
||||
className: 'Button Button--primary',
|
||||
itemClassName: 'App-primaryControl',
|
||||
onclick: this.onsubmit.bind(this)
|
||||
})
|
||||
);
|
||||
|
@@ -91,7 +91,12 @@ export default class UserBio extends Component {
|
||||
if (user.bio() !== value) {
|
||||
this.loading = true;
|
||||
|
||||
user.save({bio: value}).finally(this.loaded.bind(this));
|
||||
user.save({bio: value})
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
this.editing = false;
|
||||
|
@@ -134,7 +134,8 @@ export default class UserPage extends Page {
|
||||
href: app.route('user.posts', {username: user.username()}),
|
||||
children: [app.translator.trans('core.forum.user.posts_link'), <span className="Button-badge">{user.commentsCount()}</span>],
|
||||
icon: 'comment-o'
|
||||
})
|
||||
}),
|
||||
100
|
||||
);
|
||||
|
||||
items.add('discussions',
|
||||
@@ -142,17 +143,19 @@ export default class UserPage extends Page {
|
||||
href: app.route('user.discussions', {username: user.username()}),
|
||||
children: [app.translator.trans('core.forum.user.discussions_link'), <span className="Button-badge">{user.discussionsCount()}</span>],
|
||||
icon: 'reorder'
|
||||
})
|
||||
}),
|
||||
90
|
||||
);
|
||||
|
||||
if (app.session.user === user) {
|
||||
items.add('separator', Separator.component());
|
||||
items.add('separator', Separator.component(), -90);
|
||||
items.add('settings',
|
||||
LinkButton.component({
|
||||
href: app.route('settings'),
|
||||
children: app.translator.trans('core.forum.user.settings_link'),
|
||||
icon: 'cog'
|
||||
})
|
||||
}),
|
||||
-100
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import highlight from 'flarum/helpers/highlight';
|
||||
import avatar from 'flarum/helpers/avatar';
|
||||
import username from 'flarum/helpers/username';
|
||||
|
||||
/**
|
||||
* The `UsersSearchSource` finds and displays user search results in the search
|
||||
@@ -23,14 +24,19 @@ export default class UsersSearchResults {
|
||||
|
||||
return [
|
||||
<li className="Dropdown-header">{app.translator.trans('core.forum.search.users_heading')}</li>,
|
||||
results.map(user => (
|
||||
<li className="UserSearchResult" data-index={'users' + user.id()}>
|
||||
<a href={app.route.user(user)} config={m.route}>
|
||||
{avatar(user)}
|
||||
{highlight(user.username(), query)}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
results.map(user => {
|
||||
const name = username(user);
|
||||
name.children[0] = highlight(name.children[0], query);
|
||||
|
||||
return (
|
||||
<li className="UserSearchResult" data-index={'users' + user.id()}>
|
||||
<a href={app.route.user(user)} config={m.route}>
|
||||
{avatar(user)}
|
||||
{name}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
];
|
||||
}
|
||||
}
|
||||
|
55
js/forum/src/initializers/alertEmailConfirmation.js
Normal file
55
js/forum/src/initializers/alertEmailConfirmation.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import Alert from 'flarum/components/Alert';
|
||||
import Button from 'flarum/components/Button';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
/**
|
||||
* Shows an alert if the user has not yet confirmed their email address.
|
||||
*
|
||||
* @param {ForumApp} app
|
||||
*/
|
||||
export default function alertEmailConfirmation(app) {
|
||||
const user = app.session.user;
|
||||
|
||||
if (!user || user.isActivated()) return;
|
||||
|
||||
const resendButton = Button.component({
|
||||
className: 'Button Button--link',
|
||||
children: app.translator.trans('core.forum.user_email_confirmation.resend_button'),
|
||||
onclick: function() {
|
||||
resendButton.props.loading = true;
|
||||
m.redraw();
|
||||
|
||||
app.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/send-confirmation',
|
||||
}).then(() => {
|
||||
resendButton.props.loading = false;
|
||||
resendButton.props.children = [icon('check'), ' ', app.translator.trans('core.forum.user_email_confirmation.sent_message')];
|
||||
resendButton.props.disabled = true;
|
||||
m.redraw();
|
||||
}).catch(() => {
|
||||
resendButton.props.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
class ContainedAlert extends Alert {
|
||||
view() {
|
||||
const vdom = super.view();
|
||||
|
||||
vdom.children = [<div className="container">{vdom.children}</div>];
|
||||
|
||||
return vdom;
|
||||
}
|
||||
}
|
||||
|
||||
m.mount(
|
||||
$('<div/>').insertBefore('#content')[0],
|
||||
ContainedAlert.component({
|
||||
dismissible: false,
|
||||
children: app.translator.trans('core.forum.user_email_confirmation.alert_message', {email: <strong>{user.email()}</strong>}),
|
||||
controls: [resendButton]
|
||||
})
|
||||
);
|
||||
}
|
@@ -217,18 +217,19 @@ export default {
|
||||
*/
|
||||
deleteAction() {
|
||||
if (confirm(extractText(app.translator.trans('core.forum.discussion_controls.delete_confirmation')))) {
|
||||
// If there is a discussion list in the cache, remove this discussion.
|
||||
if (app.cache.discussionList) {
|
||||
app.cache.discussionList.removeDiscussion(this);
|
||||
}
|
||||
|
||||
// If we're currently viewing the discussion that was deleted, go back
|
||||
// to the previous page.
|
||||
if (app.viewingDiscussion(this)) {
|
||||
app.history.back();
|
||||
}
|
||||
|
||||
return this.delete();
|
||||
return this.delete().then(() => {
|
||||
// If there is a discussion list in the cache, remove this discussion.
|
||||
if (app.cache.discussionList) {
|
||||
app.cache.discussionList.removeDiscussion(this);
|
||||
m.redraw();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -78,7 +78,7 @@ export default {
|
||||
* @return {ItemList}
|
||||
* @protected
|
||||
*/
|
||||
destructiveControls(post) {
|
||||
destructiveControls(post, context) {
|
||||
const items = new ItemList();
|
||||
|
||||
if (post.contentType() === 'comment' && !post.isHidden()) {
|
||||
@@ -97,11 +97,11 @@ export default {
|
||||
onclick: this.restoreAction.bind(post)
|
||||
}));
|
||||
}
|
||||
if (post.canDelete() && post.number() !== 1) {
|
||||
if (post.canDelete()) {
|
||||
items.add('delete', Button.component({
|
||||
icon: 'times',
|
||||
children: app.translator.trans('core.forum.post_controls.delete_forever_button'),
|
||||
onclick: this.deleteAction.bind(post)
|
||||
onclick: this.deleteAction.bind(post, context)
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -144,9 +144,32 @@ export default {
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
deleteAction() {
|
||||
this.discussion().removePost(this.id());
|
||||
deleteAction(context) {
|
||||
if (context) context.loading = true;
|
||||
|
||||
return this.delete();
|
||||
return this.delete()
|
||||
.then(() => {
|
||||
const discussion = this.discussion();
|
||||
|
||||
discussion.removePost(this.id());
|
||||
|
||||
// If this was the last post in the discussion, then we will assume that
|
||||
// the whole discussion was deleted too.
|
||||
if (!discussion.postIds().length) {
|
||||
// If there is a discussion list in the cache, remove this discussion.
|
||||
if (app.cache.discussionList) {
|
||||
app.cache.discussionList.removeDiscussion(discussion);
|
||||
}
|
||||
|
||||
if (app.viewingDiscussion(discussion)) {
|
||||
app.history.back();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
if (context) context.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -2,6 +2,7 @@ import ItemList from 'flarum/utils/ItemList';
|
||||
import Alert from 'flarum/components/Alert';
|
||||
import Button from 'flarum/components/Button';
|
||||
import RequestErrorModal from 'flarum/components/RequestErrorModal';
|
||||
import ConfirmPasswordModal from 'flarum/components/ConfirmPasswordModal';
|
||||
import Translator from 'flarum/Translator';
|
||||
import extract from 'flarum/utils/extract';
|
||||
import patchMithril from 'flarum/utils/patchMithril';
|
||||
@@ -182,20 +183,23 @@ export default class App {
|
||||
* @return {Promise}
|
||||
* @public
|
||||
*/
|
||||
request(options) {
|
||||
request(originalOptions) {
|
||||
const options = Object.assign({}, originalOptions);
|
||||
|
||||
// Set some default options if they haven't been overridden. We want to
|
||||
// authenticate all requests with the session token. We also want all
|
||||
// requests to run asynchronously in the background, so that they don't
|
||||
// prevent redraws from occurring.
|
||||
options.config = options.config || this.session.authorize.bind(this.session);
|
||||
options.background = options.background || true;
|
||||
|
||||
extend(options, 'config', (result, xhr) => xhr.setRequestHeader('X-CSRF-Token', this.session.csrfToken));
|
||||
|
||||
// If the method is something like PATCH or DELETE, which not all servers
|
||||
// support, then we'll send it as a POST request with a the intended method
|
||||
// specified in the X-Fake-Http-Method header.
|
||||
// and clients support, then we'll send it as a POST request with the
|
||||
// intended method specified in the X-HTTP-Method-Override header.
|
||||
if (options.method !== 'GET' && options.method !== 'POST') {
|
||||
const method = options.method;
|
||||
extend(options, 'config', (result, xhr) => xhr.setRequestHeader('X-Fake-Http-Method', method));
|
||||
extend(options, 'config', (result, xhr) => xhr.setRequestHeader('X-HTTP-Method-Override', method));
|
||||
options.method = 'POST';
|
||||
}
|
||||
|
||||
@@ -218,7 +222,7 @@ export default class App {
|
||||
if (original) {
|
||||
responseText = original(xhr.responseText);
|
||||
} else {
|
||||
responseText = xhr.responseText.length > 0 ? xhr.responseText : null;
|
||||
responseText = xhr.responseText || null;
|
||||
}
|
||||
|
||||
const status = xhr.status;
|
||||
@@ -227,6 +231,11 @@ export default class App {
|
||||
throw new RequestError(status, responseText, options, xhr);
|
||||
}
|
||||
|
||||
if (xhr.getResponseHeader) {
|
||||
const csrfToken = xhr.getResponseHeader('X-CSRF-Token');
|
||||
if (csrfToken) app.session.csrfToken = csrfToken;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(responseText);
|
||||
} catch (e) {
|
||||
@@ -238,7 +247,9 @@ export default class App {
|
||||
|
||||
// Now make the request. If it's a failure, inspect the error that was
|
||||
// returned and show an alert containing its contents.
|
||||
return m.request(options).then(null, error => {
|
||||
const deferred = m.deferred();
|
||||
|
||||
m.request(options).then(response => deferred.resolve(response), error => {
|
||||
this.requestError = error;
|
||||
|
||||
let children;
|
||||
@@ -283,8 +294,10 @@ export default class App {
|
||||
this.alerts.show(error.alert);
|
||||
}
|
||||
|
||||
throw error;
|
||||
deferred.reject(error);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -54,6 +54,14 @@ export default class Component {
|
||||
*/
|
||||
this.element = null;
|
||||
|
||||
/**
|
||||
* Whether or not to retain the component's subtree on redraw.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @public
|
||||
*/
|
||||
this.retain = false;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
@@ -91,7 +99,7 @@ export default class Component {
|
||||
* @public
|
||||
*/
|
||||
render() {
|
||||
const vdom = this.view();
|
||||
const vdom = this.retain ? {subtree: 'retain'} : this.view();
|
||||
|
||||
// Override the root element's config attribute with our own function, which
|
||||
// will set the component instance's element property to the root DOM
|
||||
|
@@ -150,14 +150,17 @@ export default class Model {
|
||||
// Before we update the model's data, we should make a copy of the model's
|
||||
// old data so that we can revert back to it if something goes awry during
|
||||
// persistence.
|
||||
const oldData = JSON.parse(JSON.stringify(this.data));
|
||||
const oldData = this.copyData();
|
||||
|
||||
this.pushData(data);
|
||||
|
||||
const request = {data};
|
||||
if (options.meta) request.meta = options.meta;
|
||||
|
||||
return app.request(Object.assign({
|
||||
method: this.exists ? 'PATCH' : 'POST',
|
||||
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
|
||||
data: {data}
|
||||
data: request
|
||||
}, options)).then(
|
||||
// If everything went well, we'll make sure the store knows that this
|
||||
// model exists now (if it didn't already), and we'll push the data that
|
||||
@@ -209,6 +212,10 @@ export default class Model {
|
||||
return '/' + this.data.type + (this.exists ? '/' + this.data.id : '');
|
||||
}
|
||||
|
||||
copyData() {
|
||||
return JSON.parse(JSON.stringify(this.data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a function which returns the value of the given attribute.
|
||||
*
|
||||
|
@@ -3,7 +3,7 @@
|
||||
* to the current authenticated user, and provides methods to log in/out.
|
||||
*/
|
||||
export default class Session {
|
||||
constructor(token, user) {
|
||||
constructor(user, csrfToken) {
|
||||
/**
|
||||
* The current authenticated user.
|
||||
*
|
||||
@@ -13,12 +13,12 @@ export default class Session {
|
||||
this.user = user;
|
||||
|
||||
/**
|
||||
* The token that was used for authentication.
|
||||
* The CSRF token.
|
||||
*
|
||||
* @type {String|null}
|
||||
* @public
|
||||
*/
|
||||
this.token = token;
|
||||
this.csrfToken = csrfToken;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,8 +35,7 @@ export default class Session {
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('baseUrl') + '/login',
|
||||
data: {identification, password}
|
||||
}, options))
|
||||
.then(() => window.location.reload());
|
||||
}, options));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,19 +44,6 @@ export default class Session {
|
||||
* @public
|
||||
*/
|
||||
logout() {
|
||||
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply an authorization header with the current token to the given
|
||||
* XMLHttpRequest object.
|
||||
*
|
||||
* @param {XMLHttpRequest} xhr
|
||||
* @public
|
||||
*/
|
||||
authorize(xhr) {
|
||||
if (this.token) {
|
||||
xhr.setRequestHeader('Authorization', 'Token ' + this.token);
|
||||
}
|
||||
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.csrfToken;
|
||||
}
|
||||
}
|
||||
|
@@ -24,11 +24,6 @@ export default class Badge extends Component {
|
||||
attrs.className = 'Badge ' + (type ? 'Badge--' + type : '') + ' ' + (attrs.className || '');
|
||||
attrs.title = extract(attrs, 'label') || '';
|
||||
|
||||
// Give the badge a unique key so that when badges are displayed together,
|
||||
// and then one is added/removed, Mithril will correctly redraw the series
|
||||
// of badges.
|
||||
attrs.key = attrs.type;
|
||||
|
||||
return (
|
||||
<span {...attrs}>
|
||||
{iconName ? icon(iconName, {className: 'Badge-icon'}) : m.trust(' ')}
|
||||
|
@@ -47,13 +47,20 @@ export default class Dropdown extends Component {
|
||||
// bottom of the viewport. If it does, we will apply class to make it show
|
||||
// above the toggle button instead of below it.
|
||||
this.$().on('shown.bs.dropdown', () => {
|
||||
const $menu = this.$('.Dropdown-menu').removeClass('Dropdown-menu--top');
|
||||
const $menu = this.$('.Dropdown-menu');
|
||||
const isRight = $menu.hasClass('Dropdown-menu--right');
|
||||
$menu.removeClass('Dropdown-menu--top Dropdown-menu--right');
|
||||
|
||||
$menu.toggleClass(
|
||||
'Dropdown-menu--top',
|
||||
$menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height()
|
||||
);
|
||||
|
||||
$menu.toggleClass(
|
||||
'Dropdown-menu--right',
|
||||
isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width()
|
||||
);
|
||||
|
||||
if (this.props.onshow) {
|
||||
this.props.onshow();
|
||||
m.redraw();
|
||||
|
@@ -98,7 +98,10 @@ export default class Modal extends Component {
|
||||
* Focus on the first input when the modal is ready to be used.
|
||||
*/
|
||||
onready() {
|
||||
this.$('form :input:first').focus().select();
|
||||
this.$('form').find('input, select, textarea').first().focus().select();
|
||||
}
|
||||
|
||||
onhide() {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -46,6 +46,8 @@ export default class ModalManager extends Component {
|
||||
this.showing = true;
|
||||
this.component = component;
|
||||
|
||||
app.current.retain = true;
|
||||
|
||||
m.redraw(true);
|
||||
|
||||
this.$().modal({backdrop: this.component.isDismissible() ? true : 'static'}).modal('show');
|
||||
@@ -77,8 +79,14 @@ export default class ModalManager extends Component {
|
||||
* @protected
|
||||
*/
|
||||
clear() {
|
||||
if (this.component) {
|
||||
this.component.onhide();
|
||||
}
|
||||
|
||||
this.component = null;
|
||||
|
||||
app.current.retain = false;
|
||||
|
||||
m.lazyRedraw();
|
||||
}
|
||||
|
||||
|
@@ -34,6 +34,14 @@ export default function listItems(items) {
|
||||
const active = item.component && item.component.isActive && item.component.isActive(item.props);
|
||||
const className = item.props ? item.props.itemClassName : item.itemClassName;
|
||||
|
||||
if (isListItem) {
|
||||
item.attrs = item.attrs || {};
|
||||
item.attrs.key = item.attrs.key || item.itemName;
|
||||
}
|
||||
|
||||
const space = new String(' ');
|
||||
space.attrs = {key: '_space_'+item.itemName};
|
||||
|
||||
return [
|
||||
isListItem
|
||||
? item
|
||||
@@ -41,10 +49,11 @@ export default function listItems(items) {
|
||||
(item.itemName ? 'item-' + item.itemName : ''),
|
||||
className,
|
||||
(active ? 'active' : '')
|
||||
])}>
|
||||
])}
|
||||
key={item.itemName}>
|
||||
{item}
|
||||
</li>,
|
||||
' '
|
||||
space
|
||||
];
|
||||
});
|
||||
}
|
||||
|
13
js/lib/helpers/userOnline.js
Normal file
13
js/lib/helpers/userOnline.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
/**
|
||||
* The `useronline` helper displays a green circle if the user is online
|
||||
*
|
||||
* @param {User} user
|
||||
* @return {Object}
|
||||
*/
|
||||
export default function userOnline(user) {
|
||||
if (user.lastSeenTime() && user.isOnline()) {
|
||||
return <span className="UserOnline">{icon('circle')}</span>;
|
||||
}
|
||||
}
|
@@ -6,7 +6,7 @@
|
||||
* @return {Object}
|
||||
*/
|
||||
export default function username(user) {
|
||||
const name = (user && user.username()) || app.translator.trans('core.lib.deleted_user_text');
|
||||
const name = (user && user.username()) || app.translator.trans('core.lib.username.deleted_text');
|
||||
|
||||
return <span className="username">{name}</span>;
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@ export default function preload(app) {
|
||||
app.forum = app.store.getById('forums', 1);
|
||||
|
||||
app.session = new Session(
|
||||
app.preload.session.token,
|
||||
app.store.getById('users', app.preload.session.userId)
|
||||
app.store.getById('users', app.preload.session.userId),
|
||||
app.preload.session.csrfToken
|
||||
);
|
||||
}
|
||||
|
@@ -2,14 +2,13 @@ import Model from 'flarum/Model';
|
||||
import mixin from 'flarum/utils/mixin';
|
||||
import computed from 'flarum/utils/computed';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import { slug } from 'flarum/utils/string';
|
||||
import Badge from 'flarum/components/Badge';
|
||||
|
||||
export default class Discussion extends Model {}
|
||||
|
||||
Object.assign(Discussion.prototype, {
|
||||
title: Model.attribute('title'),
|
||||
slug: computed('title', slug),
|
||||
slug: Model.attribute('slug'),
|
||||
|
||||
startTime: Model.attribute('startTime', Model.transformDate),
|
||||
startUser: Model.hasOne('startUser'),
|
||||
@@ -99,7 +98,9 @@ Object.assign(Discussion.prototype, {
|
||||
* @public
|
||||
*/
|
||||
postIds() {
|
||||
return this.data.relationships.posts.data.map(link => link.id);
|
||||
const posts = this.data.relationships.posts;
|
||||
|
||||
return posts ? posts.data.map(link => link.id) : [];
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -17,7 +17,7 @@ Object.assign(User.prototype, {
|
||||
|
||||
avatarUrl: Model.attribute('avatarUrl'),
|
||||
bio: Model.attribute('bio'),
|
||||
bioHtml: computed('bio', bio => bio ? '<p>' + $('<div/>').text(bio).html().replace(/\n/g, '<br>').autoLink() + '</p>' : ''),
|
||||
bioHtml: computed('bio', bio => bio ? '<p>' + $('<div/>').text(bio).html().replace(/\n/g, '<br>').autoLink({rel: 'nofollow'}) + '</p>' : ''),
|
||||
preferences: Model.attribute('preferences'),
|
||||
groups: Model.hasMany('groups'),
|
||||
|
||||
|
@@ -14,15 +14,21 @@
|
||||
}
|
||||
@media @desktop, @desktop-hd {
|
||||
.App-nav {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
top: @header-height;
|
||||
bottom: 0;
|
||||
height: ~"calc(100vh - @{header-height})";
|
||||
width: @admin-pane-width;
|
||||
.box-shadow(0 6px 6px @shadow-color);
|
||||
background: @body-bg;
|
||||
border-top: 1px solid @control-bg;
|
||||
z-index: @zindex-pane;
|
||||
overflow: auto;
|
||||
|
||||
.affix & {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
.App-content .sideNavOffset {
|
||||
margin-left: @admin-pane-width;
|
||||
|
@@ -39,12 +39,15 @@
|
||||
h3 {
|
||||
margin: 0;
|
||||
line-height: 1.5em;
|
||||
color: @secondary-color;
|
||||
|
||||
&, input, a {
|
||||
color: @secondary-color;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
input, a {
|
||||
color: inherit;
|
||||
}
|
||||
input {
|
||||
font-size: 16px;
|
||||
width: 500px;
|
||||
@@ -142,6 +145,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.Composer-controls .fa-minus:before {
|
||||
content: @fa-var-times;
|
||||
}
|
||||
.composer-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -172,6 +178,21 @@
|
||||
border-bottom: 0;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.normal &:first-child {
|
||||
margin: -@header-height-phone 50px 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: @zindex-header + 1;
|
||||
border: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
h3 {
|
||||
color: @header-control-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
&, a, input {
|
||||
@@ -184,10 +205,9 @@
|
||||
}
|
||||
.ComposerBody-editor {
|
||||
padding: 15px;
|
||||
|
||||
textarea {
|
||||
height: 50vh !important;
|
||||
}
|
||||
}
|
||||
.ComposerBody-editor .TextEditor-controls .item-submit {
|
||||
position: absolute !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +304,9 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
.ComposerBody--discussion .ComposerBody-header .item-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media @desktop-up {
|
||||
|
@@ -32,7 +32,7 @@
|
||||
pointer-events: none;
|
||||
|
||||
.Badge {
|
||||
margin-left: -15px;
|
||||
margin-left: -10px;
|
||||
position: relative;
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -199,7 +199,7 @@
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: @control-bg;
|
||||
background: mix(@control-bg, @body-bg, 50%);
|
||||
}
|
||||
&:hover .DiscussionListItem-controls,
|
||||
.DiscussionListItem-controls.open {
|
||||
|
@@ -128,7 +128,6 @@
|
||||
.DiscussionPage-list {
|
||||
.panePinned & {
|
||||
left: 0;
|
||||
z-index: @zindex-composer - 1;
|
||||
.transition(none);
|
||||
}
|
||||
}
|
||||
|
@@ -6,8 +6,7 @@
|
||||
margin-bottom: 20px;
|
||||
|
||||
.Button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
.Button--block();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,11 @@
|
||||
.LogInButton {
|
||||
&:extend(.Button--block);
|
||||
|
||||
.Button-icon {
|
||||
font-size: 18px;
|
||||
vertical-align: -1px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
.LogInButtons {
|
||||
width: 200px;
|
||||
|
@@ -32,7 +32,7 @@
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 17px;
|
||||
background: @control-color;
|
||||
background: @header-control-color;
|
||||
color: @header-bg;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
@@ -42,6 +42,11 @@
|
||||
border: 1px solid @header-bg;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
text-align: center;
|
||||
|
||||
@media @phone {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.new & {
|
||||
background: @header-color;
|
||||
|
@@ -49,6 +49,16 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.UserOnline {
|
||||
& .icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
& .fa-circle {
|
||||
color: @online-user-circle-color;
|
||||
}
|
||||
}
|
||||
|
||||
.UserCard {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
@@ -71,7 +81,7 @@
|
||||
pointer-events: none;
|
||||
|
||||
.Badge {
|
||||
margin-left: -15px;
|
||||
margin-left: -10px;
|
||||
position: relative;
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -167,6 +177,9 @@
|
||||
color: @muted-more-color;
|
||||
}
|
||||
}
|
||||
.Post--loading {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.PostMeta {
|
||||
display: inline;
|
||||
}
|
||||
@@ -250,7 +263,6 @@
|
||||
margin-top: -5px;
|
||||
float: right;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
.transition(opacity 0.2s);
|
||||
|
||||
.EventPost &, .Post--hidden:not(.revealContent) & {
|
||||
@@ -275,9 +287,6 @@
|
||||
.Post:hover &, &.open {
|
||||
opacity: 1;
|
||||
}
|
||||
&.open {
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.PostPreview {
|
||||
@@ -370,7 +379,6 @@
|
||||
border: 2px dashed @control-bg;
|
||||
color: @muted-color;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
|
||||
.Post-header {
|
||||
margin: 0;
|
||||
@@ -379,9 +387,6 @@
|
||||
}
|
||||
@media @tablet-up {
|
||||
.ReplyPlaceholder {
|
||||
margin-left: -20px;
|
||||
margin-right: -20px;
|
||||
padding-left: 110px;
|
||||
border-color: transparent;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
|
@@ -7,13 +7,17 @@
|
||||
padding: 0;
|
||||
|
||||
> li {
|
||||
margin-bottom: 40px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
}
|
||||
fieldset > ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
> li {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.Settings-account {
|
||||
|
@@ -96,6 +96,8 @@
|
||||
> .Button {
|
||||
color: @header-color;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
.App-titleControl--text {
|
||||
@@ -179,17 +181,17 @@
|
||||
display: none;
|
||||
}
|
||||
.Header-title {
|
||||
border-bottom: 1px solid @header-control-bg;
|
||||
padding: 13px 10px;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
line-height: @header-height-phone - 1;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
.Header-controls {
|
||||
margin-top: 10px;
|
||||
|
||||
> li {
|
||||
padding: 10px 10px 0;
|
||||
padding: 0 10px 0;
|
||||
}
|
||||
.FormControl, .ButtonGroup, .Button {
|
||||
width: 100%;
|
||||
@@ -201,6 +203,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.Header-secondary .Search {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
// On other devices, we stick the header up the top of the page, overlaying
|
||||
|
@@ -1,12 +1,11 @@
|
||||
.Badge {
|
||||
.Badge--size(22px);
|
||||
border: 1px solid @body-bg;
|
||||
background: @muted-color;
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
.box-shadow(0 2px 6px @shadow-color);
|
||||
.box-shadow(0 2px 4px @shadow-color);
|
||||
|
||||
.Badge-label {
|
||||
display: none;
|
||||
@@ -17,7 +16,7 @@
|
||||
width: @size;
|
||||
height: @size;
|
||||
border-radius: @size / 2;
|
||||
line-height: @size - 3px;
|
||||
line-height: @size - 1px;
|
||||
|
||||
&, .Badge-icon {
|
||||
font-size: 0.56 * @size;
|
||||
|
@@ -192,6 +192,8 @@
|
||||
.Button--block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
// Vertically space out multiple block buttons
|
||||
+ .Button--block {
|
||||
@@ -217,7 +219,7 @@
|
||||
border-radius: 18px;
|
||||
|
||||
.Avatar {
|
||||
margin: -2px 5px -2px -5px;
|
||||
margin: -2px 5px -2px -6px;
|
||||
.Avatar--size(24px);
|
||||
}
|
||||
}
|
||||
|
@@ -33,6 +33,8 @@
|
||||
border: 0;
|
||||
background: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.box-shadow(none);
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
@@ -127,9 +129,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.Dropdown-menu li:first-child {
|
||||
&, + li.Dropdown-separator {
|
||||
display: none;
|
||||
@media @tablet-up {
|
||||
.Dropdown-menu li:first-child {
|
||||
&, + li.Dropdown-separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
|
||||
.icon {
|
||||
font-size: 14px;
|
||||
|
@@ -2,12 +2,15 @@
|
||||
position: relative;
|
||||
}
|
||||
@media @tablet-up {
|
||||
.Search.focused {
|
||||
margin-left: -400px;
|
||||
.transition(all 0.4s);
|
||||
.Search {
|
||||
.transition(margin-left 0.4s);
|
||||
|
||||
input, .Search-results {
|
||||
width: 400px;
|
||||
&.focused {
|
||||
margin-left: -400px;
|
||||
|
||||
input, .Search-results {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,3 @@
|
||||
@import url(//fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700,600);
|
||||
|
||||
@import "font-awesome.less";
|
||||
@fa-font-path: "../../assets/fonts";
|
||||
|
||||
|
@@ -1,18 +1,21 @@
|
||||
// This is a mixin which styles components (buttons, inputs, etc.) for use on
|
||||
// dark backgrounds.
|
||||
.light-contents(@color: #fff, @control-bg: fade(#000, 10%), @control-color: #fff) {
|
||||
&, a, .Button--link, .Search-input {
|
||||
&, a {
|
||||
color: @color;
|
||||
}
|
||||
.Button--link, .Search-input {
|
||||
color: @control-color;
|
||||
}
|
||||
.FormControl {
|
||||
background: @control-bg;
|
||||
border: 0;
|
||||
color: @control-color;
|
||||
.placeholder(fade(@control-color, 80%));
|
||||
.placeholder(@control-color);
|
||||
|
||||
&:focus {
|
||||
color: @control-color;
|
||||
background: fadein(@control-bg, 5%);
|
||||
color: @color;
|
||||
background: fadein(darken(@control-bg, 5%), 10%);
|
||||
}
|
||||
}
|
||||
.Button, .Button:hover {
|
||||
|
@@ -66,6 +66,7 @@
|
||||
> ul > li, .Dropdown-menu > li {
|
||||
display: inline-block;
|
||||
margin: 0 20px 0 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
.Dropdown-separator {
|
||||
display: none;
|
||||
@@ -94,7 +95,7 @@
|
||||
float: left;
|
||||
|
||||
&, > ul {
|
||||
width: 175px;
|
||||
width: 190px;
|
||||
}
|
||||
> ul {
|
||||
margin-top: 30px;
|
||||
@@ -118,6 +119,6 @@
|
||||
@media @desktop-up {
|
||||
.sideNavOffset {
|
||||
margin-top: 30px;
|
||||
margin-left: 225px;
|
||||
margin-left: 240px;
|
||||
}
|
||||
}
|
||||
|
@@ -81,8 +81,8 @@
|
||||
.define-header(true) {
|
||||
@header-bg: @primary-color;
|
||||
@header-color: @body-bg;
|
||||
@header-control-bg: fade(#000, 10%);
|
||||
@header-control-color: @body-bg;
|
||||
@header-control-bg: mix(#000, @header-bg, 10%);
|
||||
@header-control-color: mix(@body-bg, @header-bg, 60%);
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
@@ -95,10 +95,10 @@
|
||||
|
||||
@border-radius: 4px;
|
||||
|
||||
@zindex-dropdown: 1000;
|
||||
@zindex-header: 1010;
|
||||
@zindex-header: 1000;
|
||||
@zindex-pane: 1010;
|
||||
@zindex-composer: 1020;
|
||||
@zindex-pane: 1030;
|
||||
@zindex-dropdown: 1030;
|
||||
@zindex-modal-background: 1040;
|
||||
@zindex-modal: 1050;
|
||||
@zindex-alerts: 1060;
|
||||
|
@@ -8,25 +8,15 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Core\Migration;
|
||||
|
||||
use Flarum\Database\AbstractMigration;
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateAccessTokensTable extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->schema->create('access_tokens', function (Blueprint $table) {
|
||||
$table->string('id', 100)->primary();
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->timestamp('created_at');
|
||||
$table->timestamp('expires_at');
|
||||
});
|
||||
return Migration::createTable(
|
||||
'access_tokens',
|
||||
function (Blueprint $table) {
|
||||
$table->string('id', 100)->primary();
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->timestamp('created_at');
|
||||
$table->timestamp('expires_at');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->schema->drop('access_tokens');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@@ -8,22 +8,12 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Core\Migration;
|
||||
|
||||
use Flarum\Database\AbstractMigration;
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateApiKeysTable extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->schema->create('api_keys', function (Blueprint $table) {
|
||||
$table->string('id', 100)->primary();
|
||||
});
|
||||
return Migration::createTable(
|
||||
'api_keys',
|
||||
function (Blueprint $table) {
|
||||
$table->string('id', 100)->primary();
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->schema->drop('api_keys');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@@ -8,23 +8,13 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Core\Migration;
|
||||
|
||||
use Flarum\Database\AbstractMigration;
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateConfigTable extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->schema->create('config', function (Blueprint $table) {
|
||||
$table->string('key', 100)->primary();
|
||||
$table->binary('value')->nullable();
|
||||
});
|
||||
return Migration::createTable(
|
||||
'config',
|
||||
function (Blueprint $table) {
|
||||
$table->string('key', 100)->primary();
|
||||
$table->binary('value')->nullable();
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->schema->drop('config');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@@ -8,35 +8,25 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Core\Migration;
|
||||
|
||||
use Flarum\Database\AbstractMigration;
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateDiscussionsTable extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->schema->create('discussions', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('title', 200);
|
||||
$table->integer('comments_count')->unsigned()->default(0);
|
||||
$table->integer('participants_count')->unsigned()->default(0);
|
||||
$table->integer('number_index')->unsigned()->default(0);
|
||||
return Migration::createTable(
|
||||
'discussions',
|
||||
function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('title', 200);
|
||||
$table->integer('comments_count')->unsigned()->default(0);
|
||||
$table->integer('participants_count')->unsigned()->default(0);
|
||||
$table->integer('number_index')->unsigned()->default(0);
|
||||
|
||||
$table->dateTime('start_time');
|
||||
$table->integer('start_user_id')->unsigned()->nullable();
|
||||
$table->integer('start_post_id')->unsigned()->nullable();
|
||||
$table->dateTime('start_time');
|
||||
$table->integer('start_user_id')->unsigned()->nullable();
|
||||
$table->integer('start_post_id')->unsigned()->nullable();
|
||||
|
||||
$table->dateTime('last_time')->nullable();
|
||||
$table->integer('last_user_id')->unsigned()->nullable();
|
||||
$table->integer('last_post_id')->unsigned()->nullable();
|
||||
$table->integer('last_post_number')->unsigned()->nullable();
|
||||
});
|
||||
$table->dateTime('last_time')->nullable();
|
||||
$table->integer('last_user_id')->unsigned()->nullable();
|
||||
$table->integer('last_post_id')->unsigned()->nullable();
|
||||
$table->integer('last_post_number')->unsigned()->nullable();
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->schema->drop('discussions');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@@ -8,25 +8,15 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Core\Migration;
|
||||
|
||||
use Flarum\Database\AbstractMigration;
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateEmailTokensTable extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->schema->create('email_tokens', function (Blueprint $table) {
|
||||
$table->string('id', 100)->primary();
|
||||
$table->string('email', 150);
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->timestamp('created_at');
|
||||
});
|
||||
return Migration::createTable(
|
||||
'email_tokens',
|
||||
function (Blueprint $table) {
|
||||
$table->string('id', 100)->primary();
|
||||
$table->string('email', 150);
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->timestamp('created_at');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->schema->drop('email_tokens');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@@ -8,26 +8,16 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Core\Migration;
|
||||
|
||||
use Flarum\Database\AbstractMigration;
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateGroupsTable extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->schema->create('groups', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('name_singular', 100);
|
||||
$table->string('name_plural', 100);
|
||||
$table->string('color', 20)->nullable();
|
||||
$table->string('icon', 100)->nullable();
|
||||
});
|
||||
return Migration::createTable(
|
||||
'groups',
|
||||
function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('name_singular', 100);
|
||||
$table->string('name_plural', 100);
|
||||
$table->string('color', 20)->nullable();
|
||||
$table->string('icon', 100)->nullable();
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->schema->drop('groups');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@@ -8,31 +8,21 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Core\Migration;
|
||||
|
||||
use Flarum\Database\AbstractMigration;
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateNotificationsTable extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->schema->create('notifications', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->integer('sender_id')->unsigned()->nullable();
|
||||
$table->string('type', 100);
|
||||
$table->string('subject_type', 200)->nullable();
|
||||
$table->integer('subject_id')->unsigned()->nullable();
|
||||
$table->binary('data')->nullable();
|
||||
$table->dateTime('time');
|
||||
$table->boolean('is_read')->default(0);
|
||||
$table->boolean('is_deleted')->default(0);
|
||||
});
|
||||
return Migration::createTable(
|
||||
'notifications',
|
||||
function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->integer('sender_id')->unsigned()->nullable();
|
||||
$table->string('type', 100);
|
||||
$table->string('subject_type', 200)->nullable();
|
||||
$table->integer('subject_id')->unsigned()->nullable();
|
||||
$table->binary('data')->nullable();
|
||||
$table->dateTime('time');
|
||||
$table->boolean('is_read')->default(0);
|
||||
$table->boolean('is_deleted')->default(0);
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->schema->drop('notifications');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@@ -8,24 +8,14 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Core\Migration;
|
||||
|
||||
use Flarum\Database\AbstractMigration;
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreatePasswordTokensTable extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->schema->create('password_tokens', function (Blueprint $table) {
|
||||
$table->string('id', 100)->primary();
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->timestamp('created_at');
|
||||
});
|
||||
return Migration::createTable(
|
||||
'password_tokens',
|
||||
function (Blueprint $table) {
|
||||
$table->string('id', 100)->primary();
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->timestamp('created_at');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->schema->drop('password_tokens');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@@ -8,24 +8,14 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Core\Migration;
|
||||
|
||||
use Flarum\Database\AbstractMigration;
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreatePermissionsTable extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->schema->create('permissions', function (Blueprint $table) {
|
||||
$table->integer('group_id')->unsigned();
|
||||
$table->string('permission', 100);
|
||||
$table->primary(['group_id', 'permission']);
|
||||
});
|
||||
return Migration::createTable(
|
||||
'permissions',
|
||||
function (Blueprint $table) {
|
||||
$table->integer('group_id')->unsigned();
|
||||
$table->string('permission', 100);
|
||||
$table->primary(['group_id', 'permission']);
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->schema->drop('permissions');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@@ -8,16 +8,14 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Core\Migration;
|
||||
|
||||
use Flarum\Database\AbstractMigration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
class CreatePostsTable extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->schema->create('posts', function (Blueprint $table) {
|
||||
// We need a full custom migration here, because we need to add the fulltext
|
||||
// index for the content with a raw SQL statement after creating the table.
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$schema->create('posts', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('discussion_id')->unsigned();
|
||||
$table->integer('number')->unsigned()->nullable();
|
||||
@@ -37,12 +35,11 @@ class CreatePostsTable extends AbstractMigration
|
||||
$table->engine = 'MyISAM';
|
||||
});
|
||||
|
||||
$prefix = $this->schema->getConnection()->getTablePrefix();
|
||||
$this->schema->getConnection()->statement('ALTER TABLE '.$prefix.'posts ADD FULLTEXT content (content)');
|
||||
}
|
||||
$prefix = $schema->getConnection()->getTablePrefix();
|
||||
$schema->getConnection()->statement('ALTER TABLE '.$prefix.'posts ADD FULLTEXT content (content)');
|
||||
},
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->schema->drop('posts');
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->drop('posts');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
@@ -8,26 +8,16 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Core\Migration;
|
||||
|
||||
use Flarum\Database\AbstractMigration;
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateUsersDiscussionsTable extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->schema->create('users_discussions', function (Blueprint $table) {
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->integer('discussion_id')->unsigned();
|
||||
$table->dateTime('read_time')->nullable();
|
||||
$table->integer('read_number')->unsigned()->nullable();
|
||||
$table->primary(['user_id', 'discussion_id']);
|
||||
});
|
||||
return Migration::createTable(
|
||||
'users_discussions',
|
||||
function (Blueprint $table) {
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->integer('discussion_id')->unsigned();
|
||||
$table->dateTime('read_time')->nullable();
|
||||
$table->integer('read_number')->unsigned()->nullable();
|
||||
$table->primary(['user_id', 'discussion_id']);
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->schema->drop('users_discussions');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@@ -8,24 +8,14 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Core\Migration;
|
||||
|
||||
use Flarum\Database\AbstractMigration;
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateUsersGroupsTable extends AbstractMigration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->schema->create('users_groups', function (Blueprint $table) {
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->integer('group_id')->unsigned();
|
||||
$table->primary(['user_id', 'group_id']);
|
||||
});
|
||||
return Migration::createTable(
|
||||
'users_groups',
|
||||
function (Blueprint $table) {
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->integer('group_id')->unsigned();
|
||||
$table->primary(['user_id', 'group_id']);
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->schema->drop('users_groups');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user