mirror of
https://github.com/flarum/core.git
synced 2025-08-16 21:34:08 +02:00
Compare commits
866 Commits
v0.1.0-bet
...
v0.1.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
5bcf72dd49 | ||
|
0536b208e1 | ||
|
c6aeeeb3c1 | ||
|
ce8a5b3e0f | ||
|
5faf0fcde5 | ||
|
65c0b436c0 | ||
|
8d76168bd4 | ||
|
d16f4dbefa | ||
|
e3e4786391 | ||
|
c1c7d4c73a | ||
|
8da8c9ac7d | ||
|
fb68aa88db | ||
|
afc597c189 | ||
|
4f3e67714e | ||
|
54be3ad3c8 | ||
|
0b00d56416 | ||
|
89d4a1e849 | ||
|
43ee7b59a4 | ||
|
d052f6b639 | ||
|
4b47adabcf | ||
|
93140b8fa4 | ||
|
ade2166310 | ||
|
a9969119d2 | ||
|
94a8eaec64 | ||
|
8ea13dc826 | ||
|
99d42372c3 | ||
|
01b56eecdb | ||
|
bcdcb8c20e | ||
|
d6c99eccdb | ||
|
01cb8ab79d | ||
|
877aed215b | ||
|
57570d960e | ||
|
04c4806f6f | ||
|
4c0339c30e | ||
|
e64dc4ea45 | ||
|
305076814f | ||
|
e31edd29d2 | ||
|
23b423c6ce | ||
|
1af1f472f9 | ||
|
7c86f7a34c | ||
|
14e49269d6 | ||
|
7837fff107 | ||
|
3dfa6bc8cb | ||
|
e47fe288fa | ||
|
03e30d7d4f | ||
|
9836ff6c54 | ||
|
bb1c655c90 | ||
|
bf20fe595a | ||
|
b5db57156b | ||
|
a7d3bdf244 | ||
|
c82f0bde61 | ||
|
986102c1d3 | ||
|
2140619c0b | ||
|
2f714a01ed | ||
|
231d018de5 | ||
|
5d62231004 | ||
|
123c3a93f5 | ||
|
c1eec2b261 | ||
|
60d3d6ef99 | ||
|
7862bd32dd | ||
|
92b555a246 | ||
|
687ec6a199 | ||
|
f788a0a972 | ||
|
e7bec9fe29 | ||
|
57da4e24cb | ||
|
7d1a22bcb5 | ||
|
8cc117d89d | ||
|
31ef02dc2c | ||
|
95c9ff9243 | ||
|
9718f54683 | ||
|
bb1e3278de | ||
|
bbcc33b5b5 | ||
|
30076547e5 | ||
|
7b710d5898 | ||
|
d02b5c9db7 | ||
|
cd70819fd5 | ||
|
20b4619e75 | ||
|
fdec2fd094 | ||
|
d7e4ae09b3 | ||
|
fcfc1b2a37 | ||
|
01eba18164 | ||
|
0dcf7d6aa9 | ||
|
015967a76c | ||
|
3cd59e12f5 | ||
|
26d07699e9 | ||
|
b7d6ba4893 | ||
|
d3753d94ae | ||
|
9349ed13fc | ||
|
91ace15f6d | ||
|
7c1b0bfcf2 | ||
|
542bae6277 | ||
|
275c14ee7f | ||
|
bccc970231 | ||
|
da6f79b34a | ||
|
a3cbec25db | ||
|
2225fdec72 | ||
|
6a532ec14e | ||
|
9416d16ebb | ||
|
d6857b0fe5 | ||
|
2c7e7f5b39 | ||
|
b5b18dd436 | ||
|
4778ae5f74 | ||
|
0936a630ef | ||
|
ec8ae6e03b | ||
|
9ffdeff608 | ||
|
8540932638 | ||
|
974f45e4e8 | ||
|
32ac48c6a9 | ||
|
af5b86806a | ||
|
aeef45b3cd | ||
|
8aa70de765 | ||
|
076a71c621 | ||
|
06c32b668d | ||
|
7af4b8d45f | ||
|
cbba325a87 | ||
|
b7d7e8b18a | ||
|
1031826a3d | ||
|
3612ca7aca | ||
|
c2ee84a115 | ||
|
060745ecb7 | ||
|
dd209b1747 | ||
|
aeb0a411b9 | ||
|
1ebb8bf39a | ||
|
fcdf36b3d0 | ||
|
ab912ba1ad | ||
|
4b8eb5d6e4 | ||
|
0e20949eb0 | ||
|
b2c691a03d | ||
|
dde0de046a | ||
|
7a9795fbc3 | ||
|
f30fac6a94 | ||
|
1fb8092987 | ||
|
ea6b943dbd | ||
|
b9918e6c40 | ||
|
b3e1a023c2 | ||
|
46bb66dd94 | ||
|
96926a180a | ||
|
e58ff71f93 | ||
|
2d5090ef12 | ||
|
f3bdc163fa | ||
|
0df6eee10f | ||
|
971b4c121c | ||
|
9bb7ca5d80 | ||
|
258a4b352d | ||
|
24580ced7a | ||
|
8e90d9f9e2 | ||
|
af36ef3fa9 | ||
|
eef63745e6 | ||
|
c702e911b3 | ||
|
73d2ee825b | ||
|
9f99610542 | ||
|
1192867c4f | ||
|
b048498b84 | ||
|
81f7a39a31 | ||
|
ea12bbaf48 | ||
|
c8122a7879 | ||
|
1a5d7a337d | ||
|
c29ea98d48 | ||
|
3702ffa998 | ||
|
58f9c22375 | ||
|
939a1e9ca8 | ||
|
736f22a31a | ||
|
34f3d93ce5 | ||
|
cba278611a | ||
|
f20b35080b | ||
|
df247925d4 | ||
|
44726633ce | ||
|
0d8c8c3be3 | ||
|
592dd6a927 | ||
|
882d22191f | ||
|
0d99f75a6d | ||
|
d5797dae79 | ||
|
82be1cea5d | ||
|
e1b3642453 | ||
|
7031ef7ef7 | ||
|
371e2ef759 | ||
|
203358a796 | ||
|
ff68c104a6 | ||
|
7b3ac18c14 | ||
|
1ced0456ca | ||
|
c0407ab016 | ||
|
2c5aa138cd | ||
|
53fd7b66b4 | ||
|
a1a22aa4ce | ||
|
cde5d20c4c | ||
|
05c9ce335e | ||
|
08aaba6426 | ||
|
2819fd63aa | ||
|
cc23430a9e | ||
|
1a2174d614 | ||
|
85bd82eab1 | ||
|
d06a834238 | ||
|
32aa3f0cba | ||
|
998bb5708e | ||
|
5f7291db39 | ||
|
f5988bae23 | ||
|
0b3cc0c18f | ||
|
6db27dff4f | ||
|
58d7be95c1 | ||
|
feffe53a86 | ||
|
01a6dccb83 | ||
|
786c2fcfa5 | ||
|
27556fea38 | ||
|
8600d81a5e | ||
|
1ce6afaaeb | ||
|
4bd05ee561 | ||
|
8328c446b0 | ||
|
31997b8fdf | ||
|
2d5a7ce064 | ||
|
a380424de4 | ||
|
c3dfa3560a | ||
|
40a78d302e | ||
|
7c0a72047a | ||
|
15adfc528f | ||
|
be08c32c96 | ||
|
a9199ad9d9 | ||
|
fd44db407c | ||
|
240aa9e83b | ||
|
1177880483 | ||
|
607b3a66ae | ||
|
96eda5cfeb | ||
|
0b0c1055d6 | ||
|
2b9ec71a81 | ||
|
6aa017659f | ||
|
f0f668fb93 | ||
|
b322cf669a | ||
|
8e99059f62 | ||
|
1b7a0ecb33 | ||
|
9bfb797fdc | ||
|
2525e3e7ad | ||
|
a14562b100 | ||
|
d3606b7f7e | ||
|
ac096926af | ||
|
909f52522b | ||
|
38c15c5a08 | ||
|
1f5764e5e3 | ||
|
28f72d5648 | ||
|
9f69b7b846 | ||
|
c05f8b732d | ||
|
58c9a6164a | ||
|
60e50713e7 | ||
|
513c586be3 | ||
|
9f8c2ed458 | ||
|
0cc75be55e | ||
|
fa7871cc16 | ||
|
a884a3592b | ||
|
9637d27b56 | ||
|
3866e518fa | ||
|
9d2d302f2d | ||
|
99dbea4524 | ||
|
77837ef7d1 | ||
|
3f9fe7d33e | ||
|
dd0dc44dd8 | ||
|
18ee8578e8 | ||
|
7256122a43 | ||
|
696f562b0e | ||
|
aca497e7be | ||
|
bc34b858a2 | ||
|
251862222c | ||
|
006ea02227 | ||
|
3c6429aba8 | ||
|
619561cf56 | ||
|
805f86b249 | ||
|
eba782d48f | ||
|
6d809cb023 | ||
|
77a5b59a10 | ||
|
7192c4391b | ||
|
3d812c287f | ||
|
7bd3fa82b1 | ||
|
6b108d99cb | ||
|
777579e146 | ||
|
d8b043dacb | ||
|
645a908dff | ||
|
bf79383204 | ||
|
db53103396 | ||
|
b5a9d0183e | ||
|
8065dc1806 | ||
|
694f5ad2e8 | ||
|
821bce38be | ||
|
45045a2ac1 | ||
|
3000ec695d | ||
|
323339190c | ||
|
5f60297eb1 | ||
|
af5f47bb90 | ||
|
02b110e545 | ||
|
f177c0d8a0 | ||
|
a12b5591c3 | ||
|
5293117c80 | ||
|
181c19eac7 | ||
|
f403feb3b1 | ||
|
b5fc7b9bf5 | ||
|
cd16adfa69 | ||
|
e11401b551 | ||
|
c9112624c0 | ||
|
603537c3d1 | ||
|
0f975da403 | ||
|
66a39bbbf5 | ||
|
6dd190114d | ||
|
01c0cf443b | ||
|
ffaafb92d4 | ||
|
8673a0bc2d | ||
|
b068536dbd | ||
|
4dc9e7741c | ||
|
6d5582e4ac | ||
|
e30f8f261b | ||
|
729103c519 | ||
|
ee8f4f04de | ||
|
8e35afe204 | ||
|
37d7f315d3 | ||
|
b799039c29 | ||
|
b74ca9979f | ||
|
895281acb2 | ||
|
42c9086c32 | ||
|
1fbce0db33 | ||
|
bd50a23966 | ||
|
210bbc800a | ||
|
f97ebfcbc0 | ||
|
9e79470603 | ||
|
328a244f92 | ||
|
d6c6e78193 | ||
|
bc11ec68dd | ||
|
9ae189bb9f | ||
|
179fcfb3ca | ||
|
51da153592 | ||
|
25d18d79fb | ||
|
eb76767e70 | ||
|
98c4883cfd | ||
|
dbbbc689bb | ||
|
16b229649a | ||
|
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 | ||
|
4734dbf46d | ||
|
783a14610a | ||
|
63d00e8b34 | ||
|
351d2d1366 | ||
|
e7b417121a | ||
|
9e3ecd528e | ||
|
3518fb2299 | ||
|
a6eff9383b | ||
|
d806c4491d | ||
|
c9a878d49c | ||
|
30856a8e2b | ||
|
3d3be6983a | ||
|
96b85f1330 | ||
|
25932cf7c4 | ||
|
d497782f65 | ||
|
98ccfdcee5 | ||
|
b4439dc6b3 | ||
|
72a2749943 | ||
|
f13ded1255 | ||
|
c719cc6d8a | ||
|
7dcb99621d | ||
|
4e047bae6a | ||
|
90def3f0db | ||
|
17619843b5 | ||
|
d46316e979 | ||
|
b44ffd9f8d | ||
|
953f81176b | ||
|
119d1721e0 | ||
|
7d4a04760c | ||
|
73c44adb96 | ||
|
eb571c5595 | ||
|
95e3ff8fa8 | ||
|
e1315d27a4 | ||
|
7127bea15e | ||
|
a3a5d0a351 | ||
|
aa7b4dd754 | ||
|
409a63d77a | ||
|
78f6249b24 | ||
|
2edda9baaa | ||
|
49fddbd450 | ||
|
1b3d674c39 | ||
|
400aa4fef9 | ||
|
a4ef9e7cf4 | ||
|
f230c72ebb | ||
|
f0883471ef | ||
|
5e2f659f54 | ||
|
bb250baddf | ||
|
68498cedae | ||
|
f3612261ec | ||
|
0a65d2bb0d | ||
|
59fa623f11 | ||
|
e95cb09caa | ||
|
9836fa64ed | ||
|
415b68f84f | ||
|
c0364cbc9d | ||
|
1cd6908dbb | ||
|
323ced8b00 | ||
|
ea98e4bda9 | ||
|
a471a44ca6 | ||
|
2903a7068c | ||
|
fc7db914db | ||
|
a7c2a7a2d3 | ||
|
2a5c0c1c7a | ||
|
14af6c0e8b | ||
|
c2f802878a | ||
|
b148c9d7da | ||
|
b23e821013 | ||
|
9aeaccf9a4 | ||
|
12830265d9 | ||
|
6d7b826133 | ||
|
9b0aa574f0 | ||
|
845daf1ab6 | ||
|
22ffb76cb5 | ||
|
067552efe5 | ||
|
659cfb72ad | ||
|
49d59089e4 | ||
|
a9eb62880e | ||
|
26a821e3e2 | ||
|
7490709af8 | ||
|
96c42ed337 | ||
|
5cd2d6a79f | ||
|
a3a64749c5 | ||
|
1242fa79af | ||
|
ddfedcb4dd | ||
|
43c44efe3d | ||
|
7e763ec22b | ||
|
e0b6aacc9e | ||
|
d8eed9d171 | ||
|
46ba8a3b8d | ||
|
c08b62af80 | ||
|
a23180f279 | ||
|
cc68c6f503 | ||
|
d0a188bc42 | ||
|
dfb9f23eee | ||
|
044d730480 | ||
|
e145873d59 | ||
|
7b49f3c24c | ||
|
4b3e1b16d9 | ||
|
cde8dd0dc4 | ||
|
bdf626b552 | ||
|
68a7886cec | ||
|
8fc43cac9e | ||
|
a3c11587b7 | ||
|
c7c2d9a755 | ||
|
b928cb523a | ||
|
60bdbe6e52 | ||
|
9772e398f6 | ||
|
b83c81c06e | ||
|
e3569d39cc | ||
|
33dd5fff36 | ||
|
2ae7392dea | ||
|
6df48b04c2 | ||
|
6f7cce5adf | ||
|
4c2ff6e82d | ||
|
b53e612007 | ||
|
208d90293d | ||
|
1c3fda4a71 | ||
|
663de42917 | ||
|
cde7dd3ce1 | ||
|
4580ebe100 | ||
|
60483b2c62 | ||
|
cf42765513 | ||
|
8e5b13903e | ||
|
7387dfb7da | ||
|
0b888ea342 | ||
|
6f1c46819e | ||
|
18def302d6 | ||
|
bddbf24055 | ||
|
0ce014b3bb | ||
|
c3cf5fe074 | ||
|
72a3582287 | ||
|
dd67291ce0 | ||
|
1a3e085a9c | ||
|
78cd35d93c | ||
|
4725ac4131 | ||
|
8c7cdb184f | ||
|
2223e1a13c | ||
|
296b822636 | ||
|
232f3b6bc6 | ||
|
0c065520e4 | ||
|
56e10ce6ba | ||
|
03f862fe8c | ||
|
b4cb5a11da | ||
|
ef2cc9b0cd | ||
|
2a17590412 | ||
|
e251cf34c4 | ||
|
0142b01cc5 | ||
|
8a5eb9cd42 | ||
|
89338290a4 | ||
|
58eaf79a98 | ||
|
f255d318ef | ||
|
9e91ada4a8 | ||
|
06de5c430b | ||
|
a590150698 | ||
|
0a66229169 | ||
|
4e5b3099f8 | ||
|
aa203de6e9 | ||
|
e0aa99fabb | ||
|
6463d912a9 | ||
|
b39a991940 | ||
|
0db4708ef9 | ||
|
5382d0ce1a | ||
|
295f29e53e | ||
|
ce094be83e | ||
|
f5b5d9ca5c | ||
|
040ce52724 | ||
|
56f9016ff7 | ||
|
1f7afb3e4a | ||
|
b179ca1c48 | ||
|
c3374197d1 | ||
|
9529ce9ba2 | ||
|
bac3fe84da | ||
|
a00226c05a | ||
|
7706714ad9 | ||
|
538a3e5e98 | ||
|
f1c40eeccc | ||
|
3efbffdcec | ||
|
02e40f7c47 | ||
|
26143272bd | ||
|
eabd8842ed | ||
|
4851596c78 | ||
|
de216af08d | ||
|
418b1b9bac | ||
|
68369ac5bb | ||
|
7404debb21 | ||
|
88372640aa | ||
|
fdb598187f | ||
|
753808c3f1 | ||
|
dbef2a4c1f | ||
|
35360b690c | ||
|
d2c4569112 | ||
|
b9bda2d443 | ||
|
b126055611 | ||
|
9b3b87e4db | ||
|
91fb24f7a3 | ||
|
393f2de146 | ||
|
6f47f4a86f | ||
|
4c6e03a692 | ||
|
c2ad1181b1 | ||
|
d5d7185794 | ||
|
a0267d9515 | ||
|
69a50565bb | ||
|
273461040c | ||
|
858feb5ac0 | ||
|
ee9862004d | ||
|
0b0f1bc142 | ||
|
153655f1f1 | ||
|
3020710959 | ||
|
db067c7d87 | ||
|
7a0299d246 | ||
|
5598e885b7 | ||
|
264725d872 | ||
|
c7ed189cf3 | ||
|
ab6e3351b4 | ||
|
8e19312534 | ||
|
ed602c6032 | ||
|
d6ed04ffce | ||
|
bd02e4307a | ||
|
3eafed0ae3 | ||
|
f591851cb2 | ||
|
f55d95c9b7 | ||
|
d610ea663f | ||
|
8ab0686666 | ||
|
8937050aed | ||
|
efca923d30 | ||
|
80665450fc | ||
|
4041c18014 | ||
|
514eec7466 | ||
|
8f387bbd52 | ||
|
c4dc1a5ee2 | ||
|
ca09e834b1 | ||
|
4752142c11 | ||
|
6582c5fcf0 | ||
|
6fff3cc0dc | ||
|
0b406a06a1 | ||
|
1fc369c59e | ||
|
f4a4ed8b49 | ||
|
dbd33f687c | ||
|
e038c5c9d9 | ||
|
974d301bed | ||
|
7fb582e8d7 | ||
|
633f84bbe5 | ||
|
84e670082b | ||
|
8b06a6c282 | ||
|
a2b43f6f78 | ||
|
66510d6887 | ||
|
9767bce1e3 | ||
|
ad060126ae | ||
|
ffcba1f173 | ||
|
c019ed6fb0 | ||
|
b0da51309e | ||
|
92437edd1b | ||
|
b695f4d063 | ||
|
8414a59908 | ||
|
64207a53c6 | ||
|
a9e001a4ce | ||
|
fc8dfd8893 | ||
|
701ad0a977 | ||
|
cd5f5515e2 | ||
|
3221e80014 | ||
|
d8c2cbc265 | ||
|
f6ad891850 | ||
|
e524c59f97 | ||
|
cac670e699 | ||
|
d9062ced96 | ||
|
90a3bff638 | ||
|
ddafefc354 | ||
|
fa265152c7 | ||
|
5c98a08e0f | ||
|
6beb4fe898 | ||
|
e54944d6c3 | ||
|
d39bca192e | ||
|
efff485d6c |
@@ -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
|
||||
}
|
||||
}
|
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -1,5 +1,11 @@
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
stubs/extension/.gitignore -export-ignore
|
||||
.gitmodules export-ignore
|
||||
.travis.yml export-ignore
|
||||
.editorconfig export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
||||
phpunit.xml export-ignore
|
||||
tests export-ignore
|
||||
|
||||
js/*/dist/*.js -diff
|
||||
|
3
.github/CONTRIBUTING.md
vendored
Normal file
3
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Contributing to Flarum
|
||||
|
||||
Howdy! We're really excited that you are interested in contributing to Flarum. Before submitting your contribution, please take a moment and read through the [Contributing Guidelines](https://github.com/flarum/flarum/blob/master/CONTRIBUTING.md).
|
26
.github/ISSUE_TEMPLATE.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
> Issues on Github are meant for bug reporting. Please post feature requests on the [discussion forum](https://discuss.flarum.org/t/features).
|
||||
---
|
||||
> Try to complete the below form as far as you are able and are willing to share. Add a screenshot of the issue if you can.
|
||||
|
||||
## Bug report
|
||||
- Version of Flarum: x.y.z
|
||||
- Website URL where the bug is visible: http://example.com
|
||||
- The webserver you are running: apache, nginx or something else
|
||||
- PHP version: x.y.z
|
||||
- Hosted environment: shared or vps
|
||||
- Hosting provider: http://some-amazing-provider.com
|
||||
|
||||
## Flarum info
|
||||
|
||||
```
|
||||
Output of "php flarum info", run this in terminal in your Flarum directory.
|
||||
```
|
||||
|
||||
## Additional comments
|
||||
Some additional information you'd like to share, eg what have you tried so far.
|
||||
|
||||
## Log files
|
||||
|
||||
```
|
||||
Put any relevant logs here.
|
||||
```
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ composer.phar
|
||||
Thumbs.db
|
||||
tests/_output/*
|
||||
.vagrant
|
||||
.idea/*
|
||||
|
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);
|
18
.styleci.yml
Normal file
18
.styleci.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
preset: recommended
|
||||
|
||||
enabled:
|
||||
- logical_not_operators_with_successor_space
|
||||
|
||||
disabled:
|
||||
- align_double_arrow
|
||||
- blank_line_after_opening_tag
|
||||
- multiline_array_trailing_comma
|
||||
- new_with_braces
|
||||
- phpdoc_align
|
||||
- phpdoc_order
|
||||
- phpdoc_separation
|
||||
- phpdoc_types
|
||||
|
||||
finder:
|
||||
exclude:
|
||||
- "stubs"
|
15
.travis.yml
15
.travis.yml
@@ -1,8 +1,10 @@
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 5.5
|
||||
- 5.6
|
||||
- 7.0
|
||||
- 7.1
|
||||
- hhvm
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
@@ -10,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 --coverage-clover=coverage.xml
|
||||
|
||||
notifications:
|
||||
email:
|
||||
@@ -27,4 +29,7 @@ notifications:
|
||||
on_failure: always
|
||||
on_start: false
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
sudo: false
|
||||
|
44
CHANGELOG.md
44
CHANGELOG.md
@@ -1,44 +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/).
|
||||
|
||||
## [Unreleased][unreleased]
|
||||
*nothing yet*
|
||||
|
||||
## [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
|
||||
|
||||
[unreleased]: https://github.com/flarum/core/compare/v0.1.0-beta.2...HEAD
|
||||
[0.1.0-beta.2]: https://github.com/flarum/core/compare/v0.1.0-beta...v0.1.0-beta.2
|
@@ -1,13 +0,0 @@
|
||||
# Contributing to Flarum
|
||||
|
||||
Thanks for your interest in contributing to Flarum! Please read the [Contributing docs](http://flarum.org/docs/contributing) to learn how you can help.
|
||||
|
||||
## Contributor License Agreement
|
||||
|
||||
By contributing your code to Flarum you grant Toby Zerner a non-exclusive, irrevocable, worldwide, royalty-free, sublicenseable, transferable license under all of Your relevant intellectual property rights (including copyright, patent, and any other rights), to use, copy, prepare derivative works of, distribute and publicly perform and display the Contributions on any licensing terms, including without limitation: (a) open source licenses like the MIT license; and (b) binary, proprietary, or commercial licenses. Except for the licenses granted herein, You reserve all right, title, and interest in and to the Contribution.
|
||||
|
||||
You confirm that you are able to grant us these rights. You represent that You are legally entitled to grant the above license. If Your employer has rights to intellectual property that You create, You represent that You have received permission to make the Contributions on behalf of that employer, or that Your employer has waived such rights for the Contributions.
|
||||
|
||||
You represent that the Contributions are Your original works of authorship, and to Your knowledge, no other person claims, or has the right to claim, any right in any invention or patent related to the Contributions. You also represent that You are not legally obligated, whether by entering into an agreement or otherwise, in any way that conflicts with the terms of this license.
|
||||
|
||||
Toby Zerner acknowledges that, except as explicitly described in this Agreement, any Contribution which you provide is on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
|
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2015 Toby Zerner
|
||||
Copyright (c) 2014-2017 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
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# Flarum Core
|
||||
|
||||
This repository contains the core code of Flarum. If you want to install Flarum, visit the [main Flarum repository](http://github.com/flarum/flarum).
|
||||
This repository contains Flarum's core code. If you want to set up a forum, visit the [main Flarum repository](http://github.com/flarum/flarum).
|
||||
|
||||
## Contributing
|
||||
|
||||
Interested in contributing to Flarum? Please read the [Contributing docs](http://flarum.org/docs/contributing) to learn how you can help.
|
||||
Flarum is open-source and we would love your help building it! Please read the [Contributing Guide](https://github.com/flarum/flarum/blob/master/CONTRIBUTING.md) to learn how you can help.
|
||||
|
@@ -1,20 +1,36 @@
|
||||
{
|
||||
"name": "flarum/core",
|
||||
"description": "",
|
||||
"description": "Delightfully simple forum software.",
|
||||
"keywords": ["forum", "discussion"],
|
||||
"homepage": "http://flarum.org",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Toby Zerner",
|
||||
"email": "toby@flarum.org"
|
||||
"email": "toby.zerner@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Franz Liedke",
|
||||
"email": "franz@develophp.org"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/flarum/core/issues",
|
||||
"source": "https://github.com/flarum/core",
|
||||
"docs": "http://flarum.org/docs"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.4.0",
|
||||
"php": ">=5.6.0",
|
||||
"dflydev/fig-cookies": "^1.0.2",
|
||||
"doctrine/dbal": "^2.5",
|
||||
"components/font-awesome": "^4.6",
|
||||
"franzl/whoops-middleware": "^0.4.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.*",
|
||||
@@ -22,22 +38,26 @@
|
||||
"illuminate/support": "5.1.*",
|
||||
"illuminate/validation": "5.1.*",
|
||||
"illuminate/view": "5.1.*",
|
||||
"league/flysystem": "^1.0.11",
|
||||
"tobscure/json-api": "^0.1.1",
|
||||
"oyejorge/less.php": "~1.5",
|
||||
"intervention/image": "^2.3.0",
|
||||
"s9e/text-formatter": "^0.3.2",
|
||||
"psr/http-message": "^1.0",
|
||||
"zendframework/zend-diactoros": "^1.1",
|
||||
"league/flysystem": "^1.0.11",
|
||||
"league/oauth2-client": "~1.0",
|
||||
"matthiasmullie/minify": "^1.3",
|
||||
"monolog/monolog": "^1.16.0",
|
||||
"nikic/fast-route": "^0.6",
|
||||
"dflydev/fig-cookies": "^1.0",
|
||||
"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",
|
||||
"doctrine/dbal": "^2.5"
|
||||
"s9e/text-formatter": "^0.8.1",
|
||||
"tobscure/json-api": "^0.3.0",
|
||||
"zendframework/zend-diactoros": "^1.1",
|
||||
"zendframework/zend-stratigility": "^1.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"squizlabs/php_codesniffer": "2.*",
|
||||
"phpspec/phpspec": "^2.2"
|
||||
"mockery/mockery": "^0.9.4",
|
||||
"phpunit/phpunit": "^4.8"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -47,8 +67,14 @@
|
||||
"src/helpers.php"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"test": "phpspec run",
|
||||
"style": "phpcs --standard=PSR2 -np src"
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "0.1.x-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2874
composer.lock
generated
2874
composer.lock
generated
File diff suppressed because it is too large
Load Diff
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>
|
13
error/404.html
Normal file
13
error/404.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>404 Not Found</h1>
|
||||
<p>Looks like this page could not be found.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
13
error/500.html
Normal file
13
error/500.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>500 Internal Server Error</h1>
|
||||
<p>Something went wrong on our server.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
13
error/503.html
Normal file
13
error/503.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>503 Service Unavailable</h1>
|
||||
<p>This forum is down for maintenance.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
2
js/admin/.gitignore
vendored
2
js/admin/.gitignore
vendored
@@ -1,3 +1 @@
|
||||
node_modules
|
||||
mithril.js
|
||||
dist
|
||||
|
@@ -1,15 +1,14 @@
|
||||
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',
|
||||
bowerDir + '/m.attrs.bidi/bidi.js',
|
||||
bowerDir + '/jquery/dist/jquery.js',
|
||||
bowerDir + '/moment/moment.js',
|
||||
|
||||
@@ -28,6 +27,5 @@ gulp({
|
||||
'../lib/**/*.js'
|
||||
]
|
||||
},
|
||||
externalHelpers: true,
|
||||
outputFile: 'dist/app.js'
|
||||
});
|
||||
|
23990
js/admin/dist/app.js
vendored
Normal file
23990
js/admin/dist/app.js
vendored
Normal file
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"
|
||||
}
|
||||
}
|
||||
|
@@ -14,4 +14,20 @@ app.initializers.add('boot', boot, -100);
|
||||
|
||||
app.extensionSettings = {};
|
||||
|
||||
app.getRequiredPermissions = function(permission) {
|
||||
const required = [];
|
||||
|
||||
if (permission === 'startDiscussion' || permission.indexOf('discussion.') === 0) {
|
||||
required.push('viewDiscussions');
|
||||
}
|
||||
if (permission === 'discussion.delete') {
|
||||
required.push('discussion.hide');
|
||||
}
|
||||
if (permission === 'discussion.deletePosts') {
|
||||
required.push('discussion.editPosts');
|
||||
}
|
||||
|
||||
return required;
|
||||
};
|
||||
|
||||
export default app;
|
||||
|
@@ -15,15 +15,15 @@ export default class AddExtensionModal extends Modal {
|
||||
}
|
||||
|
||||
title() {
|
||||
return 'Add Extension';
|
||||
return app.translator.trans('core.admin.add_extension.title');
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
@@ -35,36 +35,43 @@ export default class AdminNav extends Component {
|
||||
items.add('dashboard', AdminLinkButton.component({
|
||||
href: app.route('dashboard'),
|
||||
icon: 'bar-chart',
|
||||
children: 'Dashboard',
|
||||
description: 'Your forum at a glance.'
|
||||
children: app.translator.trans('core.admin.nav.dashboard_button'),
|
||||
description: app.translator.trans('core.admin.nav.dashboard_text')
|
||||
}));
|
||||
|
||||
items.add('basics', AdminLinkButton.component({
|
||||
href: app.route('basics'),
|
||||
icon: 'pencil',
|
||||
children: 'Basics',
|
||||
description: 'Set your forum title, language, and other basic settings.'
|
||||
children: app.translator.trans('core.admin.nav.basics_button'),
|
||||
description: app.translator.trans('core.admin.nav.basics_text')
|
||||
}));
|
||||
|
||||
items.add('mail', AdminLinkButton.component({
|
||||
href: app.route('mail'),
|
||||
icon: 'envelope',
|
||||
children: app.translator.trans('core.admin.nav.email_button'),
|
||||
description: app.translator.trans('core.admin.nav.email_text')
|
||||
}));
|
||||
|
||||
items.add('permissions', AdminLinkButton.component({
|
||||
href: app.route('permissions'),
|
||||
icon: 'key',
|
||||
children: 'Permissions',
|
||||
description: 'Configure who can see and do what.'
|
||||
children: app.translator.trans('core.admin.nav.permissions_button'),
|
||||
description: app.translator.trans('core.admin.nav.permissions_text')
|
||||
}));
|
||||
|
||||
items.add('appearance', AdminLinkButton.component({
|
||||
href: app.route('appearance'),
|
||||
icon: 'paint-brush',
|
||||
children: 'Appearance',
|
||||
description: 'Customize your forum\'s colors, logos, and other variables.'
|
||||
children: app.translator.trans('core.admin.nav.appearance_button'),
|
||||
description: app.translator.trans('core.admin.nav.appearance_text')
|
||||
}));
|
||||
|
||||
items.add('extensions', AdminLinkButton.component({
|
||||
href: app.route('extensions'),
|
||||
icon: 'puzzle-piece',
|
||||
children: 'Extensions',
|
||||
description: 'Add extra functionality to your forum and make it your own.'
|
||||
children: app.translator.trans('core.admin.nav.extensions_button'),
|
||||
description: app.translator.trans('core.admin.nav.extensions_text')
|
||||
}));
|
||||
|
||||
return items;
|
||||
|
@@ -1,17 +1,19 @@
|
||||
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 saveConfig from 'flarum/utils/saveConfig';
|
||||
import EditCustomHeaderModal from 'flarum/components/EditCustomHeaderModal';
|
||||
import UploadImageButton from 'flarum/components/UploadImageButton';
|
||||
import saveSettings from 'flarum/utils/saveSettings';
|
||||
|
||||
export default class AppearancePage extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
export default class AppearancePage extends Page {
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.primaryColor = m.prop(app.config.theme_primary_color);
|
||||
this.secondaryColor = m.prop(app.config.theme_secondary_color);
|
||||
this.darkMode = m.prop(app.config.theme_dark_mode === '1');
|
||||
this.coloredHeader = m.prop(app.config.theme_colored_header === '1');
|
||||
this.primaryColor = m.prop(app.data.settings.theme_primary_color);
|
||||
this.secondaryColor = m.prop(app.data.settings.theme_secondary_color);
|
||||
this.darkMode = m.prop(app.data.settings.theme_dark_mode === '1');
|
||||
this.coloredHeader = m.prop(app.data.settings.theme_colored_header === '1');
|
||||
}
|
||||
|
||||
view() {
|
||||
@@ -20,44 +22,73 @@ export default class AppearancePage extends Component {
|
||||
<div className="container">
|
||||
<form onsubmit={this.onsubmit.bind(this)}>
|
||||
<fieldset className="AppearancePage-colors">
|
||||
<legend>Colors</legend>
|
||||
<legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
|
||||
<div className="helpText">
|
||||
Choose two colors to theme your forum with. The first will be used as a highlight color, while the second will be used to style background elements.
|
||||
{app.translator.trans('core.admin.appearance.colors_text')}
|
||||
</div>
|
||||
|
||||
<div className="AppearancePage-colors-input">
|
||||
<input className="FormControl" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/>
|
||||
<input className="FormControl" placeholder="#aaaaaa" value={this.secondaryColor()} onchange={m.withAttr('value', this.secondaryColor)}/>
|
||||
<input className="FormControl" type="color" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/>
|
||||
<input className="FormControl" type="color" placeholder="#aaaaaa" value={this.secondaryColor()} onchange={m.withAttr('value', this.secondaryColor)}/>
|
||||
</div>
|
||||
|
||||
{Switch.component({
|
||||
state: this.darkMode(),
|
||||
children: 'Dark Mode',
|
||||
children: app.translator.trans('core.admin.appearance.dark_mode_label'),
|
||||
onchange: this.darkMode
|
||||
})}
|
||||
|
||||
{Switch.component({
|
||||
state: this.coloredHeader(),
|
||||
children: 'Colored Header',
|
||||
children: app.translator.trans('core.admin.appearance.colored_header_label'),
|
||||
onchange: this.coloredHeader
|
||||
})}
|
||||
|
||||
{Button.component({
|
||||
className: 'Button Button--primary',
|
||||
children: 'Save Changes',
|
||||
type: 'submit',
|
||||
children: app.translator.trans('core.admin.appearance.submit_button'),
|
||||
loading: this.loading
|
||||
})}
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<fieldset>
|
||||
<legend>Custom Styles</legend>
|
||||
<legend>{app.translator.trans('core.admin.appearance.logo_heading')}</legend>
|
||||
<div className="helpText">
|
||||
Customize your forum's appearance by adding your own LESS/CSS code to be applied on top of Flarum's default styles.
|
||||
{app.translator.trans('core.admin.appearance.logo_text')}
|
||||
</div>
|
||||
<UploadImageButton name="logo"/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>{app.translator.trans('core.admin.appearance.favicon_heading')}</legend>
|
||||
<div className="helpText">
|
||||
{app.translator.trans('core.admin.appearance.favicon_text')}
|
||||
</div>
|
||||
<UploadImageButton name="favicon"/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>{app.translator.trans('core.admin.appearance.custom_header_heading')}</legend>
|
||||
<div className="helpText">
|
||||
{app.translator.trans('core.admin.appearance.custom_header_text')}
|
||||
</div>
|
||||
{Button.component({
|
||||
className: 'Button',
|
||||
children: 'Edit Custom CSS',
|
||||
children: app.translator.trans('core.admin.appearance.edit_header_button'),
|
||||
onclick: () => app.modal.show(new EditCustomHeaderModal())
|
||||
})}
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
|
||||
<div className="helpText">
|
||||
{app.translator.trans('core.admin.appearance.custom_styles_text')}
|
||||
</div>
|
||||
{Button.component({
|
||||
className: 'Button',
|
||||
children: app.translator.trans('core.admin.appearance.edit_css_button'),
|
||||
onclick: () => app.modal.show(new EditCustomCssModal())
|
||||
})}
|
||||
</fieldset>
|
||||
@@ -72,13 +103,13 @@ export default class AppearancePage extends Component {
|
||||
const hex = /^#[0-9a-f]{3}([0-9a-f]{3})?$/i;
|
||||
|
||||
if (!hex.test(this.primaryColor()) || !hex.test(this.secondaryColor())) {
|
||||
alert('Please enter a hexadecimal color code.');
|
||||
alert(app.translator.trans('core.admin.appearance.enter_hex_message'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
saveConfig({
|
||||
saveSettings({
|
||||
theme_primary_color: this.primaryColor(),
|
||||
theme_secondary_color: this.secondaryColor(),
|
||||
theme_dark_mode: this.darkMode(),
|
||||
|
@@ -1,13 +1,15 @@
|
||||
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';
|
||||
import Alert from 'flarum/components/Alert';
|
||||
import saveConfig from 'flarum/utils/saveConfig';
|
||||
import saveSettings from 'flarum/utils/saveSettings';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import Switch from 'flarum/components/Switch';
|
||||
|
||||
export default class BasicsPage extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
export default class BasicsPage extends Page {
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.loading = false;
|
||||
|
||||
@@ -15,20 +17,23 @@ export default class BasicsPage extends Component {
|
||||
'forum_title',
|
||||
'forum_description',
|
||||
'default_locale',
|
||||
'show_language_selector',
|
||||
'default_route',
|
||||
'welcome_title',
|
||||
'welcome_message'
|
||||
];
|
||||
this.values = {};
|
||||
|
||||
const config = app.config;
|
||||
this.fields.forEach(key => this.values[key] = m.prop(config[key]));
|
||||
const settings = app.data.settings;
|
||||
this.fields.forEach(key => this.values[key] = m.prop(settings[key] || false));
|
||||
|
||||
this.localeOptions = {};
|
||||
const locales = app.locales;
|
||||
const locales = app.data.locales;
|
||||
for (const i in locales) {
|
||||
this.localeOptions[i] = `${locales[i]} (${i})`;
|
||||
}
|
||||
|
||||
if (typeof this.values.show_language_selector() !== "number") this.values.show_language_selector(1);
|
||||
}
|
||||
|
||||
view() {
|
||||
@@ -37,17 +42,17 @@ export default class BasicsPage extends Component {
|
||||
<div className="container">
|
||||
<form onsubmit={this.onsubmit.bind(this)}>
|
||||
{FieldSet.component({
|
||||
label: 'Forum Title',
|
||||
label: app.translator.trans('core.admin.basics.forum_title_heading'),
|
||||
children: [
|
||||
<input className="FormControl" value={this.values.forum_title()} oninput={m.withAttr('value', this.values.forum_title)}/>
|
||||
]
|
||||
})}
|
||||
|
||||
{FieldSet.component({
|
||||
label: 'Forum Description',
|
||||
label: app.translator.trans('core.admin.basics.forum_description_heading'),
|
||||
children: [
|
||||
<div className="helpText">
|
||||
Enter a short sentence or two that describes your community. This will appear in the meta tag and show up in search engines.
|
||||
{app.translator.trans('core.admin.basics.forum_description_text')}
|
||||
</div>,
|
||||
<textarea className="FormControl" value={this.values.forum_description()} oninput={m.withAttr('value', this.values.forum_description)}/>
|
||||
]
|
||||
@@ -55,44 +60,44 @@ export default class BasicsPage extends Component {
|
||||
|
||||
{Object.keys(this.localeOptions).length > 1
|
||||
? FieldSet.component({
|
||||
label: 'Default Language',
|
||||
label: app.translator.trans('core.admin.basics.default_language_heading'),
|
||||
children: [
|
||||
Select.component({
|
||||
options: this.localeOptions,
|
||||
value: this.values.default_locale(),
|
||||
onchange: this.values.default_locale
|
||||
}),
|
||||
Switch.component({
|
||||
state: this.values.show_language_selector(),
|
||||
onchange: this.values.show_language_selector,
|
||||
children: app.translator.trans('core.admin.basics.show_language_selector_label'),
|
||||
})
|
||||
]
|
||||
})
|
||||
: ''}
|
||||
|
||||
{FieldSet.component({
|
||||
label: 'Home Page',
|
||||
label: app.translator.trans('core.admin.basics.home_page_heading'),
|
||||
className: 'BasicsPage-homePage',
|
||||
children: [
|
||||
<div className="helpText">
|
||||
Choose the page which users will first see when they visit your forum. If entering a custom value, use the path relative to the forum root.
|
||||
{app.translator.trans('core.admin.basics.home_page_text')}
|
||||
</div>,
|
||||
<label className="checkbox">
|
||||
<input type="radio" name="homePage" value="/all" checked={this.values.default_route() === '/all'} onclick={m.withAttr('value', this.values.default_route)}/>
|
||||
All Discussions
|
||||
</label>,
|
||||
<label className="checkbox">
|
||||
<input type="radio" name="homePage" value="custom" checked={this.values.default_route() !== '/all'} onclick={() => {
|
||||
this.values.default_route('');
|
||||
m.redraw(true);
|
||||
this.$('.BasicsPage-homePage input').select();
|
||||
}}/>
|
||||
Custom <input className="FormControl" value={this.values.default_route()} oninput={m.withAttr('value', this.values.default_route)} style={this.values.default_route() !== '/all' ? 'margin-top: 5px' : 'display:none'}/>
|
||||
</label>
|
||||
this.homePageItems().toArray().map(({path, label}) =>
|
||||
<label className="checkbox">
|
||||
<input type="radio" name="homePage" value={path} checked={this.values.default_route() === path} onclick={m.withAttr('value', this.values.default_route)}/>
|
||||
{label}
|
||||
</label>
|
||||
)
|
||||
]
|
||||
})}
|
||||
|
||||
{FieldSet.component({
|
||||
label: 'Welcome Banner',
|
||||
label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
|
||||
className: 'BasicsPage-welcomeBanner',
|
||||
children: [
|
||||
<div className="helpText">
|
||||
Configure the text that displays in the banner on the All Discussions page. Use this to welcome guests to your forum.
|
||||
{app.translator.trans('core.admin.basics.welcome_banner_text')}
|
||||
</div>,
|
||||
<div className="BasicsPage-welcomeBanner-input">
|
||||
<input className="FormControl" value={this.values.welcome_title()} oninput={m.withAttr('value', this.values.welcome_title)}/>
|
||||
@@ -104,7 +109,7 @@ export default class BasicsPage extends Component {
|
||||
{Button.component({
|
||||
type: 'submit',
|
||||
className: 'Button Button--primary',
|
||||
children: 'Save Changes',
|
||||
children: app.translator.trans('core.admin.basics.submit_button'),
|
||||
loading: this.loading,
|
||||
disabled: !this.changed()
|
||||
})}
|
||||
@@ -115,9 +120,25 @@ export default class BasicsPage extends Component {
|
||||
}
|
||||
|
||||
changed() {
|
||||
const config = app.config;
|
||||
return this.fields.some(key => this.values[key]() !== app.data.settings[key]);
|
||||
}
|
||||
|
||||
return this.fields.some(key => this.values[key]() !== config[key]);
|
||||
/**
|
||||
* Build a list of options for the default homepage. Each option must be an
|
||||
* object with `path` and `label` properties.
|
||||
*
|
||||
* @return {ItemList}
|
||||
* @public
|
||||
*/
|
||||
homePageItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('allDiscussions', {
|
||||
path: '/all',
|
||||
label: app.translator.trans('core.admin.basics.all_discussions_label')
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
@@ -128,15 +149,16 @@ export default class BasicsPage extends Component {
|
||||
this.loading = true;
|
||||
app.alerts.dismiss(this.successAlert);
|
||||
|
||||
const config = {};
|
||||
const settings = {};
|
||||
|
||||
this.fields.forEach(key => config[key] = this.values[key]());
|
||||
this.fields.forEach(key => settings[key] = this.values[key]());
|
||||
|
||||
saveConfig(config)
|
||||
saveSettings(settings)
|
||||
.then(() => {
|
||||
app.alerts.show(this.successAlert = new Alert({type: 'success', children: 'Your changes were saved.'}));
|
||||
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,18 +1,19 @@
|
||||
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>
|
||||
<p>Thanks for trying out Flarum! You are running version <strong>{app.forum.attribute('version')}</strong>. This is beta software, and should not be used in production.</p>
|
||||
<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>
|
||||
<li>Having problems? Read the <a href="http://flarum.org/docs/troubleshooting" target="_blank">Troubleshooting docs</a>.</li>
|
||||
<li>Found a bug? Please <a href="https://github.com/flarum/core/issues" target="_blank">report it on GitHub</a>.</li>
|
||||
<li>Got some feedback? Let us know what you think on the <a href="http://discuss.flarum.org" target="_blank">Support Forum</a>.</li>
|
||||
<li>Want to contribute? Read the <a href="http://flarum.org/docs/contributing" target="_blank">Contributing docs</a>.</li>
|
||||
<li>Interested in developing extensions? Read the <a href="http://flarum.org/docs/extend" target="_blank">Extension docs</a>.</li>
|
||||
<li>{app.translator.trans('core.admin.dashboard.contributing_text', {a: <a href="http://flarum.org/docs/contributing" target="_blank"/>})}</li>
|
||||
<li>{app.translator.trans('core.admin.dashboard.troubleshooting_text', {a: <a href="http://flarum.org/docs/troubleshooting" target="_blank"/>})}</li>
|
||||
<li>{app.translator.trans('core.admin.dashboard.support_text', {a: <a href="http://discuss.flarum.org/t/support" target="_blank"/>})}</li>
|
||||
<li>{app.translator.trans('core.admin.dashboard.features_text', {a: <a href="http://discuss.flarum.org/t/features" target="_blank"/>})}</li>
|
||||
<li>{app.translator.trans('core.admin.dashboard.extension_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,51 +1,24 @@
|
||||
import Modal from 'flarum/components/Modal';
|
||||
import Button from 'flarum/components/Button';
|
||||
import saveConfig from 'flarum/utils/saveConfig';
|
||||
|
||||
export default class EditCustomCssModal extends Modal {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.customLess = m.prop(app.config.custom_less || '');
|
||||
}
|
||||
import SettingsModal from 'flarum/components/SettingsModal';
|
||||
|
||||
export default class EditCustomCssModal extends SettingsModal {
|
||||
className() {
|
||||
return 'EditCustomCssModal Modal--large';
|
||||
}
|
||||
|
||||
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="">Read the documentation</a> for more information.</p>
|
||||
|
||||
<div className="Form">
|
||||
<div className="Form-group">
|
||||
<textarea className="FormControl" rows="30" value={this.customLess()} onchange={m.withAttr('value', this.customLess)}/>
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary',
|
||||
children: 'Save Changes',
|
||||
loading: this.loading
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
form() {
|
||||
return [
|
||||
<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-group">
|
||||
<textarea className="FormControl" rows="30" bidi={this.setting('custom_less')}/>
|
||||
</div>
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
saveConfig({
|
||||
custom_less: this.customLess()
|
||||
}).then(() => window.location.reload());
|
||||
onsaved() {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
24
js/admin/src/components/EditCustomHeaderModal.js
Normal file
24
js/admin/src/components/EditCustomHeaderModal.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import SettingsModal from 'flarum/components/SettingsModal';
|
||||
|
||||
export default class EditCustomHeaderModal extends SettingsModal {
|
||||
className() {
|
||||
return 'EditCustomHeaderModal Modal--large';
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.translator.trans('core.admin.edit_header.title');
|
||||
}
|
||||
|
||||
form() {
|
||||
return [
|
||||
<p>{app.translator.trans('core.admin.edit_header.customize_text')}</p>,
|
||||
<div className="Form-group">
|
||||
<textarea className="FormControl" rows="30" bidi={this.setting('custom_header')}/>
|
||||
</div>
|
||||
];
|
||||
}
|
||||
|
||||
onsaved() {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
@@ -8,9 +8,7 @@ import Group from 'flarum/models/Group';
|
||||
* to create or edit a group.
|
||||
*/
|
||||
export default class EditGroupModal extends Modal {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
init() {
|
||||
this.group = this.props.group || app.store.createRecord('groups');
|
||||
|
||||
this.nameSingular = m.prop(this.group.nameSingular() || '');
|
||||
@@ -30,7 +28,7 @@ export default class EditGroupModal extends Modal {
|
||||
style: {backgroundColor: this.color()}
|
||||
}) : '',
|
||||
' ',
|
||||
this.namePlural() || 'Create Group'
|
||||
this.namePlural() || app.translator.trans('core.admin.edit_group.title')
|
||||
];
|
||||
}
|
||||
|
||||
@@ -39,22 +37,22 @@ export default class EditGroupModal extends Modal {
|
||||
<div className="Modal-body">
|
||||
<div className="Form">
|
||||
<div className="Form-group">
|
||||
<label>Name</label>
|
||||
<label>{app.translator.trans('core.admin.edit_group.name_label')}</label>
|
||||
<div className="EditGroupModal-name-input">
|
||||
<input className="FormControl" placeholder="Singular (e.g. Mod)" value={this.nameSingular()} oninput={m.withAttr('value', this.nameSingular)}/>
|
||||
<input className="FormControl" placeholder="Plural (e.g. Mods)" value={this.namePlural()} oninput={m.withAttr('value', this.namePlural)}/>
|
||||
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.singular_placeholder')} value={this.nameSingular()} oninput={m.withAttr('value', this.nameSingular)}/>
|
||||
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.plural_placeholder')} value={this.namePlural()} oninput={m.withAttr('value', this.namePlural)}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
<label>Color</label>
|
||||
<label>{app.translator.trans('core.admin.edit_group.color_label')}</label>
|
||||
<input className="FormControl" placeholder="#aaaaaa" value={this.color()} oninput={m.withAttr('value', this.color)}/>
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
<label>Icon</label>
|
||||
<label>{app.translator.trans('core.admin.edit_group.icon_label')}</label>
|
||||
<div className="helpText">
|
||||
Enter the name of any <a href="http://fortawesome.github.io/Font-Awesome/icons/" tabindex="-1">FontAwesome</a> icon class, <em>without</em> the <code>fa-</code> prefix.
|
||||
{app.translator.trans('core.admin.edit_group.icon_text', {a: <a href="http://fortawesome.github.io/Font-Awesome/icons/" tabindex="-1"/>})}
|
||||
</div>
|
||||
<input className="FormControl" placeholder="bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)}/>
|
||||
</div>
|
||||
@@ -64,11 +62,11 @@ export default class EditGroupModal extends Modal {
|
||||
type: 'submit',
|
||||
className: 'Button Button--primary EditGroupModal-save',
|
||||
loading: this.loading,
|
||||
children: 'Save Changes'
|
||||
children: app.translator.trans('core.admin.edit_group.submit_button')
|
||||
})}
|
||||
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
|
||||
<button type="button" className="Button EditGroupModal-delete" onclick={this.delete.bind(this)}>
|
||||
Delete Group
|
||||
<button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
|
||||
{app.translator.trans('core.admin.edit_group.delete_button')}
|
||||
</button>
|
||||
) : ''}
|
||||
</div>
|
||||
@@ -87,17 +85,16 @@ export default class EditGroupModal extends Modal {
|
||||
namePlural: this.namePlural(),
|
||||
color: this.color(),
|
||||
icon: this.icon()
|
||||
}).then(
|
||||
() => this.hide(),
|
||||
() => {
|
||||
}, {errorHandler: this.onerror.bind(this)})
|
||||
.then(this.hide.bind(this))
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
if (confirm('Are you sure you want to delete this group? The group members will NOT be deleted.')) {
|
||||
deleteGroup() {
|
||||
if (confirm(app.translator.trans('core.admin.edit_group.delete_confirmation'))) {
|
||||
this.group.delete().then(() => m.redraw());
|
||||
this.hide();
|
||||
}
|
||||
|
@@ -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';
|
||||
@@ -7,15 +7,16 @@ import AddExtensionModal from 'flarum/components/AddExtensionModal';
|
||||
import LoadingModal from 'flarum/components/LoadingModal';
|
||||
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() {
|
||||
return (
|
||||
<div className="ExtensionsPage">
|
||||
<div className="ExtensionsPage-header">
|
||||
<div className="container">
|
||||
{Button.component({
|
||||
children: 'Add Extension',
|
||||
children: app.translator.trans('core.admin.extensions.add_button'),
|
||||
icon: 'plus',
|
||||
className: 'Button Button--primary',
|
||||
onclick: () => app.modal.show(new AddExtensionModal())
|
||||
@@ -26,29 +27,33 @@ export default class ExtensionsPage extends Component {
|
||||
<div className="ExtensionsPage-list">
|
||||
<div className="container">
|
||||
<ul className="ExtensionList">
|
||||
{app.extensions
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(extension => (
|
||||
<li className={'ExtensionListItem ' + (!this.isEnabled(extension.name) ? 'disabled' : '')}>
|
||||
{Dropdown.component({
|
||||
icon: 'ellipsis-v',
|
||||
children: this.controlItems(extension).toArray(),
|
||||
className: 'ExtensionListItem-controls',
|
||||
buttonClassName: 'Button Button--icon Button--flat',
|
||||
menuClassName: 'Dropdown-menu--right'
|
||||
})}
|
||||
{Object.keys(app.data.extensions)
|
||||
.map(id => {
|
||||
const extension = app.data.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.icon}>
|
||||
{extension.icon ? icon(extension.icon.name) : ''}
|
||||
</span>
|
||||
<h4 className="ExtensionListItem-title">
|
||||
{extension.title}{' '}
|
||||
<small className="ExtensionListItem-version">{extension.version}</small>
|
||||
</h4>
|
||||
<div className="ExtensionListItem-description">{extension.description}</div>
|
||||
{controls.length ? (
|
||||
<Dropdown
|
||||
className="ExtensionListItem-controls"
|
||||
buttonClassName="Button Button--icon Button--flat"
|
||||
menuClassName="Dropdown-menu--right"
|
||||
icon="ellipsis-h">
|
||||
{controls}
|
||||
</Dropdown>
|
||||
) : ''}
|
||||
<label className="ExtensionListItem-title">
|
||||
<input type="checkbox" checked={this.isEnabled(extension.id)} onclick={this.toggle.bind(this, extension.id)}/> {' '}
|
||||
{extension.extra['flarum-extension'].title}
|
||||
</label>
|
||||
<div className="ExtensionListItem-version">{extension.version}</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</li>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,40 +61,26 @@ export default class ExtensionsPage extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
controlItems(extension) {
|
||||
controlItems(name) {
|
||||
const items = new ItemList();
|
||||
const enabled = this.isEnabled(extension.name);
|
||||
const enabled = this.isEnabled(name);
|
||||
|
||||
if (app.extensionSettings[extension.name]) {
|
||||
if (app.extensionSettings[name]) {
|
||||
items.add('settings', Button.component({
|
||||
icon: 'cog',
|
||||
children: 'Settings',
|
||||
onclick: app.extensionSettings[extension.name]
|
||||
children: app.translator.trans('core.admin.extensions.settings_button'),
|
||||
onclick: app.extensionSettings[name]
|
||||
}));
|
||||
}
|
||||
|
||||
items.add('toggle', Button.component({
|
||||
icon: 'power-off',
|
||||
children: enabled ? 'Disable' : 'Enable',
|
||||
onclick: () => {
|
||||
app.request({
|
||||
url: app.forum.attribute('apiUrl') + '/extensions/' + extension.name,
|
||||
method: 'PATCH',
|
||||
data: {enabled: !enabled}
|
||||
}).then(() => window.location.reload());
|
||||
|
||||
app.modal.show(new LoadingModal());
|
||||
}
|
||||
}));
|
||||
|
||||
if (!enabled) {
|
||||
items.add('uninstall', Button.component({
|
||||
icon: 'trash-o',
|
||||
children: 'Uninstall',
|
||||
children: app.translator.trans('core.admin.extensions.uninstall_button'),
|
||||
onclick: () => {
|
||||
app.request({
|
||||
url: app.forum.attribute('apiUrl') + '/extensions/' + extension.name,
|
||||
method: 'DELETE',
|
||||
url: app.forum.attribute('apiUrl') + '/extensions/' + name,
|
||||
method: 'DELETE'
|
||||
}).then(() => window.location.reload());
|
||||
|
||||
app.modal.show(new LoadingModal());
|
||||
@@ -97,19 +88,27 @@ export default class ExtensionsPage extends Component {
|
||||
}));
|
||||
}
|
||||
|
||||
// items.add('separator2', Separator.component());
|
||||
|
||||
// items.add('support', LinkButton.component({
|
||||
// icon: 'support',
|
||||
// children: 'Support'
|
||||
// }));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
isEnabled(name) {
|
||||
const enabled = JSON.parse(app.config.extensions_enabled);
|
||||
const enabled = JSON.parse(app.data.settings.extensions_enabled);
|
||||
|
||||
return enabled.indexOf(name) !== -1;
|
||||
}
|
||||
|
||||
toggle(id) {
|
||||
const enabled = this.isEnabled(id);
|
||||
|
||||
app.request({
|
||||
url: app.forum.attribute('apiUrl') + '/extensions/' + id,
|
||||
method: 'PATCH',
|
||||
data: {enabled: !enabled}
|
||||
}).then(() => {
|
||||
if (!enabled) localStorage.setItem('enabledExtension', id);
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
app.modal.show(new LoadingModal());
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,13 @@ export default class HeaderPrimary extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
// Since this component is 'above' the content of the page (that is, it is a
|
||||
// part of the global UI that persists between routes), we will flag the DOM
|
||||
// to be retained across route changes.
|
||||
context.retain = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the controls.
|
||||
*
|
||||
|
@@ -15,6 +15,13 @@ export default class HeaderSecondary extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
// Since this component is 'above' the content of the page (that is, it is a
|
||||
// part of the global UI that persists between routes), we will flag the DOM
|
||||
// to be retained across route changes.
|
||||
context.retain = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the controls.
|
||||
*
|
||||
|
@@ -10,7 +10,7 @@ export default class LoadingModal extends Modal {
|
||||
}
|
||||
|
||||
title() {
|
||||
return 'Please Wait...';
|
||||
return app.translator.trans('core.admin.loading.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
|
124
js/admin/src/components/MailPage.js
Normal file
124
js/admin/src/components/MailPage.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import Page from 'flarum/components/Page';
|
||||
import FieldSet from 'flarum/components/FieldSet';
|
||||
import Button from 'flarum/components/Button';
|
||||
import Alert from 'flarum/components/Alert';
|
||||
import saveSettings from 'flarum/utils/saveSettings';
|
||||
|
||||
export default class MailPage extends Page {
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.loading = false;
|
||||
|
||||
this.fields = [
|
||||
'mail_driver',
|
||||
'mail_host',
|
||||
'mail_from',
|
||||
'mail_port',
|
||||
'mail_username',
|
||||
'mail_password',
|
||||
'mail_encryption'
|
||||
];
|
||||
this.values = {};
|
||||
|
||||
const settings = app.data.settings;
|
||||
this.fields.forEach(key => this.values[key] = m.prop(settings[key]));
|
||||
|
||||
this.localeOptions = {};
|
||||
const locales = app.locales;
|
||||
for (const i in locales) {
|
||||
this.localeOptions[i] = `${locales[i]} (${i})`;
|
||||
}
|
||||
}
|
||||
|
||||
view() {
|
||||
return (
|
||||
<div className="MailPage">
|
||||
<div className="container">
|
||||
<form onsubmit={this.onsubmit.bind(this)}>
|
||||
<h2>{app.translator.trans('core.admin.email.heading')}</h2>
|
||||
<div className="helpText">
|
||||
{app.translator.trans('core.admin.email.text')}
|
||||
</div>
|
||||
|
||||
{FieldSet.component({
|
||||
label: app.translator.trans('core.admin.email.server_heading'),
|
||||
className: 'MailPage-MailSettings',
|
||||
children: [
|
||||
<div className="MailPage-MailSettings-input">
|
||||
<label>{app.translator.trans('core.admin.email.driver_label')}</label>
|
||||
<input className="FormControl" value={this.values.mail_driver() || ''} oninput={m.withAttr('value', this.values.mail_driver)} />
|
||||
<label>{app.translator.trans('core.admin.email.host_label')}</label>
|
||||
<input className="FormControl" value={this.values.mail_host() || ''} oninput={m.withAttr('value', this.values.mail_host)} />
|
||||
<label>{app.translator.trans('core.admin.email.port_label')}</label>
|
||||
<input className="FormControl" value={this.values.mail_port() || ''} oninput={m.withAttr('value', this.values.mail_port)} />
|
||||
<label>{app.translator.trans('core.admin.email.encryption_label')}</label>
|
||||
<input className="FormControl" value={this.values.mail_encryption() || ''} oninput={m.withAttr('value', this.values.mail_encryption)} />
|
||||
</div>
|
||||
]
|
||||
})}
|
||||
|
||||
{FieldSet.component({
|
||||
label: app.translator.trans('core.admin.email.account_heading'),
|
||||
className: 'MailPage-MailSettings',
|
||||
children: [
|
||||
<div className="MailPage-MailSettings-input">
|
||||
<label>{app.translator.trans('core.admin.email.username_label')}</label>
|
||||
<input className="FormControl" value={this.values.mail_username() || ''} oninput={m.withAttr('value', this.values.mail_username)} />
|
||||
<label>{app.translator.trans('core.admin.email.password_label')}</label>
|
||||
<input className="FormControl" value={this.values.mail_password() || ''} oninput={m.withAttr('value', this.values.mail_password)} />
|
||||
</div>
|
||||
]
|
||||
})}
|
||||
|
||||
{FieldSet.component({
|
||||
label: app.translator.trans('core.admin.email.addresses_heading'),
|
||||
className: 'MailPage-MailSettings',
|
||||
children: [
|
||||
<div className="MailPage-MailSettings-input">
|
||||
<label>{app.translator.trans('core.admin.email.from_label')}</label>
|
||||
<input className="FormControl" value={this.values.mail_from() || ''} oninput={m.withAttr('value', this.values.mail_from)} />
|
||||
</div>
|
||||
]
|
||||
})}
|
||||
|
||||
{Button.component({
|
||||
type: 'submit',
|
||||
className: 'Button Button--primary',
|
||||
children: app.translator.trans('core.admin.email.submit_button'),
|
||||
loading: this.loading,
|
||||
disabled: !this.changed()
|
||||
})}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
changed() {
|
||||
return this.fields.some(key => this.values[key]() !== app.data.settings[key]);
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.loading) return;
|
||||
|
||||
this.loading = true;
|
||||
app.alerts.dismiss(this.successAlert);
|
||||
|
||||
const settings = {};
|
||||
|
||||
this.fields.forEach(key => settings[key] = this.values[key]());
|
||||
|
||||
saveSettings(settings)
|
||||
.then(() => {
|
||||
app.alerts.show(this.successAlert = new Alert({type: 'success', children: app.translator.trans('core.admin.basics.saved_message')}));
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@ import Dropdown from 'flarum/components/Dropdown';
|
||||
import Button from 'flarum/components/Button';
|
||||
import Separator from 'flarum/components/Separator';
|
||||
import Group from 'flarum/models/Group';
|
||||
import Badge from 'flarum/components/Badge';
|
||||
import GroupBadge from 'flarum/components/GroupBadge';
|
||||
|
||||
function badgeForId(id) {
|
||||
@@ -10,6 +11,27 @@ function badgeForId(id) {
|
||||
return group ? GroupBadge.component({group, label: null}) : '';
|
||||
}
|
||||
|
||||
function filterByRequiredPermissions(groupIds, permission) {
|
||||
app.getRequiredPermissions(permission)
|
||||
.forEach(required => {
|
||||
const restrictToGroupIds = app.data.permissions[required] || [];
|
||||
|
||||
if (restrictToGroupIds.indexOf(Group.GUEST_ID) !== -1) {
|
||||
// do nothing
|
||||
} else if (restrictToGroupIds.indexOf(Group.MEMBER_ID) !== -1) {
|
||||
groupIds = groupIds.filter(id => id !== Group.GUEST_ID);
|
||||
} else if (groupIds.indexOf(Group.MEMBER_ID) !== -1) {
|
||||
groupIds = restrictToGroupIds;
|
||||
} else {
|
||||
groupIds = restrictToGroupIds.filter(id => groupIds.indexOf(id) !== -1);
|
||||
}
|
||||
|
||||
groupIds = filterByRequiredPermissions(groupIds, required);
|
||||
});
|
||||
|
||||
return groupIds;
|
||||
}
|
||||
|
||||
export default class PermissionDropdown extends Dropdown {
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
@@ -21,15 +43,18 @@ export default class PermissionDropdown extends Dropdown {
|
||||
view() {
|
||||
this.props.children = [];
|
||||
|
||||
const groupIds = app.permissions[this.props.permission] || [];
|
||||
let groupIds = app.data.permissions[this.props.permission] || [];
|
||||
|
||||
groupIds = filterByRequiredPermissions(groupIds, this.props.permission);
|
||||
|
||||
const everyone = groupIds.indexOf(Group.GUEST_ID) !== -1;
|
||||
const members = groupIds.indexOf(Group.MEMBER_ID) !== -1;
|
||||
const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
|
||||
|
||||
if (everyone) {
|
||||
this.props.label = 'Everyone';
|
||||
this.props.label = Badge.component({icon: 'globe'});
|
||||
} else if (members) {
|
||||
this.props.label = 'Members';
|
||||
this.props.label = Badge.component({icon: 'user'});
|
||||
} else {
|
||||
this.props.label = [
|
||||
badgeForId(Group.ADMINISTRATOR_ID),
|
||||
@@ -37,57 +62,62 @@ export default class PermissionDropdown extends Dropdown {
|
||||
];
|
||||
}
|
||||
|
||||
if (this.props.allowGuest) {
|
||||
if (this.showing) {
|
||||
if (this.props.allowGuest) {
|
||||
this.props.children.push(
|
||||
Button.component({
|
||||
children: [Badge.component({icon: 'globe'}), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')],
|
||||
icon: everyone ? 'check' : true,
|
||||
onclick: () => this.save([Group.GUEST_ID]),
|
||||
disabled: this.isGroupDisabled(Group.GUEST_ID)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.props.children.push(
|
||||
Button.component({
|
||||
children: 'Everyone',
|
||||
icon: everyone ? 'check' : true,
|
||||
onclick: () => this.save([Group.GUEST_ID])
|
||||
children: [Badge.component({icon: 'user'}), ' ', app.translator.trans('core.admin.permissions_controls.members_button')],
|
||||
icon: members ? 'check' : true,
|
||||
onclick: () => this.save([Group.MEMBER_ID]),
|
||||
disabled: this.isGroupDisabled(Group.MEMBER_ID)
|
||||
}),
|
||||
|
||||
Separator.component(),
|
||||
|
||||
Button.component({
|
||||
children: [badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()],
|
||||
icon: !everyone && !members ? 'check' : true,
|
||||
disabled: !everyone && !members,
|
||||
onclick: e => {
|
||||
if (e.shiftKey) e.stopPropagation();
|
||||
this.save([]);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
[].push.apply(
|
||||
this.props.children,
|
||||
app.store.all('groups')
|
||||
.filter(group => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
||||
.map(group => Button.component({
|
||||
children: [badgeForId(group.id()), ' ', group.namePlural()],
|
||||
icon: groupIds.indexOf(group.id()) !== -1 ? 'check' : true,
|
||||
onclick: (e) => {
|
||||
if (e.shiftKey) e.stopPropagation();
|
||||
this.toggle(group.id());
|
||||
},
|
||||
disabled: this.isGroupDisabled(group.id()) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID)
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
this.props.children.push(
|
||||
Button.component({
|
||||
children: 'Members',
|
||||
icon: members ? 'check' : true,
|
||||
onclick: () => this.save([Group.MEMBER_ID])
|
||||
}),
|
||||
|
||||
Separator.component(),
|
||||
|
||||
Button.component({
|
||||
children: [GroupBadge.component({group: adminGroup, label: null}), ' ', adminGroup.namePlural()],
|
||||
icon: !everyone && !members ? 'check' : true,
|
||||
disabled: !everyone && !members,
|
||||
onclick: e => {
|
||||
e.stopPropagation();
|
||||
this.save([]);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
[].push.apply(
|
||||
this.props.children,
|
||||
app.store.all('groups')
|
||||
.filter(group => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
||||
.map(group => Button.component({
|
||||
children: [GroupBadge.component({group, label: null}), ' ', group.namePlural()],
|
||||
icon: groupIds.indexOf(group.id()) !== -1 ? 'check' : true,
|
||||
onclick: (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggle(group.id());
|
||||
}
|
||||
}))
|
||||
);
|
||||
|
||||
return super.view();
|
||||
}
|
||||
|
||||
save(groupIds) {
|
||||
const permission = this.props.permission;
|
||||
|
||||
app.permissions[permission] = groupIds;
|
||||
app.data.permissions[permission] = groupIds;
|
||||
|
||||
app.request({
|
||||
method: 'POST',
|
||||
@@ -99,7 +129,7 @@ export default class PermissionDropdown extends Dropdown {
|
||||
toggle(groupId) {
|
||||
const permission = this.props.permission;
|
||||
|
||||
let groupIds = app.permissions[permission] || [];
|
||||
let groupIds = app.data.permissions[permission] || [];
|
||||
|
||||
const index = groupIds.indexOf(groupId);
|
||||
|
||||
@@ -112,4 +142,8 @@ export default class PermissionDropdown extends Dropdown {
|
||||
|
||||
this.save(groupIds);
|
||||
}
|
||||
|
||||
isGroupDisabled(id) {
|
||||
return filterByRequiredPermissions([id], this.props.permission).indexOf(id) === -1;
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,12 @@
|
||||
import Component from 'flarum/Component';
|
||||
import PermissionDropdown from 'flarum/components/PermissionDropdown';
|
||||
import ConfigDropdown from 'flarum/components/ConfigDropdown';
|
||||
import SettingDropdown from 'flarum/components/SettingDropdown';
|
||||
import Button from 'flarum/components/Button';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
export default class PermissionGrid extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
init() {
|
||||
this.permissions = this.permissionItems().toArray();
|
||||
}
|
||||
|
||||
@@ -45,7 +44,7 @@ export default class PermissionGrid extends Component {
|
||||
</tr>
|
||||
{section.children.map(child => (
|
||||
<tr className="PermissionGrid-child">
|
||||
<th>{child.label}</th>
|
||||
<th>{icon(child.icon)}{child.label}</th>
|
||||
{permissionCells(child)}
|
||||
<td/>
|
||||
</tr>
|
||||
@@ -60,24 +59,24 @@ export default class PermissionGrid extends Component {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('view', {
|
||||
label: 'View the forum',
|
||||
label: app.translator.trans('core.admin.permissions.read_heading'),
|
||||
children: this.viewItems().toArray()
|
||||
});
|
||||
}, 100);
|
||||
|
||||
items.add('start', {
|
||||
label: 'Start discussions',
|
||||
label: app.translator.trans('core.admin.permissions.create_heading'),
|
||||
children: this.startItems().toArray()
|
||||
});
|
||||
}, 90);
|
||||
|
||||
items.add('reply', {
|
||||
label: 'Reply to discussions',
|
||||
label: app.translator.trans('core.admin.permissions.participate_heading'),
|
||||
children: this.replyItems().toArray()
|
||||
});
|
||||
}, 80);
|
||||
|
||||
items.add('moderate', {
|
||||
label: 'Moderate',
|
||||
label: app.translator.trans('core.admin.permissions.moderate_heading'),
|
||||
children: this.moderateItems().toArray()
|
||||
});
|
||||
}, 70);
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -85,22 +84,31 @@ export default class PermissionGrid extends Component {
|
||||
viewItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('view', {
|
||||
label: 'View discussions',
|
||||
permission: 'forum.view',
|
||||
items.add('viewDiscussions', {
|
||||
icon: 'eye',
|
||||
label: app.translator.trans('core.admin.permissions.view_discussions_label'),
|
||||
permission: 'viewDiscussions',
|
||||
allowGuest: true
|
||||
});
|
||||
}, 100);
|
||||
|
||||
items.add('viewUserList', {
|
||||
icon: 'users',
|
||||
label: app.translator.trans('core.admin.permissions.view_user_list_label'),
|
||||
permission: 'viewUserList',
|
||||
allowGuest: true
|
||||
}, 100);
|
||||
|
||||
items.add('signUp', {
|
||||
label: 'Sign up',
|
||||
setting: () => ConfigDropdown.component({
|
||||
icon: 'user-plus',
|
||||
label: app.translator.trans('core.admin.permissions.sign_up_label'),
|
||||
setting: () => SettingDropdown.component({
|
||||
key: 'allow_sign_up',
|
||||
options: [
|
||||
{value: '1', label: 'Open'},
|
||||
{value: '0', label: 'Closed'}
|
||||
{value: '1', label: app.translator.trans('core.admin.permissions_controls.signup_open_button')},
|
||||
{value: '0', label: app.translator.trans('core.admin.permissions_controls.signup_closed_button')}
|
||||
]
|
||||
})
|
||||
});
|
||||
}, 90);
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -109,26 +117,30 @@ export default class PermissionGrid extends Component {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('start', {
|
||||
label: 'Start discussions',
|
||||
permission: 'forum.startDiscussion'
|
||||
});
|
||||
icon: 'edit',
|
||||
label: app.translator.trans('core.admin.permissions.start_discussions_label'),
|
||||
permission: 'startDiscussion'
|
||||
}, 100);
|
||||
|
||||
items.add('allowRenaming', {
|
||||
label: 'Allow renaming',
|
||||
icon: 'i-cursor',
|
||||
label: app.translator.trans('core.admin.permissions.allow_renaming_label'),
|
||||
setting: () => {
|
||||
const minutes = parseInt(app.config.allow_renaming, 10);
|
||||
const minutes = parseInt(app.data.settings.allow_renaming, 10);
|
||||
|
||||
return ConfigDropdown.component({
|
||||
defaultLabel: minutes ? `For ${minutes} minutes` : 'Indefinitely',
|
||||
return SettingDropdown.component({
|
||||
defaultLabel: minutes
|
||||
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, {count: minutes})
|
||||
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
|
||||
key: 'allow_renaming',
|
||||
options: [
|
||||
{value: '-1', label: 'Indefinitely'},
|
||||
{value: '10', label: 'For 10 minutes'},
|
||||
{value: 'reply', label: 'Until next reply'}
|
||||
{value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')},
|
||||
{value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button')},
|
||||
{value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button')}
|
||||
]
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 90);
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -137,26 +149,30 @@ export default class PermissionGrid extends Component {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('reply', {
|
||||
label: 'Reply to discussions',
|
||||
icon: 'reply',
|
||||
label: app.translator.trans('core.admin.permissions.reply_to_discussions_label'),
|
||||
permission: 'discussion.reply'
|
||||
});
|
||||
}, 100);
|
||||
|
||||
items.add('allowPostEditing', {
|
||||
label: 'Allow post editing',
|
||||
icon: 'pencil',
|
||||
label: app.translator.trans('core.admin.permissions.allow_post_editing_label'),
|
||||
setting: () => {
|
||||
const minutes = parseInt(app.config.allow_post_editing, 10);
|
||||
const minutes = parseInt(app.data.settings.allow_post_editing, 10);
|
||||
|
||||
return ConfigDropdown.component({
|
||||
defaultLabel: minutes ? `For ${minutes} minutes` : 'Indefinitely',
|
||||
return SettingDropdown.component({
|
||||
defaultLabel: minutes
|
||||
? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, {count: minutes})
|
||||
: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
|
||||
key: 'allow_post_editing',
|
||||
options: [
|
||||
{value: '-1', label: 'Indefinitely'},
|
||||
{value: '10', label: 'For 10 minutes'},
|
||||
{value: 'reply', label: 'Until next reply'}
|
||||
{value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')},
|
||||
{value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button')},
|
||||
{value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button')}
|
||||
]
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 90);
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -164,25 +180,41 @@ export default class PermissionGrid extends Component {
|
||||
moderateItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('editPosts', {
|
||||
label: 'Edit posts',
|
||||
permission: 'discussion.editPosts'
|
||||
});
|
||||
|
||||
items.add('deletePosts', {
|
||||
label: 'Delete posts',
|
||||
permission: 'discussion.deletePosts'
|
||||
});
|
||||
items.add('viewIpsPosts', {
|
||||
icon: 'bullseye',
|
||||
label: app.translator.trans('core.admin.permissions.view_post_ips_label'),
|
||||
permission: 'discussion.viewIpsPosts'
|
||||
}, 110);
|
||||
|
||||
items.add('renameDiscussions', {
|
||||
label: 'Rename discussions',
|
||||
icon: 'i-cursor',
|
||||
label: app.translator.trans('core.admin.permissions.rename_discussions_label'),
|
||||
permission: 'discussion.rename'
|
||||
});
|
||||
}, 100);
|
||||
|
||||
items.add('hideDiscussions', {
|
||||
icon: 'trash-o',
|
||||
label: app.translator.trans('core.admin.permissions.delete_discussions_label'),
|
||||
permission: 'discussion.hide'
|
||||
}, 90);
|
||||
|
||||
items.add('deleteDiscussions', {
|
||||
label: 'Delete discussions',
|
||||
icon: 'times',
|
||||
label: app.translator.trans('core.admin.permissions.delete_discussions_forever_label'),
|
||||
permission: 'discussion.delete'
|
||||
});
|
||||
}, 80);
|
||||
|
||||
items.add('editPosts', {
|
||||
icon: 'pencil',
|
||||
label: app.translator.trans('core.admin.permissions.edit_and_delete_posts_label'),
|
||||
permission: 'discussion.editPosts'
|
||||
}, 70);
|
||||
|
||||
items.add('deletePosts', {
|
||||
icon: 'times',
|
||||
label: app.translator.trans('core.admin.permissions.delete_posts_forever_label'),
|
||||
permission: 'discussion.deletePosts'
|
||||
}, 60);
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -191,17 +223,20 @@ export default class PermissionGrid extends Component {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('global', {
|
||||
label: 'Global',
|
||||
label: app.translator.trans('core.admin.permissions.global_heading'),
|
||||
render: item => {
|
||||
if (item.setting) {
|
||||
return item.setting();
|
||||
} else if (item.permission) {
|
||||
return PermissionDropdown.component(Object.assign({}, item));
|
||||
return PermissionDropdown.component({
|
||||
permission: item.permission,
|
||||
allowGuest: item.allowGuest
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
@@ -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">
|
||||
@@ -25,7 +25,7 @@ export default class PermissionsPage extends Component {
|
||||
))}
|
||||
<button className="Button Group Group--add" onclick={() => app.modal.show(new EditGroupModal())}>
|
||||
{icon('plus', {className: 'Group-icon'})}
|
||||
<span className="Group-name">New Group</span>
|
||||
<span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -43,7 +43,7 @@ export default class SessionDropdown extends Dropdown {
|
||||
items.add('logOut',
|
||||
Button.component({
|
||||
icon: 'sign-out',
|
||||
children: app.trans('core.log_out'),
|
||||
children: app.translator.trans('core.admin.header.log_out_button'),
|
||||
onclick: app.session.logout.bind(app.session)
|
||||
}),
|
||||
-100
|
||||
|
@@ -1,23 +1,23 @@
|
||||
import SelectDropdown from 'flarum/components/SelectDropdown';
|
||||
import Button from 'flarum/components/Button';
|
||||
import saveConfig from 'flarum/utils/saveConfig';
|
||||
import saveSettings from 'flarum/utils/saveSettings';
|
||||
|
||||
export default class ConfigDropdown extends SelectDropdown {
|
||||
export default class SettingDropdown extends SelectDropdown {
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.className = 'ConfigDropdown';
|
||||
props.className = 'SettingDropdown';
|
||||
props.buttonClassName = 'Button Button--text';
|
||||
props.caretIcon = 'caret-down';
|
||||
props.defaultLabel = 'Custom';
|
||||
|
||||
props.children = props.options.map(({value, label}) => {
|
||||
const active = app.config[props.key] === value;
|
||||
const active = app.data.settings[props.key] === value;
|
||||
|
||||
return Button.component({
|
||||
children: label,
|
||||
icon: active ? 'check' : true,
|
||||
onclick: saveConfig.bind(this, {[props.key]: value}),
|
||||
onclick: saveSettings.bind(this, {[props.key]: value}),
|
||||
active
|
||||
});
|
||||
});
|
79
js/admin/src/components/SettingsModal.js
Normal file
79
js/admin/src/components/SettingsModal.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import Modal from 'flarum/components/Modal';
|
||||
import Button from 'flarum/components/Button';
|
||||
import saveSettings from 'flarum/utils/saveSettings';
|
||||
|
||||
export default class SettingsModal extends Modal {
|
||||
init() {
|
||||
this.settings = {};
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
form() {
|
||||
return '';
|
||||
}
|
||||
|
||||
content() {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form">
|
||||
{this.form()}
|
||||
|
||||
<div className="Form-group">
|
||||
{this.submitButton()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
submitButton() {
|
||||
return (
|
||||
<Button
|
||||
type="submit"
|
||||
className="Button Button--primary"
|
||||
loading={this.loading}
|
||||
disabled={!this.changed()}>
|
||||
{app.translator.trans('core.admin.settings.submit_button')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
setting(key, fallback = '') {
|
||||
this.settings[key] = this.settings[key] || m.prop(app.data.settings[key] || fallback);
|
||||
|
||||
return this.settings[key];
|
||||
}
|
||||
|
||||
dirty() {
|
||||
const dirty = {};
|
||||
|
||||
Object.keys(this.settings).forEach(key => {
|
||||
const value = this.settings[key]();
|
||||
|
||||
if (value !== app.data.settings[key]) {
|
||||
dirty[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return dirty;
|
||||
}
|
||||
|
||||
changed() {
|
||||
return Object.keys(this.dirty()).length;
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
saveSettings(this.dirty()).then(
|
||||
this.onsaved.bind(this),
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
onsaved() {
|
||||
this.hide();
|
||||
}
|
||||
}
|
97
js/admin/src/components/UploadImageButton.js
Normal file
97
js/admin/src/components/UploadImageButton.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import Button from 'flarum/components/Button';
|
||||
|
||||
export default class UploadImageButton extends Button {
|
||||
init() {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
view() {
|
||||
this.props.loading = this.loading;
|
||||
this.props.className = (this.props.className || '') + ' Button';
|
||||
|
||||
if (app.data.settings[this.props.name + '_path']) {
|
||||
this.props.onclick = this.remove.bind(this);
|
||||
this.props.children = app.translator.trans('core.admin.upload_image.remove_button');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p><img src={app.forum.attribute(this.props.name+'Url')} alt=""/></p>
|
||||
<p>{super.view()}</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
this.props.onclick = this.upload.bind(this);
|
||||
this.props.children = app.translator.trans('core.admin.upload_image.upload_button');
|
||||
}
|
||||
|
||||
return super.view();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user to upload an image.
|
||||
*/
|
||||
upload() {
|
||||
if (this.loading) return;
|
||||
|
||||
const $input = $('<input type="file">');
|
||||
|
||||
$input.appendTo('body').hide().click().on('change', e => {
|
||||
const data = new FormData();
|
||||
data.append(this.props.name, $(e.target)[0].files[0]);
|
||||
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
app.request({
|
||||
method: 'POST',
|
||||
url: this.resourceUrl(),
|
||||
serialize: raw => raw,
|
||||
data
|
||||
}).then(
|
||||
this.success.bind(this),
|
||||
this.failure.bind(this)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the logo.
|
||||
*/
|
||||
remove() {
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
app.request({
|
||||
method: 'DELETE',
|
||||
url: this.resourceUrl()
|
||||
}).then(
|
||||
this.success.bind(this),
|
||||
this.failure.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
resourceUrl() {
|
||||
return app.forum.attribute('apiUrl') + '/' + this.props.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* After a successful upload/removal, reload the page.
|
||||
*
|
||||
* @param {Object} response
|
||||
* @protected
|
||||
*/
|
||||
success(response) {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* If upload/removal fails, stop loading.
|
||||
*
|
||||
* @param {Object} response
|
||||
* @protected
|
||||
*/
|
||||
failure(response) {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
}
|
||||
}
|
@@ -31,6 +31,7 @@ export default function boot(app) {
|
||||
app.alerts = m.mount(document.getElementById('alerts'), AlertManager.component());
|
||||
app.history = {
|
||||
canGoBack: () => true,
|
||||
getPrevious: () => {},
|
||||
backUrl: () => app.forum.attribute('baseUrl'),
|
||||
back: function() {
|
||||
window.location = this.backUrl();
|
||||
@@ -54,4 +55,12 @@ export default function boot(app) {
|
||||
}).start();
|
||||
|
||||
app.booted = true;
|
||||
|
||||
// If an extension has just been enabled, then we will run its settings
|
||||
// callback.
|
||||
const enabled = localStorage.getItem('enabledExtension');
|
||||
if (enabled && app.extensionSettings[enabled]) {
|
||||
app.extensionSettings[enabled]();
|
||||
localStorage.removeItem('enabledExtension');
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import BasicsPage from 'flarum/components/BasicsPage';
|
||||
import PermissionsPage from 'flarum/components/PermissionsPage';
|
||||
import AppearancePage from 'flarum/components/AppearancePage';
|
||||
import ExtensionsPage from 'flarum/components/ExtensionsPage';
|
||||
import MailPage from 'flarum/components/MailPage';
|
||||
|
||||
/**
|
||||
* The `routes` initializer defines the admin app's routes.
|
||||
@@ -15,6 +16,7 @@ export default function(app) {
|
||||
'basics': {path: '/basics', component: BasicsPage.component()},
|
||||
'permissions': {path: '/permissions', component: PermissionsPage.component()},
|
||||
'appearance': {path: '/appearance', component: AppearancePage.component()},
|
||||
'extensions': {path: '/extensions', component: ExtensionsPage.component()}
|
||||
'extensions': {path: '/extensions', component: ExtensionsPage.component()},
|
||||
'mail': {path: '/mail', component: MailPage.component()}
|
||||
};
|
||||
}
|
||||
|
@@ -1,14 +0,0 @@
|
||||
export default function saveConfig(config) {
|
||||
const oldConfig = JSON.parse(JSON.stringify(app.config));
|
||||
|
||||
Object.assign(app.config, config);
|
||||
|
||||
return app.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/config',
|
||||
data: {config}
|
||||
}).catch(error => {
|
||||
app.config = oldConfig;
|
||||
throw error;
|
||||
});
|
||||
}
|
14
js/admin/src/utils/saveSettings.js
Normal file
14
js/admin/src/utils/saveSettings.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function saveSettings(settings) {
|
||||
const oldSettings = JSON.parse(JSON.stringify(app.data.settings));
|
||||
|
||||
Object.assign(app.data.settings, settings);
|
||||
|
||||
return app.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/settings',
|
||||
data: settings
|
||||
}).catch(error => {
|
||||
app.data.settings = oldSettings;
|
||||
throw error;
|
||||
});
|
||||
}
|
@@ -7,9 +7,11 @@
|
||||
"spin.js": "~2.0.1",
|
||||
"moment": "~2.8.4",
|
||||
"color-thief": "v2.0",
|
||||
"mithril": "lhorie/mithril.js#next",
|
||||
"mithril": "lhorie/mithril.js#v0.2.5",
|
||||
"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",
|
||||
"punycode": "http://cdnjs.cloudflare.com/ajax/libs/punycode/1.4.1/punycode.js"
|
||||
}
|
||||
}
|
||||
|
2
js/forum/.gitignore
vendored
2
js/forum/.gitignore
vendored
@@ -1,3 +1 @@
|
||||
node_modules
|
||||
mithril.js
|
||||
dist
|
||||
|
@@ -1,20 +1,19 @@
|
||||
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',
|
||||
bowerDir + '/m.attrs.bidi/bidi.js',
|
||||
bowerDir + '/jquery/dist/jquery.js',
|
||||
bowerDir + '/jquery.hotkeys/jquery.hotkeys.js',
|
||||
bowerDir + '/color-thief/js/color-thief.js',
|
||||
bowerDir + '/color-thief/src/color-thief.js',
|
||||
bowerDir + '/moment/moment.js',
|
||||
bowerDir + '/autolink/autolink.js',
|
||||
bowerDir + '/autolink/autolink-min.js',
|
||||
|
||||
bowerDir + '/bootstrap/js/affix.js',
|
||||
bowerDir + '/bootstrap/js/dropdown.js',
|
||||
@@ -24,7 +23,8 @@ gulp({
|
||||
|
||||
bowerDir + '/spin.js/spin.js',
|
||||
bowerDir + '/spin.js/jquery.spin.js',
|
||||
bowerDir + '/fastclick/lib/fastclick.js'
|
||||
bowerDir + '/fastclick/lib/fastclick.js',
|
||||
bowerDir + '/punycode/index.js'
|
||||
],
|
||||
modules: {
|
||||
'flarum': [
|
||||
@@ -32,6 +32,5 @@ gulp({
|
||||
'../lib/**/*.js'
|
||||
]
|
||||
},
|
||||
externalHelpers: true,
|
||||
outputFile: 'dist/app.js'
|
||||
});
|
||||
|
32990
js/forum/dist/app.js
vendored
Normal file
32990
js/forum/dist/app.js
vendored
Normal file
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"
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import Search from 'flarum/components/Search';
|
||||
import Composer from 'flarum/components/Composer';
|
||||
import ReplyComposer from 'flarum/components/ReplyComposer';
|
||||
import DiscussionPage from 'flarum/components/DiscussionPage';
|
||||
import SignUpModal from 'flarum/components/SignUpModal';
|
||||
|
||||
export default class ForumApp extends App {
|
||||
constructor(...args) {
|
||||
@@ -76,4 +77,27 @@ export default class ForumApp extends App {
|
||||
return this.current instanceof DiscussionPage &&
|
||||
this.current.discussion === discussion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for when an external authenticator (social login) action has
|
||||
* completed.
|
||||
*
|
||||
* If the payload indicates that the user has been logged in, then the page
|
||||
* will be reloaded. Otherwise, a SignUpModal will be opened, prefilled
|
||||
* with the provided details.
|
||||
*
|
||||
* @param {Object} payload A dictionary of props to pass into the sign up
|
||||
* modal. A truthy `authenticated` prop indicates that the user has logged
|
||||
* in, and thus the page is reloaded.
|
||||
* @public
|
||||
*/
|
||||
authenticationComplete(payload) {
|
||||
if (payload.authenticated) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
const modal = new SignUpModal(payload);
|
||||
this.modal.show(modal);
|
||||
modal.$('[name=password]').focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -16,9 +16,7 @@ import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
* - `user`
|
||||
*/
|
||||
export default class AvatarEditor extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
init() {
|
||||
/**
|
||||
* Whether or not an avatar upload is in progress.
|
||||
*
|
||||
@@ -39,10 +37,11 @@ export default class AvatarEditor extends Component {
|
||||
return (
|
||||
<div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '')}>
|
||||
{avatar(user)}
|
||||
<a className="Dropdown-toggle"
|
||||
<a className={ user.avatarUrl() ? "Dropdown-toggle" : "Dropdown-toggle AvatarEditor--noAvatar" }
|
||||
title={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
|
||||
data-toggle="dropdown"
|
||||
onclick={this.quickUpload.bind(this)}>
|
||||
{this.loading ? LoadingIndicator.component() : icon('pencil')}
|
||||
{this.loading ? LoadingIndicator.component() : (user.avatarUrl() ? icon('pencil') : icon('plus-circle'))}
|
||||
</a>
|
||||
<ul className="Dropdown-menu Menu">
|
||||
{listItems(this.controlItems().toArray())}
|
||||
@@ -62,7 +61,7 @@ export default class AvatarEditor extends Component {
|
||||
items.add('upload',
|
||||
Button.component({
|
||||
icon: 'upload',
|
||||
children: app.trans('core.upload'),
|
||||
children: app.translator.trans('core.forum.user.avatar_upload_button'),
|
||||
onclick: this.upload.bind(this)
|
||||
})
|
||||
);
|
||||
@@ -70,7 +69,7 @@ export default class AvatarEditor extends Component {
|
||||
items.add('remove',
|
||||
Button.component({
|
||||
icon: 'times',
|
||||
children: app.trans('core.remove'),
|
||||
children: app.translator.trans('core.forum.user.avatar_remove_button'),
|
||||
onclick: this.remove.bind(this)
|
||||
})
|
||||
);
|
||||
@@ -163,7 +162,8 @@ export default class AvatarEditor extends Component {
|
||||
* @param {Object} response
|
||||
* @protected
|
||||
*/
|
||||
failure() {
|
||||
failure(response) {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
}
|
||||
}
|
||||
|
@@ -6,8 +6,8 @@ import Button from 'flarum/components/Button';
|
||||
* to change their email address.
|
||||
*/
|
||||
export default class ChangeEmailModal extends Modal {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
/**
|
||||
* Whether or not the email has been changed successfully.
|
||||
@@ -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() {
|
||||
@@ -29,21 +36,19 @@ export default class ChangeEmailModal extends Modal {
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.trans('core.change_email');
|
||||
return app.translator.trans('core.forum.change_email.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
if (this.success) {
|
||||
const emailProviderName = this.email().split('@')[1];
|
||||
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<p className="helpText">{app.trans('core.confirmation_email_sent', {email: <strong>{this.email()}</strong>})}</p>
|
||||
<p className="helpText">{app.translator.trans('core.forum.change_email.confirmation_message', {email: <strong>{this.email()}</strong>})}</p>
|
||||
<div className="Form-group">
|
||||
<a href={'http://' + emailProviderName} className="Button Button--primary Button--block">
|
||||
{app.trans('core.go_to', {location: emailProviderName})}
|
||||
</a>
|
||||
<Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}>
|
||||
{app.translator.trans('core.forum.change_email.dismiss_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,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_placeholder')}
|
||||
bidi={this.password}
|
||||
disabled={this.loading}/>
|
||||
</div>
|
||||
<div className="Form-group">
|
||||
@@ -65,7 +75,7 @@ export default class ChangeEmailModal extends Modal {
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.trans('core.save_changes')
|
||||
children: app.translator.trans('core.forum.change_email.submit_button')
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,17 +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()}).then(
|
||||
() => {
|
||||
this.loading = false;
|
||||
this.success = true;
|
||||
m.redraw();
|
||||
},
|
||||
() => {
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
app.session.user.save({email: this.email()}, {
|
||||
errorHandler: this.onerror.bind(this),
|
||||
meta: {password: this.password()}
|
||||
})
|
||||
.then(() => this.success = true)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
@@ -11,20 +11,20 @@ export default class ChangePasswordModal extends Modal {
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.trans('core.change_password');
|
||||
return app.translator.trans('core.forum.change_password.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<p className="helpText">{app.trans('core.change_password_help')}</p>
|
||||
<p className="helpText">{app.translator.trans('core.forum.change_password.text')}</p>
|
||||
<div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.trans('core.send_password_reset_email')
|
||||
children: app.translator.trans('core.forum.change_password.send_button')
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,8 +42,8 @@ export default class ChangePasswordModal extends Modal {
|
||||
url: app.forum.attribute('apiUrl') + '/forgot',
|
||||
data: {email: app.session.user.email()}
|
||||
}).then(
|
||||
() => this.hide(),
|
||||
() => this.loading = false
|
||||
this.hide.bind(this),
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@ import PostUser from 'flarum/components/PostUser';
|
||||
import PostMeta from 'flarum/components/PostMeta';
|
||||
import PostEdited from 'flarum/components/PostEdited';
|
||||
import EditPostComposer from 'flarum/components/EditPostComposer';
|
||||
import Composer from 'flarum/components/Composer';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
import Button from 'flarum/components/Button';
|
||||
@@ -21,8 +20,8 @@ import Button from 'flarum/components/Button';
|
||||
* - `post`
|
||||
*/
|
||||
export default class CommentPost extends Post {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
/**
|
||||
* If the post has been hidden, then this flag determines whether or not its
|
||||
@@ -42,16 +41,17 @@ export default class CommentPost extends Post {
|
||||
}
|
||||
|
||||
content() {
|
||||
return [
|
||||
<header className="Post-header"><ul>{listItems(this.headerItems().toArray())}</ul></header>,
|
||||
// Note: we avoid using JSX for the <ul> below because it results in some
|
||||
// weirdness in Mithril.js 0.1.x (see flarum/core#975). This workaround can
|
||||
// be reverted when we upgrade to Mithril 1.0.
|
||||
return super.content().concat([
|
||||
<header className="Post-header">{m('ul', listItems(this.headerItems().toArray()))}</header>,
|
||||
<div className="Post-body">
|
||||
{this.isEditing()
|
||||
? <div className="Post-preview" config={this.configPreview.bind(this)}/>
|
||||
: m.trust(this.props.post.contentHtml())}
|
||||
</div>,
|
||||
<footer className="Post-footer"><ul>{listItems(this.footerItems().toArray())}</ul></footer>,
|
||||
<aside className="Post-actions"><ul>{listItems(this.actionItems().toArray())}</ul></aside>
|
||||
];
|
||||
</div>
|
||||
]);
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
@@ -59,62 +59,36 @@ export default class CommentPost extends Post {
|
||||
|
||||
const contentHtml = this.isEditing() ? '' : this.props.post.contentHtml();
|
||||
|
||||
// If the post content has changed since the last render, we'll run through
|
||||
// all of the <script> tags in the content and evaluate them. This is
|
||||
// necessary because TextFormatter outputs them for e.g. syntax highlighting.
|
||||
if (context.contentHtml !== contentHtml) {
|
||||
if (typeof hljs === 'undefined') {
|
||||
this.loadHljs();
|
||||
} else {
|
||||
this.$('pre code').each(function(i, elm) {
|
||||
hljs.highlightBlock(elm);
|
||||
});
|
||||
}
|
||||
this.$('.Post-body script').each(function() {
|
||||
eval.call(window, $(this).text());
|
||||
});
|
||||
}
|
||||
|
||||
context.contentHtml = contentHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the highlight.js library and initialize highlighting when done.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
loadHljs() {
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
|
||||
const stylesheet = document.createElement('link');
|
||||
stylesheet.type = 'text/css';
|
||||
stylesheet.rel = 'stylesheet';
|
||||
stylesheet.href = '//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.7/styles/default.min.css';
|
||||
head.appendChild(stylesheet);
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.onload = () => {
|
||||
hljs._ = {};
|
||||
hljs.initHighlighting();
|
||||
};
|
||||
script.async = true;
|
||||
script.src = '//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.7/highlight.min.js';
|
||||
head.appendChild(script);
|
||||
}
|
||||
|
||||
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() {
|
||||
const post = this.props.post;
|
||||
const attrs = super.attrs();
|
||||
|
||||
return {
|
||||
className: classList({
|
||||
'CommentPost': true,
|
||||
'Post--hidden': post.isHidden(),
|
||||
'Post--edited': post.isEdited(),
|
||||
'revealContent': this.revealContent,
|
||||
'editing': this.isEditing()
|
||||
})
|
||||
};
|
||||
attrs.className += ' '+classList({
|
||||
'CommentPost': true,
|
||||
'Post--hidden': post.isHidden(),
|
||||
'Post--edited': post.isEdited(),
|
||||
'revealContent': this.revealContent,
|
||||
'editing': this.isEditing()
|
||||
});
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
configPreview(element, isInitialized, context) {
|
||||
@@ -176,22 +150,4 @@ export default class CommentPost extends Post {
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the post's footer.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
footerItems() {
|
||||
return new ItemList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the post's actions.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
actionItems() {
|
||||
return new ItemList();
|
||||
}
|
||||
}
|
||||
|
@@ -11,9 +11,7 @@ import computed from 'flarum/utils/computed';
|
||||
* `show`, `hide`, `close`, `minimize`, `fullScreen`, and `exitFullScreen`.
|
||||
*/
|
||||
class Composer extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
init() {
|
||||
/**
|
||||
* The composer's current position.
|
||||
*
|
||||
@@ -21,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).
|
||||
@@ -36,6 +27,13 @@ class Composer extends Component {
|
||||
*/
|
||||
this.height = null;
|
||||
|
||||
/**
|
||||
* Whether or not the composer currently has focus.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.active = false;
|
||||
|
||||
/**
|
||||
* Computed the composer's current height, based on the intended height, and
|
||||
* the composer's current state. This will be applied to the composer's
|
||||
@@ -61,20 +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
|
||||
'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)}>
|
||||
@@ -88,7 +85,11 @@ class Composer extends Component {
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
this.updateHeight();
|
||||
let defaultHeight;
|
||||
|
||||
if (!isInitialized) {
|
||||
defaultHeight = this.$().height();
|
||||
}
|
||||
|
||||
if (isInitialized) return;
|
||||
|
||||
@@ -99,12 +100,15 @@ class Composer extends Component {
|
||||
// Initialize the composer's intended height based on what the user has set
|
||||
// it at previously, or otherwise the composer's default height. After that,
|
||||
// we'll hide the composer.
|
||||
this.height = localStorage.getItem('composerHeight') || this.$().height();
|
||||
this.$().hide();
|
||||
this.height = localStorage.getItem('composerHeight') || defaultHeight;
|
||||
this.$().hide().css('bottom', -this.height);
|
||||
|
||||
// Whenever any of the inputs inside the composer are have focus, we want to
|
||||
// add a class to the composer to draw attention to it.
|
||||
this.$().on('focus blur', ':input', e => this.$().toggleClass('active', e.type === 'focusin'));
|
||||
this.$().on('focus blur', ':input', e => {
|
||||
this.active = e.type === 'focusin';
|
||||
m.redraw();
|
||||
});
|
||||
|
||||
// When the escape key is pressed on any inputs, close the composer.
|
||||
this.$().on('keydown', ':input', 'esc', () => this.close());
|
||||
@@ -113,7 +117,7 @@ class Composer extends Component {
|
||||
// component a chance to scream at the user to make sure they don't
|
||||
// unintentionally lose any contnet.
|
||||
window.onbeforeunload = () => {
|
||||
return (this.component && this.component.preventExit()) || null;
|
||||
return (this.component && this.component.preventExit()) || undefined;
|
||||
};
|
||||
|
||||
const handlers = {};
|
||||
@@ -198,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,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';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -363,20 +293,77 @@ 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();
|
||||
this.component.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -385,8 +372,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();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,10 +406,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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -422,7 +423,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,7 +437,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,28 +454,28 @@ class Composer extends Component {
|
||||
if (this.position === Composer.PositionEnum.FULLSCREEN) {
|
||||
items.add('exitFullScreen', ComposerButton.component({
|
||||
icon: 'compress',
|
||||
title: app.trans('core.exit_full_screen'),
|
||||
title: app.translator.trans('core.forum.composer.exit_full_screen_tooltip'),
|
||||
onclick: this.exitFullScreen.bind(this)
|
||||
}));
|
||||
} else {
|
||||
if (this.position !== Composer.PositionEnum.MINIMIZED) {
|
||||
items.add('minimize', ComposerButton.component({
|
||||
icon: 'minus minimize',
|
||||
title: app.trans('core.minimize'),
|
||||
title: app.translator.trans('core.forum.composer.minimize_tooltip'),
|
||||
onclick: this.minimize.bind(this),
|
||||
itemClassName: 'App-backControl'
|
||||
}));
|
||||
|
||||
items.add('fullScreen', ComposerButton.component({
|
||||
icon: 'expand',
|
||||
title: app.trans('core.full_screen'),
|
||||
title: app.translator.trans('core.forum.composer.full_screen_tooltip'),
|
||||
onclick: this.fullScreen.bind(this)
|
||||
}));
|
||||
}
|
||||
|
||||
items.add('close', ComposerButton.component({
|
||||
icon: 'times',
|
||||
title: app.trans('core.close'),
|
||||
title: app.translator.trans('core.forum.composer.close_tooltip'),
|
||||
onclick: this.close.bind(this)
|
||||
}));
|
||||
}
|
||||
|
@@ -22,9 +22,7 @@ import ItemList from 'flarum/utils/ItemList';
|
||||
* @abstract
|
||||
*/
|
||||
export default class ComposerBody extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
init() {
|
||||
/**
|
||||
* Whether or not the component is loading.
|
||||
*
|
||||
@@ -58,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>
|
||||
@@ -104,4 +102,12 @@ export default class ComposerBody extends Component {
|
||||
*/
|
||||
onsubmit() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop loading.
|
||||
*/
|
||||
loaded() {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import ComposerBody from 'flarum/components/ComposerBody';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
|
||||
/**
|
||||
* The `DiscussionComposer` component displays the composer content for starting
|
||||
@@ -12,8 +13,8 @@ import ComposerBody from 'flarum/components/ComposerBody';
|
||||
* - `titlePlaceholder`
|
||||
*/
|
||||
export default class DiscussionComposer extends ComposerBody {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
/**
|
||||
* The value of the title input.
|
||||
@@ -26,16 +27,19 @@ export default class DiscussionComposer extends ComposerBody {
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.placeholder = props.placeholder || app.trans('core.write_a_post');
|
||||
props.submitLabel = props.submitLabel || app.trans('core.post_discussion');
|
||||
props.confirmExit = props.confirmExit || app.trans('core.confirm_discard_discussion');
|
||||
props.titlePlaceholder = props.titlePlaceholder || app.trans('core.discussion_title');
|
||||
props.placeholder = props.placeholder || extractText(app.translator.trans('core.forum.composer_discussion.body_placeholder'));
|
||||
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()}
|
||||
@@ -64,23 +68,6 @@ export default class DiscussionComposer extends ComposerBody {
|
||||
m.redraw.strategy('none');
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
super.config(isInitialized, context);
|
||||
|
||||
// If the user presses the backspace key in the text editor, and the cursor
|
||||
// is already at the start, then we'll move the focus back into the title
|
||||
// input.
|
||||
this.editor.$('textarea').keydown((e) => {
|
||||
if (e.which === 8 && e.target.selectionStart === 0 && e.target.selectionEnd === 0) {
|
||||
e.preventDefault();
|
||||
|
||||
const $title = this.$(':input:enabled:visible:first')[0];
|
||||
$title.focus();
|
||||
$title.selectionStart = $title.selectionEnd = $title.value.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
preventExit() {
|
||||
return (this.title() || this.content()) && this.props.confirmExit;
|
||||
}
|
||||
@@ -108,11 +95,7 @@ export default class DiscussionComposer extends ComposerBody {
|
||||
app.cache.discussionList.addDiscussion(discussion);
|
||||
m.route(app.route.discussion(discussion));
|
||||
},
|
||||
response => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
app.alertErrors(response.errors);
|
||||
}
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -13,9 +13,7 @@ import Placeholder from 'flarum/components/Placeholder';
|
||||
* to send along in the API request to get discussion results.
|
||||
*/
|
||||
export default class DiscussionList extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
init() {
|
||||
/**
|
||||
* Whether or not discussion results are loading.
|
||||
*
|
||||
@@ -48,14 +46,14 @@ export default class DiscussionList extends Component {
|
||||
loading = LoadingIndicator.component();
|
||||
} else if (this.moreResults) {
|
||||
loading = Button.component({
|
||||
children: app.trans('core.load_more'),
|
||||
children: app.translator.trans('core.forum.discussion_list.load_more_button'),
|
||||
className: 'Button',
|
||||
onclick: this.loadMore.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
if (this.discussions.length === 0 && !this.loading) {
|
||||
const text = 'Looks like there are no discussions here. Why don\'t you create a new one?';
|
||||
const text = app.translator.trans('core.forum.discussion_list.empty_text');
|
||||
return (
|
||||
<div className="DiscussionList">
|
||||
{Placeholder.component({text})}
|
||||
@@ -117,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;
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ import SubtreeRetainer from 'flarum/utils/SubtreeRetainer';
|
||||
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
||||
import slidable from 'flarum/utils/slidable';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
import classList from 'flarum/utils/classList';
|
||||
|
||||
/**
|
||||
* The `DiscussionListItem` component shows a single discussion in the
|
||||
@@ -24,9 +25,7 @@ import extractText from 'flarum/utils/extractText';
|
||||
* - `params`
|
||||
*/
|
||||
export default class DiscussionListItem extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
init() {
|
||||
/**
|
||||
* Set up a subtree retainer so that the discussion will not be redrawn
|
||||
* unless new data comes in.
|
||||
@@ -43,6 +42,16 @@ export default class DiscussionListItem extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
attrs() {
|
||||
return {
|
||||
className: classList([
|
||||
'DiscussionListItem',
|
||||
this.active() ? 'active' : '',
|
||||
this.props.discussion.isHidden() ? 'DiscussionListItem--hidden' : ''
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
view() {
|
||||
const retain = this.subtree.retain();
|
||||
|
||||
@@ -56,9 +65,10 @@ export default class DiscussionListItem extends Component {
|
||||
const jumpTo = Math.min(discussion.lastPostNumber(), (discussion.readNumber() || 0) + 1);
|
||||
const relevantPosts = this.props.params.q ? discussion.relevantPosts() : [];
|
||||
const controls = DiscussionControls.controls(discussion, this).toArray();
|
||||
const attrs = this.attrs();
|
||||
|
||||
return (
|
||||
<div className={'DiscussionListItem ' + (this.active() ? 'active' : '')}>
|
||||
<div {...attrs}>
|
||||
|
||||
{controls.length ? Dropdown.component({
|
||||
icon: 'ellipsis-v',
|
||||
@@ -75,7 +85,7 @@ export default class DiscussionListItem extends Component {
|
||||
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '') + (isRead ? ' read' : '')}>
|
||||
<a href={startUser ? app.route.user(startUser) : '#'}
|
||||
className="DiscussionListItem-author"
|
||||
title={extractText(app.trans('core.discussion_started', {user: startUser, ago: humanTime(discussion.startTime())}))}
|
||||
title={extractText(app.translator.trans('core.forum.discussion_list.started_text', {user: startUser, ago: humanTime(discussion.startTime())}))}
|
||||
config={function(element) {
|
||||
$(element).tooltip({placement: 'right'});
|
||||
m.route.apply(this, arguments);
|
||||
@@ -96,7 +106,7 @@ export default class DiscussionListItem extends Component {
|
||||
|
||||
<span className="DiscussionListItem-count"
|
||||
onclick={this.markAsRead.bind(this)}
|
||||
title={showUnread ? app.trans('core.mark_as_read') : ''}>
|
||||
title={showUnread ? app.translator.trans('core.forum.discussion_list.mark_as_read_tooltip') : ''}>
|
||||
{abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'repliesCount']())}
|
||||
</span>
|
||||
|
||||
|
@@ -13,8 +13,8 @@ import DiscussionControls from 'flarum/utils/DiscussionControls';
|
||||
* the discussion list pane, the hero, the posts, and the sidebar.
|
||||
*/
|
||||
export default class DiscussionPage extends Page {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
/**
|
||||
* The discussion that is being viewed.
|
||||
@@ -128,7 +128,7 @@ export default class DiscussionPage extends Page {
|
||||
// component for the first time on page load, then any calls to m.redraw
|
||||
// will be ineffective and thus any configs (scroll code) will be run
|
||||
// before stuff is drawn to the page.
|
||||
setTimeout(this.show.bind(this, preloadedDiscussion));
|
||||
setTimeout(this.show.bind(this, preloadedDiscussion), 0);
|
||||
} else {
|
||||
const params = this.requestParams();
|
||||
|
||||
@@ -159,6 +159,7 @@ export default class DiscussionPage extends Page {
|
||||
show(discussion) {
|
||||
this.discussion = discussion;
|
||||
|
||||
app.history.push('discussion', discussion.title());
|
||||
app.setTitle(discussion.title());
|
||||
app.setTitleCount(0);
|
||||
|
||||
@@ -183,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,7 +274,7 @@ export default class DiscussionPage extends Page {
|
||||
m.route(url, true);
|
||||
window.history.replaceState(null, document.title, url);
|
||||
|
||||
app.history.push('discussion');
|
||||
app.history.push('discussion', discussion.title());
|
||||
|
||||
// If the user hasn't read past here before, then we'll update their read
|
||||
// state and redraw.
|
||||
|
@@ -20,6 +20,6 @@ export default class DiscussionRenamedNotification extends Notification {
|
||||
}
|
||||
|
||||
content() {
|
||||
return app.trans('core.discussion_renamed_notification', {user: this.props.notification.sender()});
|
||||
return app.translator.trans('core.forum.notifications.discussion_renamed_text', {user: this.props.notification.sender()});
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import EventPost from 'flarum/components/EventPost';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
|
||||
/**
|
||||
* The `DiscussionRenamedPost` component displays a discussion event post
|
||||
@@ -13,8 +14,11 @@ export default class DiscussionRenamedPost extends EventPost {
|
||||
return 'pencil';
|
||||
}
|
||||
|
||||
descriptionKey() {
|
||||
return 'core.discussion_renamed_post';
|
||||
description(data) {
|
||||
const renamed = app.translator.trans('core.forum.post_stream.discussion_renamed_text', data);
|
||||
const oldName = app.translator.trans('core.forum.post_stream.discussion_renamed_old_tooltip', data);
|
||||
|
||||
return <span title={extractText(oldName)}>{renamed}</span>;
|
||||
}
|
||||
|
||||
descriptionData() {
|
||||
@@ -23,7 +27,7 @@ export default class DiscussionRenamedPost extends EventPost {
|
||||
const newTitle = post.content()[1];
|
||||
|
||||
return {
|
||||
'old': <strong className="DiscussionRenamedPost-old">{oldTitle}</strong>,
|
||||
'old': oldTitle,
|
||||
'new': <strong className="DiscussionRenamedPost-new">{newTitle}</strong>
|
||||
};
|
||||
}
|
||||
|
@@ -13,6 +13,8 @@ export default class DiscussionsSearchSource {
|
||||
}
|
||||
|
||||
search(query) {
|
||||
query = query.toLowerCase();
|
||||
|
||||
this.results[query] = [];
|
||||
|
||||
const params = {
|
||||
@@ -25,14 +27,16 @@ export default class DiscussionsSearchSource {
|
||||
}
|
||||
|
||||
view(query) {
|
||||
query = query.toLowerCase();
|
||||
|
||||
const results = this.results[query] || [];
|
||||
|
||||
return [
|
||||
<li className="Dropdown-header">{app.trans('core.discussions')}</li>,
|
||||
<li className="Dropdown-header">{app.translator.trans('core.forum.search.discussions_heading')}</li>,
|
||||
<li>
|
||||
{LinkButton.component({
|
||||
icon: 'search',
|
||||
children: app.trans('core.search_all_discussions', {query}),
|
||||
children: app.translator.trans('core.forum.search.all_discussions_button', {query}),
|
||||
href: app.route('index', {q: query})
|
||||
})}
|
||||
</li>,
|
||||
|
@@ -6,8 +6,8 @@ import DiscussionList from 'flarum/components/DiscussionList';
|
||||
* page.
|
||||
*/
|
||||
export default class DiscussionsUserPage extends UserPage {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.loadUser(m.route.param('username'));
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -12,10 +19,12 @@ import icon from 'flarum/helpers/icon';
|
||||
* - `post`
|
||||
*/
|
||||
export default class EditPostComposer extends ComposerBody {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.editor.props.preview = e => {
|
||||
minimizeComposerIfFullScreen(e);
|
||||
|
||||
this.editor.props.preview = () => {
|
||||
m.route(app.route.post(this.props.post));
|
||||
};
|
||||
}
|
||||
@@ -23,8 +32,8 @@ export default class EditPostComposer extends ComposerBody {
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.submitLabel = props.submitLabel || app.trans('core.save_changes');
|
||||
props.confirmExit = props.confirmExit || app.trans('core.confirm_discard_edit');
|
||||
props.submitLabel = props.submitLabel || app.translator.trans('core.forum.composer_edit.submit_button');
|
||||
props.confirmExit = props.confirmExit || app.translator.trans('core.forum.composer_edit.discard_confirmation');
|
||||
props.originalContent = props.originalContent || props.post.content();
|
||||
props.user = props.user || props.post.user();
|
||||
|
||||
@@ -35,11 +44,17 @@ 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}>
|
||||
{app.trans('core.editing_post', {number: post.number(), discussion: post.discussion().title()})}
|
||||
{icon('pencil')} {' '}
|
||||
<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>
|
||||
));
|
||||
@@ -64,11 +79,8 @@ export default class EditPostComposer extends ComposerBody {
|
||||
const data = this.data();
|
||||
|
||||
this.props.post.save(data).then(
|
||||
() => {
|
||||
app.composer.hide();
|
||||
m.redraw();
|
||||
},
|
||||
() => this.loading = false
|
||||
() => app.composer.hide(),
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -2,18 +2,20 @@ import Modal from 'flarum/components/Modal';
|
||||
import Button from 'flarum/components/Button';
|
||||
import GroupBadge from 'flarum/components/GroupBadge';
|
||||
import Group from 'flarum/models/Group';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
|
||||
/**
|
||||
* The `EditUserModal` component displays a modal dialog with a login form.
|
||||
*/
|
||||
export default class EditUserModal extends Modal {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
const user = this.props.user;
|
||||
|
||||
this.username = m.prop(user.username() || '');
|
||||
this.email = m.prop(user.email() || '');
|
||||
this.isActivated = m.prop(user.isActivated() || false);
|
||||
this.setPassword = m.prop(false);
|
||||
this.password = m.prop(user.password() || '');
|
||||
this.groups = {};
|
||||
@@ -28,7 +30,7 @@ export default class EditUserModal extends Modal {
|
||||
}
|
||||
|
||||
title() {
|
||||
return 'Edit User';
|
||||
return app.translator.trans('core.forum.edit_user.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
@@ -36,52 +38,60 @@ export default class EditUserModal extends Modal {
|
||||
<div className="Modal-body">
|
||||
<div className="Form">
|
||||
<div className="Form-group">
|
||||
<label>Username</label>
|
||||
<input className="FormControl" placeholder={app.trans('core.username')}
|
||||
value={this.username()}
|
||||
onchange={m.withAttr('value', this.username)} />
|
||||
<label>{app.translator.trans('core.forum.edit_user.username_heading')}</label>
|
||||
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.username_label'))}
|
||||
bidi={this.username} />
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
<label>Email</label>
|
||||
<div>
|
||||
<input className="FormControl" placeholder={app.trans('core.email')}
|
||||
value={this.email()}
|
||||
onchange={m.withAttr('value', this.email)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
<label>Password</label>
|
||||
<div>
|
||||
<label className="checkbox">
|
||||
<input type="checkbox" checked={this.setPassword()} onchange={e => {
|
||||
this.setPassword(e.target.checked);
|
||||
m.redraw(true);
|
||||
if (e.target.checked) this.$('[name=password]').select();
|
||||
m.redraw.strategy('none');
|
||||
}}/>
|
||||
Set new password
|
||||
</label>
|
||||
{this.setPassword() ? (
|
||||
<input className="FormControl" type="password" name="password" placeholder={app.trans('core.password')}
|
||||
value={this.password()}
|
||||
onchange={m.withAttr('value', this.password)} />
|
||||
{app.session.user !== this.props.user ? [
|
||||
<div className="Form-group">
|
||||
<label>{app.translator.trans('core.forum.edit_user.email_heading')}</label>
|
||||
<div>
|
||||
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.email_label'))}
|
||||
bidi={this.email} />
|
||||
</div>
|
||||
{!this.isActivated() ? (
|
||||
<div>
|
||||
{Button.component({
|
||||
className: 'Button Button--block',
|
||||
children: app.translator.trans('core.forum.edit_user.activate_button'),
|
||||
loading: this.loading,
|
||||
onclick: this.activate.bind(this)
|
||||
})}
|
||||
</div>
|
||||
) : ''}
|
||||
</div>,
|
||||
|
||||
<div className="Form-group">
|
||||
<label>{app.translator.trans('core.forum.edit_user.password_heading')}</label>
|
||||
<div>
|
||||
<label className="checkbox">
|
||||
<input type="checkbox" checked={this.setPassword()} onchange={e => {
|
||||
this.setPassword(e.target.checked);
|
||||
m.redraw(true);
|
||||
if (e.target.checked) this.$('[name=password]').select();
|
||||
m.redraw.strategy('none');
|
||||
}}/>
|
||||
{app.translator.trans('core.forum.edit_user.set_password_label')}
|
||||
</label>
|
||||
{this.setPassword() ? (
|
||||
<input className="FormControl" type="password" name="password" placeholder={extractText(app.translator.trans('core.forum.edit_user.password_label'))}
|
||||
bidi={this.password} />
|
||||
) : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
] : ''}
|
||||
|
||||
<div className="Form-group EditUserModal-groups">
|
||||
<label>Groups</label>
|
||||
<label>{app.translator.trans('core.forum.edit_user.groups_heading')}</label>
|
||||
<div>
|
||||
{Object.keys(this.groups)
|
||||
.map(id => app.store.getById('groups', id))
|
||||
.map(group => (
|
||||
<label className="checkbox">
|
||||
<input type="checkbox"
|
||||
checked={this.groups[group.id()]()}
|
||||
disabled={this.props.user.id() === '1' && group.id() === Group.ADMINISTRATOR_ID}
|
||||
onchange={m.withAttr('checked', this.groups[group.id()])}/>
|
||||
bidi={this.groups[group.id()]}
|
||||
disabled={this.props.user.id() === '1' && group.id() === Group.ADMINISTRATOR_ID} />
|
||||
{GroupBadge.component({group, label: ''})} {group.nameSingular()}
|
||||
</label>
|
||||
))}
|
||||
@@ -93,7 +103,7 @@ export default class EditUserModal extends Modal {
|
||||
className: 'Button Button--primary',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.trans('core.save_changes')
|
||||
children: app.translator.trans('core.forum.edit_user.submit_button')
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,31 +111,55 @@ export default class EditUserModal extends Modal {
|
||||
);
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
activate() {
|
||||
this.loading = true;
|
||||
const data = {
|
||||
username: this.username(),
|
||||
isActivated: true,
|
||||
};
|
||||
this.props.user.save(data, {errorHandler: this.onerror.bind(this)})
|
||||
.then(() => {
|
||||
this.isActivated(true);
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
data() {
|
||||
const groups = Object.keys(this.groups)
|
||||
.filter(id => this.groups[id]())
|
||||
.map(id => app.store.getById('groups', id));
|
||||
|
||||
const data = {
|
||||
username: this.username(),
|
||||
email: this.email(),
|
||||
relationships: {groups}
|
||||
};
|
||||
|
||||
if (app.session.user !== this.props.user) {
|
||||
data.email = this.email();
|
||||
}
|
||||
|
||||
if (this.setPassword()) {
|
||||
data.password = this.password();
|
||||
}
|
||||
|
||||
this.props.user.save(data).then(
|
||||
() => this.hide(),
|
||||
response => {
|
||||
return data;
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.props.user.save(this.data(), {errorHandler: this.onerror.bind(this)})
|
||||
.then(this.hide.bind(this))
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
this.handleErrors(response);
|
||||
}
|
||||
);
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -16,9 +16,11 @@ import icon from 'flarum/helpers/icon';
|
||||
*/
|
||||
export default class EventPost extends Post {
|
||||
attrs() {
|
||||
return {
|
||||
className: 'EventPost ' + ucfirst(this.props.post.contentType()) + 'Post'
|
||||
};
|
||||
const attrs = super.attrs();
|
||||
|
||||
attrs.className += ' EventPost ' + ucfirst(this.props.post.contentType()) + 'Post';
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
content() {
|
||||
@@ -31,12 +33,12 @@ export default class EventPost extends Post {
|
||||
: username
|
||||
});
|
||||
|
||||
return [
|
||||
return super.content().concat([
|
||||
icon(this.icon(), {className: 'EventPost-icon'}),
|
||||
<div class="EventPost-info">
|
||||
{app.trans(this.descriptionKey(), data)}
|
||||
{this.description(data)}
|
||||
</div>
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +50,16 @@ export default class EventPost extends Post {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description text for the event.
|
||||
*
|
||||
* @param {Object} data
|
||||
* @return {String|Object} The description to render in the DOM
|
||||
*/
|
||||
description(data) {
|
||||
return app.translator.transChoice(this.descriptionKey(), data.count, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the translation key for the description of the event.
|
||||
*
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import Modal from 'flarum/components/Modal';
|
||||
import Alert from 'flarum/components/Alert';
|
||||
import Button from 'flarum/components/Button';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
|
||||
/**
|
||||
* The `ForgotPasswordModal` component displays a modal which allows the user to
|
||||
@@ -11,8 +12,8 @@ import Button from 'flarum/components/Button';
|
||||
* - `email`
|
||||
*/
|
||||
export default class ForgotPasswordModal extends Modal {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
/**
|
||||
* The value of the email input.
|
||||
@@ -34,21 +35,19 @@ export default class ForgotPasswordModal extends Modal {
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.trans('core.forgot_password');
|
||||
return app.translator.trans('core.forum.forgot_password.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
if (this.success) {
|
||||
const emailProviderName = this.email().split('@')[1];
|
||||
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<p className="helpText">{app.trans('core.password_reset_email_sent')}</p>
|
||||
<p className="helpText">{app.translator.trans('core.forum.forgot_password.email_sent_message')}</p>
|
||||
<div className="Form-group">
|
||||
<a href={'http://' + emailProviderName} className="Button Button--primary Button--block">
|
||||
{app.trans('core.go_to', {location: emailProviderName})}
|
||||
</a>
|
||||
<Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}>
|
||||
{app.translator.trans('core.forum.forgot_password.dismiss_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,9 +57,9 @@ export default class ForgotPasswordModal extends Modal {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<p className="helpText">{app.trans('core.forgot_password_help')}</p>
|
||||
<p className="helpText">{app.translator.trans('core.forum.forgot_password.text')}</p>
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="email" type="email" placeholder={app.trans('core.email')}
|
||||
<input className="FormControl" name="email" type="email" placeholder={extractText(app.translator.trans('core.forum.forgot_password.email_placeholder'))}
|
||||
value={this.email()}
|
||||
onchange={m.withAttr('value', this.email)}
|
||||
disabled={this.loading} />
|
||||
@@ -70,7 +69,7 @@ export default class ForgotPasswordModal extends Modal {
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.trans('core.recover_password')
|
||||
children: app.translator.trans('core.forum.forgot_password.submit_button')
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,23 +86,21 @@ export default class ForgotPasswordModal extends Modal {
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/forgot',
|
||||
data: {email: this.email()},
|
||||
handlers: {
|
||||
404: () => {
|
||||
this.alert = new Alert({type: 'warning', message: 'That email wasn\'t found in our database.'});
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
}).then(
|
||||
() => {
|
||||
this.loading = false;
|
||||
errorHandler: this.onerror.bind(this)
|
||||
})
|
||||
.then(() => {
|
||||
this.success = true;
|
||||
this.alert = null;
|
||||
m.redraw();
|
||||
},
|
||||
response => {
|
||||
this.loading = false;
|
||||
this.handleErrors(response);
|
||||
}
|
||||
);
|
||||
})
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import Component from 'flarum/Component';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
import SelectDropdown from 'flarum/components/SelectDropdown';
|
||||
import Button from 'flarum/components/Button';
|
||||
|
||||
/**
|
||||
* The `HeaderPrimary` component displays primary header controls. On the
|
||||
@@ -17,6 +15,13 @@ export default class HeaderPrimary extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
// Since this component is 'above' the content of the page (that is, it is a
|
||||
// part of the global UI that persists between routes), we will flag the DOM
|
||||
// to be retained across route changes.
|
||||
context.retain = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the controls.
|
||||
*
|
||||
|
@@ -22,6 +22,13 @@ export default class HeaderSecondary extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
// Since this component is 'above' the content of the page (that is, it is a
|
||||
// part of the global UI that persists between routes), we will flag the DOM
|
||||
// to be retained across route changes.
|
||||
context.retain = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the controls.
|
||||
*
|
||||
@@ -32,14 +39,14 @@ export default class HeaderSecondary extends Component {
|
||||
|
||||
items.add('search', app.search.render(), 30);
|
||||
|
||||
if (Object.keys(app.locales).length > 1) {
|
||||
if (app.forum.attribute("showLanguageSelector") && Object.keys(app.data.locales).length > 1) {
|
||||
const locales = [];
|
||||
|
||||
for (const locale in app.locales) {
|
||||
for (const locale in app.data.locales) {
|
||||
locales.push(Button.component({
|
||||
active: app.locale === locale,
|
||||
children: app.locales[locale],
|
||||
icon: app.locale === locale ? 'check' : true,
|
||||
active: app.data.locale === locale,
|
||||
children: app.data.locales[locale],
|
||||
icon: app.data.locale === locale ? 'check' : true,
|
||||
onclick: () => {
|
||||
if (app.session.user) {
|
||||
app.session.user.savePreferences({locale}).then(() => window.location.reload());
|
||||
@@ -64,7 +71,7 @@ export default class HeaderSecondary extends Component {
|
||||
if (app.forum.attribute('allowSignUp')) {
|
||||
items.add('signUp',
|
||||
Button.component({
|
||||
children: app.trans('core.sign_up'),
|
||||
children: app.translator.trans('core.forum.header.sign_up_link'),
|
||||
className: 'Button Button--link',
|
||||
onclick: () => app.modal.show(new SignUpModal())
|
||||
}), 10
|
||||
@@ -73,7 +80,7 @@ export default class HeaderSecondary extends Component {
|
||||
|
||||
items.add('logIn',
|
||||
Button.component({
|
||||
children: app.trans('core.log_in'),
|
||||
children: app.translator.trans('core.forum.header.log_in_link'),
|
||||
className: 'Button Button--link',
|
||||
onclick: () => app.modal.show(new LogInModal())
|
||||
}), 0
|
||||
|
@@ -2,12 +2,13 @@ import { extend } from 'flarum/extend';
|
||||
import Page from 'flarum/components/Page';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import DiscussionList from 'flarum/components/DiscussionList';
|
||||
import WelcomeHero from 'flarum/components/WelcomeHero';
|
||||
import DiscussionComposer from 'flarum/components/DiscussionComposer';
|
||||
import LogInModal from 'flarum/components/LogInModal';
|
||||
import DiscussionPage from 'flarum/components/DiscussionPage';
|
||||
import Select from 'flarum/components/Select';
|
||||
import Dropdown from 'flarum/components/Dropdown';
|
||||
import Button from 'flarum/components/Button';
|
||||
import LinkButton from 'flarum/components/LinkButton';
|
||||
import SelectDropdown from 'flarum/components/SelectDropdown';
|
||||
@@ -17,8 +18,8 @@ import SelectDropdown from 'flarum/components/SelectDropdown';
|
||||
* hero, the sidebar, and the discussion list.
|
||||
*/
|
||||
export default class IndexPage extends Page {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
// If the user is returning from a discussion page, then take note of which
|
||||
// discussion they have just visited. After the view is rendered, we will
|
||||
@@ -54,7 +55,7 @@ export default class IndexPage extends Page {
|
||||
app.cache.discussionList = new DiscussionList({params});
|
||||
}
|
||||
|
||||
app.history.push('index');
|
||||
app.history.push('index', app.translator.trans('core.forum.header.back_to_index_tooltip'));
|
||||
|
||||
this.bodyClass = 'App--index';
|
||||
}
|
||||
@@ -97,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
|
||||
@@ -143,11 +149,11 @@ export default class IndexPage extends Page {
|
||||
*/
|
||||
sidebarItems() {
|
||||
const items = new ItemList();
|
||||
const canStartDiscussion = app.forum.canStartDiscussion() || !app.session.user;
|
||||
const canStartDiscussion = app.forum.attribute('canStartDiscussion') || !app.session.user;
|
||||
|
||||
items.add('newDiscussion',
|
||||
Button.component({
|
||||
children: canStartDiscussion ? app.trans('core.start_a_discussion') : 'Can\'t Start Discussion',
|
||||
children: app.translator.trans(canStartDiscussion ? 'core.forum.index.start_discussion_button' : 'core.forum.index.cannot_start_discussion_button'),
|
||||
icon: 'edit',
|
||||
className: 'Button Button--primary IndexPage-newDiscussion',
|
||||
itemClassName: 'App-primaryControl',
|
||||
@@ -180,7 +186,7 @@ export default class IndexPage extends Page {
|
||||
items.add('allDiscussions',
|
||||
LinkButton.component({
|
||||
href: app.route('index', params),
|
||||
children: app.trans('core.all_discussions'),
|
||||
children: app.translator.trans('core.forum.index.all_discussions_link'),
|
||||
icon: 'comments-o'
|
||||
}),
|
||||
100
|
||||
@@ -198,17 +204,28 @@ 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()) {
|
||||
sortOptions[i] = app.trans('core.sort_' + i);
|
||||
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,
|
||||
onchange: this.changeSort.bind(this)
|
||||
Dropdown.component({
|
||||
buttonClassName: 'Button',
|
||||
label: sortOptions[this.params().sort] || Object.keys(sortMap).map(key => sortOptions[key])[0],
|
||||
children: Object.keys(sortOptions).map(value => {
|
||||
const label = sortOptions[value];
|
||||
const active = (this.params().sort || Object.keys(sortMap)[0]) === value;
|
||||
|
||||
return Button.component({
|
||||
children: label,
|
||||
icon: active ? 'check' : true,
|
||||
onclick: this.changeSort.bind(this, value),
|
||||
active: active,
|
||||
})
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -226,17 +243,23 @@ export default class IndexPage extends Page {
|
||||
|
||||
items.add('refresh',
|
||||
Button.component({
|
||||
title: app.trans('core.refresh'),
|
||||
title: app.translator.trans('core.forum.index.refresh_tooltip'),
|
||||
icon: 'refresh',
|
||||
className: 'Button Button--icon',
|
||||
onclick: () => app.cache.discussionList.refresh()
|
||||
onclick: () => {
|
||||
app.cache.discussionList.refresh();
|
||||
if (app.session.user) {
|
||||
app.store.find('users', app.session.user.id());
|
||||
m.redraw();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (app.session.user) {
|
||||
items.add('markAllAsRead',
|
||||
Button.component({
|
||||
title: app.trans('core.mark_all_as_read'),
|
||||
title: app.translator.trans('core.forum.index.mark_all_as_read_tooltip'),
|
||||
icon: 'check',
|
||||
className: 'Button Button--icon',
|
||||
onclick: this.markAllAsRead.bind(this)
|
||||
@@ -357,6 +380,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()});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
30
js/forum/src/components/LogInButton.js
Normal file
30
js/forum/src/components/LogInButton.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import Button from 'flarum/components/Button';
|
||||
|
||||
/**
|
||||
* The `LogInButton` component displays a social login button which will open
|
||||
* a popup window containing the specified path.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `path`
|
||||
*/
|
||||
export default class LogInButton extends Button {
|
||||
static initProps(props) {
|
||||
props.className = (props.className || '') + ' LogInButton';
|
||||
|
||||
props.onclick = function() {
|
||||
const width = 600;
|
||||
const height = 400;
|
||||
const $window = $(window);
|
||||
|
||||
window.open(app.forum.attribute('baseUrl') + props.path, 'logInPopup',
|
||||
`width=${width},` +
|
||||
`height=${height},` +
|
||||
`top=${$window.height() / 2 - height / 2},` +
|
||||
`left=${$window.width() / 2 - width / 2},` +
|
||||
'status=no,scrollbars=no,resizable=no');
|
||||
};
|
||||
|
||||
super.initProps(props);
|
||||
}
|
||||
}
|
25
js/forum/src/components/LogInButtons.js
Normal file
25
js/forum/src/components/LogInButtons.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import Component from 'flarum/Component';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
|
||||
/**
|
||||
* The `LogInButtons` component displays a collection of social login buttons.
|
||||
*/
|
||||
export default class LogInButtons extends Component {
|
||||
view() {
|
||||
return (
|
||||
<div className="LogInButtons">
|
||||
{this.items().toArray()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a list of LogInButton components.
|
||||
*
|
||||
* @return {ItemList}
|
||||
* @public
|
||||
*/
|
||||
items() {
|
||||
return new ItemList();
|
||||
}
|
||||
}
|
@@ -3,25 +3,27 @@ import ForgotPasswordModal from 'flarum/components/ForgotPasswordModal';
|
||||
import SignUpModal from 'flarum/components/SignUpModal';
|
||||
import Alert from 'flarum/components/Alert';
|
||||
import Button from 'flarum/components/Button';
|
||||
import LogInButtons from 'flarum/components/LogInButtons';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
|
||||
/**
|
||||
* The `LogInModal` component displays a modal dialog with a login form.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `email`
|
||||
* - `identification`
|
||||
* - `password`
|
||||
*/
|
||||
export default class LogInModal extends Modal {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
/**
|
||||
* The value of the email input.
|
||||
* The value of the identification input.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.email = m.prop(this.props.email || '');
|
||||
this.identification = m.prop(this.props.identification || '');
|
||||
|
||||
/**
|
||||
* The value of the password input.
|
||||
@@ -29,6 +31,13 @@ export default class LogInModal extends Modal {
|
||||
* @type {Function}
|
||||
*/
|
||||
this.password = m.prop(this.props.password || '');
|
||||
|
||||
/**
|
||||
* The value of the remember me input.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.remember = m.prop(!!this.props.remember);
|
||||
}
|
||||
|
||||
className() {
|
||||
@@ -36,45 +45,54 @@ export default class LogInModal extends Modal {
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.trans('core.log_in');
|
||||
return app.translator.trans('core.forum.log_in.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
return [
|
||||
<div className="Modal-body">
|
||||
<LogInButtons/>
|
||||
|
||||
<div className="Form Form--centered">
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="email" placeholder={app.trans('core.username_or_email')}
|
||||
value={this.email()}
|
||||
onchange={m.withAttr('value', this.email)}
|
||||
<input className="FormControl" name="identification" type="text" placeholder={extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'))}
|
||||
bidi={this.identification}
|
||||
disabled={this.loading} />
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="password" type="password" placeholder={app.trans('core.password')}
|
||||
value={this.password()}
|
||||
onchange={m.withAttr('value', this.password)}
|
||||
<input className="FormControl" name="password" type="password" placeholder={extractText(app.translator.trans('core.forum.log_in.password_placeholder'))}
|
||||
bidi={this.password}
|
||||
disabled={this.loading} />
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
<div>
|
||||
<label className="checkbox">
|
||||
<input type="checkbox" bidi={this.remember} disabled={this.loading} />
|
||||
{app.translator.trans('core.forum.log_in.remember_me_label')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.trans('core.log_in')
|
||||
children: app.translator.trans('core.forum.log_in.submit_button')
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
<div className="Modal-footer">
|
||||
<p className="LogInModal-forgotPassword">
|
||||
<a onclick={this.forgotPassword.bind(this)}>{app.trans('core.forgot_password_link')}</a>
|
||||
<a onclick={this.forgotPassword.bind(this)}>{app.translator.trans('core.forum.log_in.forgot_password_link')}</a>
|
||||
</p>
|
||||
|
||||
{app.forum.attribute('allowSignUp') ? (
|
||||
<p className="LogInModal-signUp">
|
||||
{app.trans('core.before_sign_up_link')}{' '}
|
||||
<a onclick={this.signUp.bind(this)}>{app.trans('core.sign_up')}</a>
|
||||
{app.translator.trans('core.forum.log_in.sign_up_text', {a: <a onclick={this.signUp.bind(this)}/>})}
|
||||
</p>
|
||||
) : ''}
|
||||
</div>
|
||||
@@ -84,9 +102,11 @@ export default class LogInModal extends Modal {
|
||||
/**
|
||||
* Open the forgot password modal, prefilling it with an email if the user has
|
||||
* entered one.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
forgotPassword() {
|
||||
const email = this.email();
|
||||
const email = this.identification();
|
||||
const props = email.indexOf('@') !== -1 ? {email} : undefined;
|
||||
|
||||
app.modal.show(new ForgotPasswordModal(props));
|
||||
@@ -95,17 +115,19 @@ export default class LogInModal extends Modal {
|
||||
/**
|
||||
* Open the sign up modal, prefilling it with an email/username/password if
|
||||
* the user has entered one.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
signUp() {
|
||||
const props = {password: this.password()};
|
||||
const email = this.email();
|
||||
props[email.indexOf('@') !== -1 ? 'email' : 'username'] = email;
|
||||
const identification = this.identification();
|
||||
props[identification.indexOf('@') !== -1 ? 'email' : 'username'] = identification;
|
||||
|
||||
app.modal.show(new SignUpModal(props));
|
||||
}
|
||||
|
||||
onready() {
|
||||
this.$('[name=' + (this.email() ? 'password' : 'email') + ']').select();
|
||||
this.$('[name=' + (this.identification() ? 'password' : 'identification') + ']').select();
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
@@ -113,28 +135,22 @@ export default class LogInModal extends Modal {
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const email = this.email();
|
||||
const identification = this.identification();
|
||||
const password = this.password();
|
||||
const remember = this.remember();
|
||||
|
||||
app.session.login(email, password).then(
|
||||
null,
|
||||
response => {
|
||||
this.loading = false;
|
||||
app.session.login({identification, password, remember}, {errorHandler: this.onerror.bind(this)})
|
||||
.then(
|
||||
() => window.location.reload(),
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
if (response && response.code === 'confirm_email') {
|
||||
this.alert = Alert.component({
|
||||
children: app.trans('core.email_confirmation_required', {email: response.email})
|
||||
});
|
||||
} else {
|
||||
this.alert = Alert.component({
|
||||
type: 'error',
|
||||
children: app.trans('core.invalid_login')
|
||||
});
|
||||
}
|
||||
onerror(error) {
|
||||
if (error.status === 401) {
|
||||
error.alert.props.children = app.translator.trans('core.forum.log_in.invalid_login_message');
|
||||
}
|
||||
|
||||
m.redraw();
|
||||
this.onready();
|
||||
}
|
||||
);
|
||||
super.onerror(error);
|
||||
}
|
||||
}
|
||||
|
@@ -77,6 +77,10 @@ export default class Notification extends Component {
|
||||
* Mark the notification as read.
|
||||
*/
|
||||
markAsRead() {
|
||||
if (this.props.notification.isRead()) return;
|
||||
|
||||
app.session.user.pushAttributes({unreadNotificationsCount: app.session.user.unreadNotificationsCount() - 1});
|
||||
|
||||
this.props.notification.save({isRead: true});
|
||||
}
|
||||
}
|
||||
|
@@ -12,17 +12,15 @@ import ItemList from 'flarum/utils/ItemList';
|
||||
* - `user`
|
||||
*/
|
||||
export default class NotificationGrid extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
init() {
|
||||
/**
|
||||
* Information about the available notification methods.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
this.methods = [
|
||||
{name: 'alert', icon: 'bell', label: app.trans('core.alert')},
|
||||
{name: 'email', icon: 'envelope-o', label: app.trans('core.email')}
|
||||
{name: 'alert', icon: 'bell', label: app.translator.trans('core.forum.settings.notify_by_web_heading')},
|
||||
{name: 'email', icon: 'envelope-o', label: app.translator.trans('core.forum.settings.notify_by_email_heading')}
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -182,7 +180,7 @@ export default class NotificationGrid extends Component {
|
||||
items.add('discussionRenamed', {
|
||||
name: 'discussionRenamed',
|
||||
icon: 'pencil',
|
||||
label: app.trans('core.notify_discussion_renamed')
|
||||
label: app.translator.trans('core.forum.settings.notify_discussion_renamed_label')
|
||||
});
|
||||
|
||||
return items;
|
||||
|
@@ -9,9 +9,7 @@ import Discussion from 'flarum/models/Discussion';
|
||||
* notifications, grouped by discussion.
|
||||
*/
|
||||
export default class NotificationList extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
init() {
|
||||
/**
|
||||
* Whether or not the notifications are loading.
|
||||
*
|
||||
@@ -59,12 +57,12 @@ export default class NotificationList extends Component {
|
||||
{Button.component({
|
||||
className: 'Button Button--icon Button--link',
|
||||
icon: 'check',
|
||||
title: app.trans('core.mark_all_as_read'),
|
||||
title: app.translator.trans('core.forum.notifications.mark_all_as_read_tooltip'),
|
||||
onclick: this.markAllAsRead.bind(this)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<h4 className="App-titleControl App-titleControl--text">{app.trans('core.notifications')}</h4>
|
||||
<h4 className="App-titleControl App-titleControl--text">{app.translator.trans('core.forum.notifications.title')}</h4>
|
||||
</div>
|
||||
|
||||
<div className="NotificationList-content">
|
||||
@@ -98,7 +96,7 @@ export default class NotificationList extends Component {
|
||||
);
|
||||
})
|
||||
: !this.loading
|
||||
? <div className="NotificationList-empty">{app.trans('core.no_notifications')}</div>
|
||||
? <div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>
|
||||
: LoadingIndicator.component({className: 'LoadingIndicator--block'})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,20 +108,23 @@ export default class NotificationList extends Component {
|
||||
* been loaded.
|
||||
*/
|
||||
load() {
|
||||
if (app.cache.notifications && !app.session.user.unreadNotificationsCount()) {
|
||||
if (app.cache.notifications && !app.session.user.newNotificationsCount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
app.store.find('notifications').then(notifications => {
|
||||
app.session.user.pushAttributes({unreadNotificationsCount: 0});
|
||||
app.cache.notifications = notifications.sort((a, b) => b.time() - a.time());
|
||||
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
app.store.find('notifications')
|
||||
.then(notifications => {
|
||||
app.session.user.pushAttributes({newNotificationsCount: 0});
|
||||
app.cache.notifications = notifications.sort((a, b) => b.time() - a.time());
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,10 +133,13 @@ export default class NotificationList extends Component {
|
||||
markAllAsRead() {
|
||||
if (!app.cache.notifications) return;
|
||||
|
||||
app.cache.notifications.forEach(notification => {
|
||||
if (!notification.isRead()) {
|
||||
notification.save({isRead: true});
|
||||
}
|
||||
app.session.user.pushAttributes({unreadNotificationsCount: 0});
|
||||
|
||||
app.cache.notifications.forEach(notification => notification.pushAttributes({isRead: true}));
|
||||
|
||||
app.request({
|
||||
url: app.forum.attribute('apiUrl') + '/notifications/read',
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -7,30 +7,25 @@ export default class NotificationsDropdown extends Dropdown {
|
||||
props.className = props.className || 'NotificationsDropdown';
|
||||
props.buttonClassName = props.buttonClassName || 'Button Button--flat';
|
||||
props.menuClassName = props.menuClassName || 'Dropdown-menu--right';
|
||||
props.label = props.label || app.trans('core.notifications');
|
||||
props.label = props.label || app.translator.trans('core.forum.notifications.tooltip');
|
||||
props.icon = props.icon || 'bell';
|
||||
|
||||
super.initProps(props);
|
||||
}
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* Whether or not the notifications dropdown is visible.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.showing = false;
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.list = new NotificationList();
|
||||
}
|
||||
|
||||
getButton() {
|
||||
const unread = this.getUnreadCount();
|
||||
const newNotifications = this.getNewCount();
|
||||
const vdom = super.getButton();
|
||||
|
||||
vdom.attrs.className += (unread ? ' unread' : '');
|
||||
vdom.attrs.title = this.props.label;
|
||||
|
||||
vdom.attrs.className += (newNotifications ? ' new' : '');
|
||||
vdom.attrs.onclick = this.onclick.bind(this);
|
||||
|
||||
return vdom;
|
||||
@@ -58,7 +53,6 @@ export default class NotificationsDropdown extends Dropdown {
|
||||
if (app.drawer.isOpen()) {
|
||||
this.goToRoute();
|
||||
} else {
|
||||
this.showing = true;
|
||||
this.list.load();
|
||||
}
|
||||
}
|
||||
@@ -71,6 +65,10 @@ export default class NotificationsDropdown extends Dropdown {
|
||||
return app.session.user.unreadNotificationsCount();
|
||||
}
|
||||
|
||||
getNewCount() {
|
||||
return app.session.user.newNotificationsCount();
|
||||
}
|
||||
|
||||
menuClick(e) {
|
||||
// Don't close the notifications dropdown if the user is opening a link in a
|
||||
// new tab or window.
|
||||
|
@@ -6,8 +6,8 @@ import NotificationList from 'flarum/components/NotificationList';
|
||||
* used on mobile devices where the notifications dropdown is within the drawer.
|
||||
*/
|
||||
export default class NotificationsPage extends Page {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
app.history.push('notifications');
|
||||
|
||||
|
@@ -6,9 +6,7 @@ import Component from 'flarum/Component';
|
||||
* @abstract
|
||||
*/
|
||||
export default class Page extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
init() {
|
||||
app.previous = app.current;
|
||||
app.current = this;
|
||||
|
||||
|
@@ -2,6 +2,8 @@ import Component from 'flarum/Component';
|
||||
import SubtreeRetainer from 'flarum/utils/SubtreeRetainer';
|
||||
import Dropdown from 'flarum/components/Dropdown';
|
||||
import PostControls from 'flarum/utils/PostControls';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
|
||||
/**
|
||||
* The `Post` component displays a single post. The basic post template just
|
||||
@@ -15,8 +17,8 @@ import PostControls from 'flarum/utils/PostControls';
|
||||
* @abstract
|
||||
*/
|
||||
export default class Post extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
this.loading = false;
|
||||
|
||||
/**
|
||||
* Set up a subtree retainer so that the post will not be redrawn
|
||||
@@ -29,14 +31,15 @@ export default class Post extends Component {
|
||||
() => {
|
||||
const user = this.props.post.user();
|
||||
return user && user.freshness;
|
||||
}
|
||||
},
|
||||
() => this.controlsOpen
|
||||
);
|
||||
}
|
||||
|
||||
view() {
|
||||
const attrs = this.attrs();
|
||||
|
||||
attrs.className = 'Post ' + (attrs.className || '');
|
||||
attrs.className = 'Post ' + (this.loading ? 'Post--loading ' : '') + (attrs.className || '');
|
||||
|
||||
return (
|
||||
<article {...attrs}>
|
||||
@@ -45,15 +48,24 @@ export default class Post extends Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{controls.length ? Dropdown.component({
|
||||
children: controls,
|
||||
className: 'Post-controls',
|
||||
buttonClassName: 'Button Button--icon Button--flat',
|
||||
menuClassName: 'Dropdown-menu--right',
|
||||
icon: 'ellipsis-v'
|
||||
}) : ''}
|
||||
|
||||
{this.content()}
|
||||
<aside className="Post-actions">
|
||||
<ul>
|
||||
{listItems(this.actionItems().toArray())}
|
||||
{controls.length ? <li>
|
||||
<Dropdown
|
||||
className="Post-controls"
|
||||
buttonClassName="Button Button--icon Button--flat"
|
||||
menuClassName="Dropdown-menu--right"
|
||||
icon="ellipsis-h"
|
||||
onshow={() => this.$('.Post-actions').addClass('open')}
|
||||
onhide={() => this.$('.Post-actions').removeClass('open')}>
|
||||
{controls}
|
||||
</Dropdown>
|
||||
</li> : ''}
|
||||
</ul>
|
||||
</aside>
|
||||
<footer className="Post-footer"><ul>{listItems(this.footerItems().toArray())}</ul></footer>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
@@ -61,6 +73,13 @@ export default class Post extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized) {
|
||||
const $actions = this.$('.Post-actions');
|
||||
const $controls = this.$('.Post-controls');
|
||||
|
||||
$actions.toggleClass('open', $controls.hasClass('open'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attributes for the post element.
|
||||
*
|
||||
@@ -73,9 +92,27 @@ export default class Post extends Component {
|
||||
/**
|
||||
* Get the post's content.
|
||||
*
|
||||
* @return {Object}
|
||||
* @return {Array}
|
||||
*/
|
||||
content() {
|
||||
return '';
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the post's actions.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
actionItems() {
|
||||
return new ItemList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the post's footer.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
footerItems() {
|
||||
return new ItemList();
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import Component from 'flarum/Component';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import humanTime from 'flarum/utils/humanTime';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
|
||||
@@ -12,19 +11,34 @@ import extractText from 'flarum/utils/extractText';
|
||||
* - `post`
|
||||
*/
|
||||
export default class PostEdited extends Component {
|
||||
init() {
|
||||
this.shouldUpdateTooltip = false;
|
||||
this.oldEditedInfo = null;
|
||||
}
|
||||
|
||||
view() {
|
||||
const post = this.props.post;
|
||||
const editUser = post.editUser();
|
||||
const title = extractText(app.trans('core.post_edited', {user: editUser, ago: humanTime(post.editTime())}));
|
||||
const editedInfo = extractText(app.translator.trans(
|
||||
'core.forum.post.edited_tooltip',
|
||||
{user: editUser, ago: humanTime(post.editTime())}
|
||||
));
|
||||
if (editedInfo !== this.oldEditedInfo) {
|
||||
this.shouldUpdateTooltip = true;
|
||||
this.oldEditedInfo = editedInfo;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="PostEdited" title={title}>{icon('pencil')}</span>
|
||||
<span className="PostEdited" title={editedInfo}>
|
||||
{app.translator.trans('core.forum.post.edited_text')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized) {
|
||||
if (isInitialized) return;
|
||||
|
||||
this.$().tooltip();
|
||||
if (this.shouldUpdateTooltip) {
|
||||
this.$().tooltip('destroy').tooltip();
|
||||
this.shouldUpdateTooltip = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ export default class PostMeta extends Component {
|
||||
view() {
|
||||
const post = this.props.post;
|
||||
const time = post.time();
|
||||
const permalink = window.location.origin + app.route.post(post);
|
||||
const permalink = this.getPermalink(post);
|
||||
const touch = 'ontouchstart' in document.documentElement;
|
||||
|
||||
// When the dropdown menu is shown, select the contents of the permalink
|
||||
@@ -33,8 +33,9 @@ export default class PostMeta extends Component {
|
||||
</a>
|
||||
|
||||
<div className="Dropdown-menu dropdown-menu">
|
||||
<span className="PostMeta-number">{app.trans('core.post_number', {number: post.number()})}</span>{' '}
|
||||
{fullTime(time)}
|
||||
<span className="PostMeta-number">{app.translator.trans('core.forum.post.number_tooltip', {number: post.number()})}</span>{' '}
|
||||
<span className="PostMeta-time">{fullTime(time)}</span>{' '}
|
||||
<span className="PostMeta-ip">{post.data.attributes.ipAddress}</span>
|
||||
{touch
|
||||
? <a className="Button PostMeta-permalink" href={permalink}>{permalink}</a>
|
||||
: <input className="FormControl PostMeta-permalink" value={permalink} onclick={e => e.stopPropagation()} />}
|
||||
@@ -42,4 +43,14 @@ export default class PostMeta extends Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permalink for the given post.
|
||||
*
|
||||
* @param {Post} post
|
||||
* @returns {String}
|
||||
*/
|
||||
getPermalink(post) {
|
||||
return window.location.origin + app.route.post(post);
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -15,10 +16,8 @@ import ReplyPlaceholder from 'flarum/components/ReplyPlaceholder';
|
||||
* - `discussion`
|
||||
* - `includedPosts`
|
||||
*/
|
||||
class PostStream extends mixin(Component, evented) {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
class PostStream extends Component {
|
||||
init() {
|
||||
/**
|
||||
* The discussion to display the post stream for.
|
||||
*
|
||||
@@ -56,7 +55,9 @@ class PostStream extends mixin(Component, evented) {
|
||||
return this.goToLast().then(() => {
|
||||
$('html,body').stop(true).animate({
|
||||
scrollTop: $(document).height() - $(window).height()
|
||||
}, 'fast');
|
||||
}, 'fast', () => {
|
||||
this.flashItem(this.$('.PostStream-item:last-child'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -181,7 +182,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
.map(id => {
|
||||
const post = app.store.getById('posts', id);
|
||||
|
||||
return post && post.discussion() && post.user() !== false ? post : null;
|
||||
return post && post.discussion() && typeof post.canEdit() !== 'undefined' ? post : null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,58 +197,72 @@ class PostStream extends mixin(Component, evented) {
|
||||
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.trans('core.period_later', {period: moment.duration(dt).humanize()})}</span>
|
||||
</div>,
|
||||
content
|
||||
];
|
||||
}
|
||||
|
||||
lastTime = time;
|
||||
} else {
|
||||
attrs.key = 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>
|
||||
);
|
||||
}
|
||||
@@ -277,7 +292,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
const marginTop = this.getMarginTop();
|
||||
const viewportHeight = $(window).height() - marginTop;
|
||||
const viewportTop = top + marginTop;
|
||||
const loadAheadDistance = 500;
|
||||
const loadAheadDistance = 300;
|
||||
|
||||
if (this.visibleStart > 0) {
|
||||
const $item = this.$('.PostStream-item[data-index=' + this.visibleStart + ']');
|
||||
@@ -391,7 +406,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
this.discussion.postIds().slice(start, end).forEach(id => {
|
||||
const post = app.store.getById('posts', id);
|
||||
|
||||
if (post && post.discussion()) {
|
||||
if (post && post.discussion() && typeof post.canEdit() !== 'undefined') {
|
||||
loaded.push(post);
|
||||
} else {
|
||||
loadIds.push(id);
|
||||
@@ -464,7 +479,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
|
||||
if (top + height > scrollTop) {
|
||||
if (!startNumber) {
|
||||
startNumber = $item.data('number');
|
||||
startNumber = endNumber = $item.data('number');
|
||||
}
|
||||
|
||||
if (top + height < scrollTop + viewportHeight) {
|
||||
@@ -583,4 +598,6 @@ class PostStream extends mixin(Component, evented) {
|
||||
*/
|
||||
PostStream.loadCount = 20;
|
||||
|
||||
Object.assign(PostStream.prototype, evented);
|
||||
|
||||
export default PostStream;
|
||||
|
@@ -15,9 +15,7 @@ import formatNumber from 'flarum/utils/formatNumber';
|
||||
* - `className`
|
||||
*/
|
||||
export default class PostStreamScrubber extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
init() {
|
||||
this.handlers = {};
|
||||
|
||||
/**
|
||||
@@ -41,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,12 +56,13 @@ export default class PostStreamScrubber extends Component {
|
||||
|
||||
view() {
|
||||
const retain = this.subtree.retain();
|
||||
const count = this.count();
|
||||
const unreadCount = this.props.stream.discussion.unreadCount();
|
||||
const unreadPercent = Math.min(this.count() - this.index, unreadCount) / this.count();
|
||||
const unreadPercent = count ? Math.min(count - this.index, unreadCount) / count : 0;
|
||||
|
||||
const viewing = app.trans('core.viewing_posts', {
|
||||
index: <span className="Scrubber-index">{retain || formatNumber(this.visibleIndex())}</span>,
|
||||
count: <span className="Scrubber-count">{formatNumber(this.count())}</span>
|
||||
const viewing = app.translator.transChoice('core.forum.post_scrubber.viewing_text', count, {
|
||||
index: <span className="Scrubber-index">{retain || formatNumber(Math.min(Math.ceil(this.index + this.visible), count))}</span>,
|
||||
count: <span className="Scrubber-count">{formatNumber(count)}</span>
|
||||
});
|
||||
|
||||
function styleUnread(element, isInitialized, context) {
|
||||
@@ -101,7 +90,7 @@ export default class PostStreamScrubber extends Component {
|
||||
<div className="Dropdown-menu dropdown-menu">
|
||||
<div className="Scrubber">
|
||||
<a className="Scrubber-first" onclick={this.goToFirst.bind(this)}>
|
||||
{icon('angle-double-up')} {app.trans('core.original_post')}
|
||||
{icon('angle-double-up')} {app.translator.trans('core.forum.post_scrubber.original_post_link')}
|
||||
</a>
|
||||
|
||||
<div className="Scrubber-scrollbar">
|
||||
@@ -116,12 +105,12 @@ export default class PostStreamScrubber extends Component {
|
||||
<div className="Scrubber-after"/>
|
||||
|
||||
<div className="Scrubber-unread" config={styleUnread}>
|
||||
{app.trans('core.unread_posts', {count: unreadCount})}
|
||||
{app.translator.trans('core.forum.post_scrubber.unread_text', {count: unreadCount})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a className="Scrubber-last" onclick={this.goToLast.bind(this)}>
|
||||
{icon('angle-double-down')} {app.trans('core.now')}
|
||||
{icon('angle-double-down')} {app.translator.trans('core.forum.post_scrubber.now_link')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,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
|
||||
@@ -220,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.
|
||||
@@ -329,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());
|
||||
|
||||
@@ -410,7 +396,7 @@ export default class PostStreamScrubber extends Component {
|
||||
// the index at which the drag was started, and then scroll there.
|
||||
const deltaPixels = (e.clientY || e.originalEvent.touches[0].clientY) - this.mouseStart;
|
||||
const deltaPercent = deltaPixels / this.$('.Scrubber-scrollbar').outerHeight() * 100;
|
||||
const deltaIndex = deltaPercent / this.percentPerPost().index;
|
||||
const deltaIndex = (deltaPercent / this.percentPerPost().index) || 0;
|
||||
const newIndex = Math.min(this.indexStart + deltaIndex, this.count() - 1);
|
||||
|
||||
this.index = Math.max(0, newIndex);
|
||||
|
@@ -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';
|
||||
|
||||
/**
|
||||
@@ -12,9 +13,7 @@ import listItems from 'flarum/helpers/listItems';
|
||||
* - `post`
|
||||
*/
|
||||
export default class PostUser extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
init() {
|
||||
/**
|
||||
* Whether or not the user hover card is visible.
|
||||
*
|
||||
@@ -47,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)}
|
||||
|
@@ -8,8 +8,8 @@ import CommentPost from 'flarum/components/CommentPost';
|
||||
* profile.
|
||||
*/
|
||||
export default class PostsUserPage extends UserPage {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
/**
|
||||
* Whether or not the activity feed is currently loading.
|
||||
@@ -51,7 +51,7 @@ export default class PostsUserPage extends UserPage {
|
||||
footer = (
|
||||
<div className="PostsUserPage-loadMore">
|
||||
{Button.component({
|
||||
children: app.trans('core.load_more'),
|
||||
children: app.translator.trans('core.forum.user.posts_load_more_button'),
|
||||
className: 'Button',
|
||||
onclick: this.loadMore.bind(this)
|
||||
})}
|
||||
@@ -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>
|
||||
|
70
js/forum/src/components/RenameDiscussionModal.js
Normal file
70
js/forum/src/components/RenameDiscussionModal.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import Modal from 'flarum/components/Modal';
|
||||
import Button from 'flarum/components/Button';
|
||||
|
||||
/**
|
||||
* The 'RenameDiscussionModal' displays a modal dialog with an input to rename a discussion
|
||||
*/
|
||||
export default class RenameDiscussionModal extends Modal {
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.discussion = this.props.discussion;
|
||||
this.currentTitle = this.props.currentTitle;
|
||||
this.newTitle = m.prop(this.currentTitle);
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'RenameDiscussionModal Modal--small';
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.translator.trans('core.forum.rename_discussion.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" bidi={this.newTitle} type="text" />
|
||||
</div>
|
||||
<div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.translator.trans('core.forum.rename_discussion.submit_button')
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const title = this.newTitle;
|
||||
const currentTitle = this.currentTitle;
|
||||
|
||||
// If the title is different to what it was before, then save it. After the
|
||||
// save has completed, update the post stream as there will be a new post
|
||||
// indicating that the discussion was renamed.
|
||||
if (title && title !== currentTitle) {
|
||||
return this.discussion.save({title}).then(() => {
|
||||
if (app.viewingDiscussion(this.discussion)) {
|
||||
app.current.stream.update();
|
||||
}
|
||||
m.redraw();
|
||||
this.hide();
|
||||
}).catch(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,6 +2,14 @@ import ComposerBody from 'flarum/components/ComposerBody';
|
||||
import Alert from 'flarum/components/Alert';
|
||||
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
|
||||
@@ -13,10 +21,12 @@ import icon from 'flarum/helpers/icon';
|
||||
* - `discussion`
|
||||
*/
|
||||
export default class ReplyComposer extends ComposerBody {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.editor.props.preview = e => {
|
||||
minimizeComposerIfFullScreen(e);
|
||||
|
||||
this.editor.props.preview = () => {
|
||||
m.route(app.route.discussion(this.props.discussion, 'reply'));
|
||||
};
|
||||
}
|
||||
@@ -24,18 +34,25 @@ export default class ReplyComposer extends ComposerBody {
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.placeholder = props.placeholder || app.trans('core.write_a_reply');
|
||||
props.submitLabel = props.submitLabel || app.trans('core.post_reply');
|
||||
props.confirmExit = props.confirmExit || app.trans('core.confirm_discard_reply');
|
||||
props.placeholder = props.placeholder || extractText(app.translator.trans('core.forum.composer_reply.body_placeholder'));
|
||||
props.submitLabel = props.submitLabel || app.translator.trans('core.forum.composer_reply.submit_button');
|
||||
props.confirmExit = props.confirmExit || extractText(app.translator.trans('core.forum.composer_reply.discard_confirmation'));
|
||||
}
|
||||
|
||||
headerItems() {
|
||||
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>
|
||||
{icon('reply')} {' '}
|
||||
<a href={app.route.discussion(discussion)} config={routeAndMinimize}>{discussion.title()}</a>
|
||||
</h3>
|
||||
));
|
||||
|
||||
@@ -75,7 +92,7 @@ export default class ReplyComposer extends ComposerBody {
|
||||
let alert;
|
||||
const viewButton = Button.component({
|
||||
className: 'Button Button--link',
|
||||
children: app.trans('core.view'),
|
||||
children: app.translator.trans('core.forum.composer_reply.view_button'),
|
||||
onclick: () => {
|
||||
m.route(app.route.post(post));
|
||||
app.alerts.dismiss(alert);
|
||||
@@ -84,7 +101,7 @@ export default class ReplyComposer extends ComposerBody {
|
||||
app.alerts.show(
|
||||
alert = new Alert({
|
||||
type: 'success',
|
||||
message: app.trans('core.reply_posted'),
|
||||
message: app.translator.trans('core.forum.composer_reply.posted_message'),
|
||||
controls: [viewButton]
|
||||
})
|
||||
);
|
||||
@@ -92,11 +109,7 @@ export default class ReplyComposer extends ComposerBody {
|
||||
|
||||
app.composer.hide();
|
||||
},
|
||||
response => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
app.alertErrors(response.errors);
|
||||
}
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -31,20 +31,15 @@ export default class ReplyPlaceholder extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
function triggerClick(e) {
|
||||
$(this).trigger('click');
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
const reply = () => {
|
||||
DiscussionControls.replyAction.call(this.props.discussion, true);
|
||||
};
|
||||
|
||||
return (
|
||||
<article className="Post ReplyPlaceholder" onclick={reply} onmousedown={triggerClick}>
|
||||
<article className="Post ReplyPlaceholder" onclick={reply}>
|
||||
<header className="Post-header">
|
||||
{avatar(app.session.user, {className: 'PostUser-avatar'})}{' '}
|
||||
{app.trans('core.write_a_reply')}
|
||||
{app.translator.trans('core.forum.post_stream.reply_placeholder')}
|
||||
</header>
|
||||
</article>
|
||||
);
|
||||
|
@@ -2,6 +2,8 @@ import Component from 'flarum/Component';
|
||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import classList from 'flarum/utils/classList';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
import KeyboardNavigatable from 'flarum/utils/KeyboardNavigatable';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import DiscussionsSearchSource from 'flarum/components/DiscussionsSearchSource';
|
||||
import UsersSearchSource from 'flarum/components/UsersSearchSource';
|
||||
@@ -16,15 +18,13 @@ import UsersSearchSource from 'flarum/components/UsersSearchSource';
|
||||
* `clearSearch` method on the controller.
|
||||
*/
|
||||
export default class Search extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
init() {
|
||||
/**
|
||||
* The value of the search input.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.value = m.prop();
|
||||
this.value = m.prop('');
|
||||
|
||||
/**
|
||||
* Whether or not the search input has focus.
|
||||
@@ -83,7 +83,8 @@ export default class Search extends Component {
|
||||
})}>
|
||||
<div className="Search-input">
|
||||
<input className="FormControl"
|
||||
placeholder={app.trans('core.search_forum')}
|
||||
type="search"
|
||||
placeholder={extractText(app.translator.trans('core.forum.header.search_placeholder'))}
|
||||
value={this.value()}
|
||||
oninput={m.withAttr('value', this.value)}
|
||||
onfocus={() => this.hasFocus = true}
|
||||
@@ -122,31 +123,18 @@ export default class Search extends Component {
|
||||
);
|
||||
});
|
||||
|
||||
// Handle navigation key events on the search input.
|
||||
this.$('input')
|
||||
.on('keydown', e => {
|
||||
switch (e.which) {
|
||||
case 40: case 38: // Down/Up
|
||||
this.setIndex(this.getCurrentNumericIndex() + (e.which === 40 ? 1 : -1), true);
|
||||
e.preventDefault();
|
||||
break;
|
||||
const $input = this.$('input');
|
||||
|
||||
case 13: // Return
|
||||
m.route(this.getItem(this.index).find('a').attr('href'));
|
||||
this.$('input').blur();
|
||||
break;
|
||||
this.navigator = new KeyboardNavigatable();
|
||||
this.navigator
|
||||
.onUp(() => this.setIndex(this.getCurrentNumericIndex() - 1, true))
|
||||
.onDown(() => this.setIndex(this.getCurrentNumericIndex() + 1, true))
|
||||
.onSelect(this.selectResult.bind(this))
|
||||
.onCancel(this.clear.bind(this))
|
||||
.bindTo($input);
|
||||
|
||||
case 27: // Escape
|
||||
this.clear();
|
||||
break;
|
||||
|
||||
default:
|
||||
// no default
|
||||
}
|
||||
})
|
||||
|
||||
// Handle input key events on the search input, triggering results to
|
||||
// load.
|
||||
// Handle input key events on the search input, triggering results to load.
|
||||
$input
|
||||
.on('input focus', function() {
|
||||
const query = this.value.toLowerCase();
|
||||
|
||||
@@ -172,6 +160,10 @@ export default class Search extends Component {
|
||||
search.searched.push(query);
|
||||
m.redraw();
|
||||
}, 250);
|
||||
})
|
||||
|
||||
.on('focus', function() {
|
||||
$(this).one('mouseup', e => e.preventDefault()).select();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -184,6 +176,19 @@ export default class Search extends Component {
|
||||
return app.current && typeof app.current.searching === 'function' && app.current.searching();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the currently selected search result and close the list.
|
||||
*/
|
||||
selectResult() {
|
||||
if (this.value()) {
|
||||
m.route(this.getItem(this.index).find('a').attr('href'));
|
||||
} else {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
this.$('input').blur();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the search input and the current controller's active search.
|
||||
*/
|
||||
|
@@ -47,7 +47,7 @@ export default class SessionDropdown extends Dropdown {
|
||||
items.add('profile',
|
||||
LinkButton.component({
|
||||
icon: 'user',
|
||||
children: app.trans('core.profile'),
|
||||
children: app.translator.trans('core.forum.header.profile_button'),
|
||||
href: app.route.user(user)
|
||||
}),
|
||||
100
|
||||
@@ -56,7 +56,7 @@ export default class SessionDropdown extends Dropdown {
|
||||
items.add('settings',
|
||||
LinkButton.component({
|
||||
icon: 'cog',
|
||||
children: app.trans('core.settings'),
|
||||
children: app.translator.trans('core.forum.header.settings_button'),
|
||||
href: app.route('settings')
|
||||
}),
|
||||
50
|
||||
@@ -66,7 +66,7 @@ export default class SessionDropdown extends Dropdown {
|
||||
items.add('administration',
|
||||
LinkButton.component({
|
||||
icon: 'wrench',
|
||||
children: app.trans('core.administration'),
|
||||
children: app.translator.trans('core.forum.header.admin_button'),
|
||||
href: app.forum.attribute('baseUrl') + '/admin',
|
||||
target: '_blank',
|
||||
config: () => {}
|
||||
@@ -80,7 +80,7 @@ export default class SessionDropdown extends Dropdown {
|
||||
items.add('logOut',
|
||||
Button.component({
|
||||
icon: 'sign-out',
|
||||
children: app.trans('core.log_out'),
|
||||
children: app.translator.trans('core.forum.header.log_out_button'),
|
||||
onclick: app.session.logout.bind(app.session)
|
||||
}),
|
||||
-100
|
||||
|
@@ -13,11 +13,11 @@ import listItems from 'flarum/helpers/listItems';
|
||||
* the context of their user profile.
|
||||
*/
|
||||
export default class SettingsPage extends UserPage {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.show(app.session.user);
|
||||
app.setTitle(app.trans('core.settings'));
|
||||
app.setTitle(app.translator.trans('core.forum.settings.title'));
|
||||
}
|
||||
|
||||
content() {
|
||||
@@ -38,7 +38,7 @@ export default class SettingsPage extends UserPage {
|
||||
|
||||
items.add('account',
|
||||
FieldSet.component({
|
||||
label: app.trans('core.account'),
|
||||
label: app.translator.trans('core.forum.settings.account_heading'),
|
||||
className: 'Settings-account',
|
||||
children: this.accountItems().toArray()
|
||||
})
|
||||
@@ -46,15 +46,15 @@ export default class SettingsPage extends UserPage {
|
||||
|
||||
items.add('notifications',
|
||||
FieldSet.component({
|
||||
label: app.trans('core.notifications'),
|
||||
label: app.translator.trans('core.forum.settings.notifications_heading'),
|
||||
className: 'Settings-notifications',
|
||||
children: [NotificationGrid.component({user: this.user})]
|
||||
children: this.notificationsItems().toArray()
|
||||
})
|
||||
);
|
||||
|
||||
items.add('privacy',
|
||||
FieldSet.component({
|
||||
label: app.trans('core.privacy'),
|
||||
label: app.translator.trans('core.forum.settings.privacy_heading'),
|
||||
className: 'Settings-privacy',
|
||||
children: this.privacyItems().toArray()
|
||||
})
|
||||
@@ -73,7 +73,7 @@ export default class SettingsPage extends UserPage {
|
||||
|
||||
items.add('changePassword',
|
||||
Button.component({
|
||||
children: app.trans('core.change_password'),
|
||||
children: app.translator.trans('core.forum.settings.change_password_button'),
|
||||
className: 'Button',
|
||||
onclick: () => app.modal.show(new ChangePasswordModal())
|
||||
})
|
||||
@@ -81,7 +81,7 @@ export default class SettingsPage extends UserPage {
|
||||
|
||||
items.add('changeEmail',
|
||||
Button.component({
|
||||
children: app.trans('core.change_email'),
|
||||
children: app.translator.trans('core.forum.settings.change_email_button'),
|
||||
className: 'Button',
|
||||
onclick: () => app.modal.show(new ChangeEmailModal())
|
||||
})
|
||||
@@ -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.
|
||||
*
|
||||
@@ -118,7 +131,7 @@ export default class SettingsPage extends UserPage {
|
||||
|
||||
items.add('discloseOnline',
|
||||
Switch.component({
|
||||
children: app.trans('core.disclose_online'),
|
||||
children: app.translator.trans('core.forum.settings.privacy_disclose_online_label'),
|
||||
state: this.user.preferences().discloseOnline,
|
||||
onchange: (value, component) => {
|
||||
this.user.pushAttributes({lastSeenTime: null});
|
||||
|
@@ -2,6 +2,8 @@ import Modal from 'flarum/components/Modal';
|
||||
import LogInModal from 'flarum/components/LogInModal';
|
||||
import avatar from 'flarum/helpers/avatar';
|
||||
import Button from 'flarum/components/Button';
|
||||
import LogInButtons from 'flarum/components/LogInButtons';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
|
||||
/**
|
||||
* The `SignUpModal` component displays a modal dialog with a singup form.
|
||||
@@ -11,10 +13,11 @@ import Button from 'flarum/components/Button';
|
||||
* - `username`
|
||||
* - `email`
|
||||
* - `password`
|
||||
* - `token` An email token to sign up with.
|
||||
*/
|
||||
export default class SignUpModal extends Modal {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
/**
|
||||
* The value of the username input.
|
||||
@@ -36,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() {
|
||||
@@ -50,7 +46,7 @@ export default class SignUpModal extends Modal {
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.trans('core.sign_up');
|
||||
return app.translator.trans('core.forum.sign_up.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
@@ -65,84 +61,49 @@ 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={app.trans('core.username')}
|
||||
<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} />
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="email" type="email" placeholder={app.trans('core.email')}
|
||||
<input className="FormControl" name="email" type="email" placeholder={extractText(app.translator.trans('core.forum.sign_up.email_placeholder'))}
|
||||
value={this.email()}
|
||||
onchange={m.withAttr('value', this.email)}
|
||||
disabled={this.loading} />
|
||||
disabled={this.loading || (this.props.token && this.props.email)} />
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="password" type="password" placeholder={app.trans('core.password')}
|
||||
value={this.password()}
|
||||
onchange={m.withAttr('value', this.password)}
|
||||
disabled={this.loading} />
|
||||
</div>
|
||||
{this.props.token ? '' : (
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="password" type="password" placeholder={extractText(app.translator.trans('core.forum.sign_up.password_placeholder'))}
|
||||
value={this.password()}
|
||||
onchange={m.withAttr('value', this.password)}
|
||||
disabled={this.loading} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.trans('core.sign_up')
|
||||
})}
|
||||
<Button
|
||||
className="Button Button--primary Button--block"
|
||||
type="submit"
|
||||
loading={this.loading}>
|
||||
{app.translator.trans('core.forum.sign_up.submit_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)];
|
||||
|
||||
if (this.welcomeUser) {
|
||||
const user = this.welcomeUser;
|
||||
const emailProviderName = user.email().split('@')[1];
|
||||
|
||||
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.trans('core.welcome_user', {user})}</h3>
|
||||
|
||||
{!user.isActivated() ? [
|
||||
<p>{app.trans('core.confirmation_email_sent', {email: <strong>{user.email()}</strong>})}</p>,
|
||||
<p>
|
||||
<a href={`http://${emailProviderName}`} className="Button Button--primary" target="_blank">
|
||||
{app.trans('core.go_to', {location: emailProviderName})}
|
||||
</a>
|
||||
</p>
|
||||
] : (
|
||||
<p>
|
||||
<button className="Button Button--primary" onclick={this.hide.bind(this)}>
|
||||
{app.trans('core.dismiss')}
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return body;
|
||||
];
|
||||
}
|
||||
|
||||
footer() {
|
||||
return [
|
||||
<p className="SignUpModal-logIn">
|
||||
{app.trans('core.before_log_in_link')}{' '}
|
||||
<a onclick={this.logIn.bind(this)}>{app.trans('core.log_in')}</a>
|
||||
{app.translator.trans('core.forum.sign_up.log_in_text', {a: <a onclick={this.logIn.bind(this)}/>})}
|
||||
</p>
|
||||
];
|
||||
}
|
||||
@@ -150,10 +111,12 @@ export default class SignUpModal extends Modal {
|
||||
/**
|
||||
* Open the log in modal, prefilling it with an email/username/password if
|
||||
* the user has entered one.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
logIn() {
|
||||
const props = {
|
||||
email: this.email() || this.username(),
|
||||
identification: this.email() || this.username(),
|
||||
password: this.password()
|
||||
};
|
||||
|
||||
@@ -161,10 +124,10 @@ export default class SignUpModal extends Modal {
|
||||
}
|
||||
|
||||
onready() {
|
||||
if (this.props.username) {
|
||||
if (this.props.username && !this.props.email) {
|
||||
this.$('[name=email]').select();
|
||||
} else {
|
||||
super.onready();
|
||||
this.$('[name=username]').select();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,24 +138,39 @@ export default class SignUpModal extends Modal {
|
||||
|
||||
const data = this.submitData();
|
||||
|
||||
app.store.createRecord('users').save(data).then(
|
||||
user => {
|
||||
this.welcomeUser = user;
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
},
|
||||
response => {
|
||||
this.loading = false;
|
||||
this.handleErrors(response.errors);
|
||||
}
|
||||
app.request({
|
||||
url: app.forum.attribute('baseUrl') + '/register',
|
||||
method: 'POST',
|
||||
data,
|
||||
errorHandler: this.onerror.bind(this)
|
||||
}).then(
|
||||
() => window.location.reload(),
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data that should be submitted in the sign-up request.
|
||||
*
|
||||
* @return {Object}
|
||||
* @public
|
||||
*/
|
||||
submitData() {
|
||||
return {
|
||||
const data = {
|
||||
username: this.username(),
|
||||
email: this.email(),
|
||||
password: this.password()
|
||||
email: this.email()
|
||||
};
|
||||
|
||||
if (this.props.token) {
|
||||
data.token = this.props.token;
|
||||
} else {
|
||||
data.password = this.password();
|
||||
}
|
||||
|
||||
if (this.props.avatarUrl) {
|
||||
data.avatarUrl = this.props.avatarUrl;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user