mirror of
https://github.com/flarum/core.git
synced 2025-08-13 20:04:24 +02:00
Compare commits
993 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 | ||
|
53f7112248 | ||
|
a2def83045 | ||
|
cbcad27679 | ||
|
9bf485359a | ||
|
60323e0cf9 | ||
|
8cccaaaf6b | ||
|
b7d8afe6a4 | ||
|
ff8ec59310 | ||
|
8eda6c7d36 | ||
|
d5b58b3146 | ||
|
f00d2b1363 | ||
|
190aa925ac | ||
|
60b19efe0a | ||
|
b2fa6b1a2e | ||
|
e7d7df3b0c | ||
|
3b5a01e603 | ||
|
b05f83d25a | ||
|
902d01712b | ||
|
502a3787d5 | ||
|
b8ac49ffcc | ||
|
4b4cea4d87 | ||
|
c0e7ff5ea1 | ||
|
e54944d6c3 | ||
|
d39bca192e | ||
|
efff485d6c | ||
|
6a5427b600 | ||
|
e8621636c5 | ||
|
1aaff46f8e | ||
|
8c4e095f23 | ||
|
05c44ad2df | ||
|
84012ca2fd | ||
|
6393432d92 | ||
|
f6e21b75e1 | ||
|
6ee9412f35 | ||
|
478ca90c31 | ||
|
1f8f79d272 | ||
|
85fc0a3129 | ||
|
db8b9ed0c0 | ||
|
a3d59977b3 | ||
|
211d2d25cd | ||
|
0a992ee9f2 | ||
|
42f1abacaf | ||
|
b26c67dd3c | ||
|
fc7fc41383 | ||
|
a5d3aa9b36 | ||
|
b18909f1af | ||
|
695df18be0 | ||
|
ece23de750 | ||
|
4705600d47 | ||
|
8423de754c | ||
|
b597e6f8f6 | ||
|
276334ec52 | ||
|
9277fca0ec | ||
|
9ca67635fb | ||
|
7a6c48c30b | ||
|
f0186d7674 | ||
|
9bf6862c6d | ||
|
44f460cb11 | ||
|
7cce5b02ba | ||
|
722058f2fb | ||
|
70815b024a | ||
|
7269385786 | ||
|
2f8a449b74 | ||
|
b3aa0298d5 | ||
|
e192402a42 | ||
|
c81ceafb54 | ||
|
93b6f11484 | ||
|
0413daab74 | ||
|
f0c240f863 | ||
|
21dd516eaa | ||
|
3c9d851889 | ||
|
942db77416 | ||
|
04db806995 | ||
|
f3bc7d1c23 | ||
|
bd47653377 | ||
|
07ed4d10c0 | ||
|
25141c0f2f | ||
|
e35bb9e400 | ||
|
753a846e7a | ||
|
d3e57d77b4 | ||
|
6e0bffe395 | ||
|
eec4e97d65 | ||
|
bf83b36882 | ||
|
6aafe54ee7 | ||
|
c91f8de1f5 | ||
|
5783dbe77b | ||
|
ab496eb8f8 | ||
|
6f13a246db | ||
|
4c34d0867d | ||
|
f2a3a0cb10 | ||
|
5b7527144c | ||
|
6c169499b5 | ||
|
5e22458014 | ||
|
c72bdc8238 | ||
|
2438bbfd41 | ||
|
5af5f1fc77 | ||
|
e7f4e5060c | ||
|
bcc16a3329 | ||
|
283abb88c2 | ||
|
af2307868a | ||
|
f9d724738c | ||
|
42e722d824 | ||
|
f5517fbd88 | ||
|
6a0e3fcf2d | ||
|
0ae2d18f28 | ||
|
0474f410a4 | ||
|
9f28b4e8dc | ||
|
f44e9f5140 | ||
|
3e14ef0714 | ||
|
c999226449 | ||
|
ba097dc147 | ||
|
1d1cc9e443 | ||
|
f5d2d2ff79 | ||
|
a04acca92e | ||
|
4033319ed0 | ||
|
a4fe6f3ce3 | ||
|
ae06b45bc1 | ||
|
be33761950 | ||
|
015aaaa899 | ||
|
67f6b8599d | ||
|
12d5e48b95 | ||
|
a41e3e66ce | ||
|
874c023f8a | ||
|
bb3c57f9a4 | ||
|
98a79e957d | ||
|
cf68c95fb8 | ||
|
d5074c5286 | ||
|
41019597d0 | ||
|
b689c9de3b | ||
|
baed659668 |
@@ -12,21 +12,8 @@ insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.js]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{css,less}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.html]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.php]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
@@ -1,5 +0,0 @@
|
||||
**/bower_components/**/*
|
||||
**/node_modules/**/*
|
||||
vendor/**/*
|
||||
**/Gulpfile.js
|
||||
**/dist/**/*
|
170
.eslintrc
170
.eslintrc
@@ -1,170 +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
|
||||
},
|
||||
"rules": {
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
7
.gitattributes
vendored
7
.gitattributes
vendored
@@ -2,3 +2,10 @@
|
||||
.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:
|
||||
- vendor/bin/phpcs --standard=PSR2 -np src
|
||||
- vendor/bin/phpspec run
|
||||
- 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
|
||||
|
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,21 +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.1.0",
|
||||
"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/yaml": "^2.7"
|
||||
"symfony/http-foundation": "^2.7",
|
||||
"symfony/translation": "^2.7",
|
||||
"symfony/yaml": "^2.7",
|
||||
"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": {
|
||||
@@ -45,5 +66,15 @@
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "0.1.x-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2392
composer.lock
generated
2392
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;
|
||||
|
@@ -1,66 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import humanTime from 'flarum/helpers/humanTime';
|
||||
import avatar from 'flarum/helpers/avatar';
|
||||
|
||||
/**
|
||||
* The `Activity` component represents a piece of activity of a user's activity
|
||||
* feed. Subclasses should implement the `description` and `content` methods.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `activity`
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class Activity extends Component {
|
||||
view() {
|
||||
const activity = this.props.activity;
|
||||
|
||||
return (
|
||||
<div className="Activity">
|
||||
{avatar(this.user(), {className: 'Activity-avatar'})}
|
||||
|
||||
<div className="Activity-header">
|
||||
<strong className="Activity-description">{this.description()}</strong>
|
||||
{humanTime(this.time())}
|
||||
</div>
|
||||
|
||||
{this.content()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user whose avatar should be displayed.
|
||||
*
|
||||
* @return {User}
|
||||
* @abstract
|
||||
*/
|
||||
user() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time of the activity.
|
||||
*
|
||||
* @return {Date}
|
||||
* @abstract
|
||||
*/
|
||||
time() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description of the activity.
|
||||
*
|
||||
* @return {VirtualElement}
|
||||
*/
|
||||
description() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content to show below the activity description.
|
||||
*
|
||||
* @return {VirtualElement}
|
||||
*/
|
||||
content() {
|
||||
}
|
||||
}
|
@@ -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">{m.trust(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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/*global s9e*/
|
||||
/*global s9e, hljs*/
|
||||
|
||||
import Post from 'flarum/components/Post';
|
||||
import classList from 'flarum/utils/classList';
|
||||
@@ -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,36 +41,54 @@ 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) {
|
||||
super.config(...arguments);
|
||||
|
||||
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) {
|
||||
this.$('.Post-body script').each(function() {
|
||||
eval.call(window, $(this).text());
|
||||
});
|
||||
}
|
||||
|
||||
context.contentHtml = contentHtml;
|
||||
}
|
||||
|
||||
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,
|
||||
'hidden': post.isHidden(),
|
||||
'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) {
|
||||
@@ -133,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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ import Component from 'flarum/Component';
|
||||
import DiscussionListItem from 'flarum/components/DiscussionListItem';
|
||||
import Button from 'flarum/components/Button';
|
||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
import Placeholder from 'flarum/components/Placeholder';
|
||||
|
||||
/**
|
||||
* The `DiscussionList` component displays a list of discussions.
|
||||
@@ -12,9 +13,7 @@ import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
* 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.
|
||||
*
|
||||
@@ -47,12 +46,21 @@ 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 = app.translator.trans('core.forum.discussion_list.empty_text');
|
||||
return (
|
||||
<div className="DiscussionList">
|
||||
{Placeholder.component({text})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="DiscussionList">
|
||||
<ul className="DiscussionList-discussions">
|
||||
@@ -107,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;
|
||||
}
|
||||
@@ -179,12 +187,7 @@ export default class DiscussionList extends Component {
|
||||
this.loading = false;
|
||||
this.moreResults = !!results.payload.links.next;
|
||||
|
||||
// Since this may be called during the component's constructor, i.e. in the
|
||||
// middle of a redraw, forcing another redraw would not bode well. Instead
|
||||
// we start/end a computation so Mithril will only redraw if it isn't
|
||||
// already doing so.
|
||||
m.startComputation();
|
||||
m.endComputation();
|
||||
m.lazyRedraw();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
@@ -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();
|
||||
|
||||
@@ -51,13 +60,15 @@ export default class DiscussionListItem extends Component {
|
||||
const discussion = this.props.discussion;
|
||||
const startUser = discussion.startUser();
|
||||
const isUnread = discussion.isUnread();
|
||||
const isRead = discussion.isRead();
|
||||
const showUnread = !this.showRepliesCount() && isUnread;
|
||||
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',
|
||||
@@ -71,10 +82,10 @@ export default class DiscussionListItem extends Component {
|
||||
{icon('check')}
|
||||
</a>
|
||||
|
||||
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '')}>
|
||||
<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);
|
||||
@@ -95,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>
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Page from 'flarum/components/Page';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import DiscussionHero from 'flarum/components/DiscussionHero';
|
||||
import PostStream from 'flarum/components/PostStream';
|
||||
@@ -6,17 +6,15 @@ import PostStreamScrubber from 'flarum/components/PostStreamScrubber';
|
||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
import SplitDropdown from 'flarum/components/SplitDropdown';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
import mixin from 'flarum/utils/mixin';
|
||||
import evented from 'flarum/utils/evented';
|
||||
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
||||
|
||||
/**
|
||||
* The `DiscussionPage` component displays a whole discussion page, including
|
||||
* the discussion list pane, the hero, the posts, and the sidebar.
|
||||
*/
|
||||
export default class DiscussionPage extends mixin(Component, evented) {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
export default class DiscussionPage extends Page {
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
/**
|
||||
* The discussion that is being viewed.
|
||||
@@ -43,18 +41,14 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
||||
app.pane.enable();
|
||||
app.pane.hide();
|
||||
|
||||
if (app.current instanceof DiscussionPage) {
|
||||
if (app.previous instanceof DiscussionPage) {
|
||||
m.redraw.strategy('diff');
|
||||
}
|
||||
}
|
||||
|
||||
// Push onto the history stack, but use a generalised key so that navigating
|
||||
// to a few different discussions won't override the behaviour of the back
|
||||
// button.
|
||||
app.history.push('discussion');
|
||||
app.current = this;
|
||||
app.drawer.hide();
|
||||
app.modal.close();
|
||||
|
||||
this.bodyClass = 'App--discussion';
|
||||
}
|
||||
|
||||
onunload(e) {
|
||||
@@ -67,9 +61,9 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
||||
if (idParam && idParam.split('-')[0] === this.discussion.id()) {
|
||||
e.preventDefault();
|
||||
|
||||
const near = Number(m.route.param('near')) || 1;
|
||||
const near = m.route.param('near') || '1';
|
||||
|
||||
if (near !== Number(this.near)) {
|
||||
if (near !== String(this.near)) {
|
||||
this.stream.goToNumber(near);
|
||||
}
|
||||
|
||||
@@ -121,13 +115,6 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
if (isInitialized) return;
|
||||
|
||||
$('#app').addClass('App--discussion');
|
||||
context.onunload = () => $('#app').removeClass('App--discussion');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear and reload the discussion.
|
||||
*/
|
||||
@@ -141,20 +128,15 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
||||
// 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.init.bind(this, preloadedDiscussion));
|
||||
setTimeout(this.show.bind(this, preloadedDiscussion), 0);
|
||||
} else {
|
||||
const params = this.requestParams();
|
||||
|
||||
app.store.find('discussions', m.route.param('id').split('-')[0], params)
|
||||
.then(this.init.bind(this));
|
||||
.then(this.show.bind(this));
|
||||
}
|
||||
|
||||
// Since this may be called during the component's constructor, i.e. in the
|
||||
// middle of a redraw, forcing another redraw would not bode well. Instead
|
||||
// we start/end a computation so Mithril will only redraw if it isn't
|
||||
// already doing so.
|
||||
m.startComputation();
|
||||
m.endComputation();
|
||||
m.lazyRedraw();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,9 +156,10 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
||||
*
|
||||
* @param {Discussion} discussion
|
||||
*/
|
||||
init(discussion) {
|
||||
show(discussion) {
|
||||
this.discussion = discussion;
|
||||
|
||||
app.history.push('discussion', discussion.title());
|
||||
app.setTitle(discussion.title());
|
||||
app.setTitleCount(0);
|
||||
|
||||
@@ -201,9 +184,7 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
||||
// 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.trigger('loaded', discussion);
|
||||
this.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,7 +274,7 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
||||
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,8 +27,8 @@ export default class DiscussionRenamedPost extends EventPost {
|
||||
const newTitle = post.content()[1];
|
||||
|
||||
return {
|
||||
old: <strong className="DiscussionRenamedPost-old">{oldTitle}</strong>,
|
||||
new: <strong className="DiscussionRenamedPost-new">{newTitle}</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.
|
||||
*
|
||||
@@ -30,16 +37,16 @@ export default class HeaderSecondary extends Component {
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('search', app.search.render());
|
||||
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());
|
||||
@@ -54,29 +61,29 @@ export default class HeaderSecondary extends Component {
|
||||
items.add('locale', SelectDropdown.component({
|
||||
children: locales,
|
||||
buttonClassName: 'Button Button--link'
|
||||
}));
|
||||
}), 20);
|
||||
}
|
||||
|
||||
if (app.session.user) {
|
||||
items.add('notifications', NotificationsDropdown.component());
|
||||
items.add('session', SessionDropdown.component());
|
||||
items.add('notifications', NotificationsDropdown.component(), 10);
|
||||
items.add('session', SessionDropdown.component(), 0);
|
||||
} else {
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import Component from 'flarum/Component';
|
||||
import { extend } from 'flarum/extend';
|
||||
import Page from 'flarum/components/Page';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import affixSidebar from 'flarum/utils/affixSidebar';
|
||||
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';
|
||||
@@ -16,22 +17,22 @@ import SelectDropdown from 'flarum/components/SelectDropdown';
|
||||
* The `IndexPage` component displays the index page, including the welcome
|
||||
* hero, the sidebar, and the discussion list.
|
||||
*/
|
||||
export default class IndexPage extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
export default class IndexPage extends Page {
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
// If the user is returning from a discussion page, then take note of which
|
||||
// discussion they have just visited. After the view is rendered, we will
|
||||
// scroll down so that this discussion is in view.
|
||||
if (app.current instanceof DiscussionPage) {
|
||||
this.lastDiscussion = app.current.discussion;
|
||||
if (app.previous instanceof DiscussionPage) {
|
||||
this.lastDiscussion = app.previous.discussion;
|
||||
}
|
||||
|
||||
// If the user is coming from the discussion list, then they have either
|
||||
// just switched one of the parameters (filter, sort, search) or they
|
||||
// probably want to refresh the results. We will clear the discussion list
|
||||
// cache so that results are reloaded.
|
||||
if (app.current instanceof IndexPage) {
|
||||
if (app.previous instanceof IndexPage) {
|
||||
app.cache.discussionList = null;
|
||||
}
|
||||
|
||||
@@ -54,10 +55,9 @@ export default class IndexPage extends Component {
|
||||
app.cache.discussionList = new DiscussionList({params});
|
||||
}
|
||||
|
||||
app.history.push('index');
|
||||
app.current = this;
|
||||
app.drawer.hide();
|
||||
app.modal.close();
|
||||
app.history.push('index', app.translator.trans('core.forum.header.back_to_index_tooltip'));
|
||||
|
||||
this.bodyClass = 'App--index';
|
||||
}
|
||||
|
||||
onunload() {
|
||||
@@ -71,7 +71,7 @@ export default class IndexPage extends Component {
|
||||
<div className="IndexPage">
|
||||
{this.hero()}
|
||||
<div className="container">
|
||||
<nav className="IndexPage-nav sideNav" config={affixSidebar}>
|
||||
<nav className="IndexPage-nav sideNav">
|
||||
<ul>{listItems(this.sidebarItems().toArray())}</ul>
|
||||
</nav>
|
||||
<div className="IndexPage-results sideNavOffset">
|
||||
@@ -87,27 +87,30 @@ export default class IndexPage extends Component {
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
super.config(...arguments);
|
||||
|
||||
if (isInitialized) return;
|
||||
|
||||
$('#app').addClass('App--index');
|
||||
context.onunload = () => {
|
||||
$('#app').removeClass('App--index')
|
||||
.css('min-height', '');
|
||||
};
|
||||
extend(context, 'onunload', () => $('#app').css('min-height', ''));
|
||||
|
||||
app.setTitle('');
|
||||
app.setTitleCount(0);
|
||||
|
||||
// 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
|
||||
@@ -146,11 +149,11 @@ export default class IndexPage extends Component {
|
||||
*/
|
||||
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',
|
||||
@@ -183,7 +186,7 @@ export default class IndexPage extends Component {
|
||||
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
|
||||
@@ -201,17 +204,28 @@ export default class IndexPage extends Component {
|
||||
*/
|
||||
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,
|
||||
})
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -229,17 +243,23 @@ export default class IndexPage extends Component {
|
||||
|
||||
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)
|
||||
@@ -360,6 +380,10 @@ export default class IndexPage extends Component {
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
@@ -19,18 +19,21 @@ export default class Notification extends Component {
|
||||
const href = this.href();
|
||||
|
||||
return (
|
||||
<div className={'Notification Notification--' + notification.contentType() + ' ' + (!notification.isRead() ? 'unread' : '')}
|
||||
onclick={this.markAsRead.bind(this)}>
|
||||
<a href={href} config={href.indexOf('://') === -1 ? m.route : undefined}>
|
||||
{avatar(notification.sender())}
|
||||
{icon(this.icon(), {className: 'Notification-icon'})}
|
||||
<span className="Notification-content">{this.content()}</span>
|
||||
{humanTime(notification.time())}
|
||||
<div className="Notification-excerpt">
|
||||
{this.excerpt()}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<a className={'Notification Notification--' + notification.contentType() + ' ' + (!notification.isRead() ? 'unread' : '')}
|
||||
href={href}
|
||||
config={function(element, isInitialized) {
|
||||
if (href.indexOf('://') === -1) m.route.apply(this, arguments);
|
||||
|
||||
if (!isInitialized) $(element).click(this.markAsRead.bind(this));
|
||||
}}>
|
||||
{avatar(notification.sender())}
|
||||
{icon(this.icon(), {className: 'Notification-icon'})}
|
||||
<span className="Notification-content">{this.content()}</span>
|
||||
{humanTime(notification.time())}
|
||||
<div className="Notification-excerpt">
|
||||
{this.excerpt()}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,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'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,50 +1,74 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Dropdown from 'flarum/components/Dropdown';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import NotificationList from 'flarum/components/NotificationList';
|
||||
|
||||
export default class NotificationsDropdown extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
export default class NotificationsDropdown extends Dropdown {
|
||||
static initProps(props) {
|
||||
props.className = props.className || 'NotificationsDropdown';
|
||||
props.buttonClassName = props.buttonClassName || 'Button Button--flat';
|
||||
props.menuClassName = props.menuClassName || 'Dropdown-menu--right';
|
||||
props.label = props.label || app.translator.trans('core.forum.notifications.tooltip');
|
||||
props.icon = props.icon || 'bell';
|
||||
|
||||
/**
|
||||
* Whether or not the notifications dropdown is visible.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.showing = false;
|
||||
super.initProps(props);
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.list = new NotificationList();
|
||||
}
|
||||
|
||||
view() {
|
||||
const user = app.session.user;
|
||||
const unread = user.unreadNotificationsCount();
|
||||
getButton() {
|
||||
const newNotifications = this.getNewCount();
|
||||
const vdom = super.getButton();
|
||||
|
||||
vdom.attrs.title = this.props.label;
|
||||
|
||||
vdom.attrs.className += (newNotifications ? ' new' : '');
|
||||
vdom.attrs.onclick = this.onclick.bind(this);
|
||||
|
||||
return vdom;
|
||||
}
|
||||
|
||||
getButtonContent() {
|
||||
const unread = this.getUnreadCount();
|
||||
|
||||
return [
|
||||
icon(this.props.icon, {className: 'Button-icon'}),
|
||||
unread ? <span className="NotificationsDropdown-unread">{unread}</span> : '',
|
||||
<span className="Button-label">{this.props.label}</span>
|
||||
];
|
||||
}
|
||||
|
||||
getMenu() {
|
||||
return (
|
||||
<div className="Dropdown NotificationsDropdown">
|
||||
<a href="javascript:;"
|
||||
className={'Dropdown-toggle Button Button--flat NotificationsDropdown-button' + (unread ? ' unread' : '')}
|
||||
data-toggle="dropdown"
|
||||
onclick={this.onclick.bind(this)}>
|
||||
<span className="Button-icon">{unread || icon('bell')}</span>
|
||||
<span className="Button-label">{app.trans('core.notifications')}</span>
|
||||
</a>
|
||||
<div className="Dropdown-menu Dropdown-menu--right" onclick={this.menuClick.bind(this)}>
|
||||
{this.showing ? this.list.render() : ''}
|
||||
</div>
|
||||
<div className={'Dropdown-menu ' + this.props.menuClassName} onclick={this.menuClick.bind(this)}>
|
||||
{this.showing ? this.list.render() : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onclick() {
|
||||
if (app.drawer.isOpen()) {
|
||||
m.route(app.route('notifications'));
|
||||
this.goToRoute();
|
||||
} else {
|
||||
this.showing = true;
|
||||
this.list.load();
|
||||
}
|
||||
}
|
||||
|
||||
goToRoute() {
|
||||
m.route(app.route('notifications'));
|
||||
}
|
||||
|
||||
getUnreadCount() {
|
||||
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.
|
||||
|
@@ -1,21 +1,20 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Page from 'flarum/components/Page';
|
||||
import NotificationList from 'flarum/components/NotificationList';
|
||||
|
||||
/**
|
||||
* The `NotificationsPage` component shows the notifications list. It is only
|
||||
* used on mobile devices where the notifications dropdown is within the drawer.
|
||||
*/
|
||||
export default class NotificationsPage extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
export default class NotificationsPage extends Page {
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
app.current = this;
|
||||
app.history.push('notifications');
|
||||
app.drawer.hide();
|
||||
app.modal.close();
|
||||
|
||||
this.list = new NotificationList();
|
||||
this.list.load();
|
||||
|
||||
this.bodyClass = 'App--notifications';
|
||||
}
|
||||
|
||||
view() {
|
||||
|
33
js/forum/src/components/Page.js
Normal file
33
js/forum/src/components/Page.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import Component from 'flarum/Component';
|
||||
|
||||
/**
|
||||
* The `Page` component
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class Page extends Component {
|
||||
init() {
|
||||
app.previous = app.current;
|
||||
app.current = this;
|
||||
|
||||
app.drawer.hide();
|
||||
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,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.
|
||||
*
|
||||
@@ -38,7 +37,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
this.loadPageTimeouts = {};
|
||||
this.pagesLoading = 0;
|
||||
|
||||
this.init(this.props.includedPosts);
|
||||
this.show(this.props.includedPosts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -153,7 +154,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
*
|
||||
* @param {Post[]} posts
|
||||
*/
|
||||
init(posts) {
|
||||
show(posts) {
|
||||
this.visibleStart = posts.length ? this.discussion.postIds().indexOf(posts[0].id()) : 0;
|
||||
this.visibleEnd = this.visibleStart + posts.length;
|
||||
}
|
||||
@@ -181,7 +182,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
.map(id => {
|
||||
const post = app.store.getById('posts', id);
|
||||
|
||||
return post && post.discussion() ? 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);
|
||||
@@ -421,7 +436,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
return app.store.find('posts', {
|
||||
filter: {discussion: this.discussion.id()},
|
||||
page: {near: number}
|
||||
}).then(this.init.bind(this));
|
||||
}).then(this.show.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,7 +457,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
|
||||
this.reset(start, end);
|
||||
|
||||
return this.loadRange(start, end).then(this.init.bind(this));
|
||||
return this.loadRange(start, end).then(this.show.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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>
|
||||
@@ -80,8 +80,8 @@ export default class PostsUserPage extends UserPage {
|
||||
* Initialize the component with a user, and trigger the loading of their
|
||||
* activity feed.
|
||||
*/
|
||||
init(user) {
|
||||
super.init(user);
|
||||
show(user) {
|
||||
super.show(user);
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
@@ -95,9 +95,7 @@ export default class PostsUserPage extends UserPage {
|
||||
this.loading = true;
|
||||
this.posts = [];
|
||||
|
||||
// Redraw, but only if we're not in the middle of a route change.
|
||||
m.startComputation();
|
||||
m.endComputation();
|
||||
m.lazyRedraw();
|
||||
|
||||
this.loadResults().then(this.parseResults.bind(this));
|
||||
}
|
||||
|
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.init(app.session.user);
|
||||
app.setTitle(app.trans('core.settings'));
|
||||
this.show(app.session.user);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,22 +136,41 @@ export default class SignUpModal extends Modal {
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const data = {
|
||||
username: this.username(),
|
||||
email: this.email(),
|
||||
password: this.password()
|
||||
};
|
||||
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() {
|
||||
const data = {
|
||||
username: this.username(),
|
||||
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