mirror of
https://github.com/flarum/core.git
synced 2025-08-13 11:54:32 +02:00
Compare commits
2204 Commits
v0.1.0-bet
...
v0.1.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
fd371c1203 | ||
|
d72416cd8a | ||
|
937ff1a0d5 | ||
|
097a87dbb6 | ||
|
7794546845 | ||
|
c43cc874ee | ||
|
33cf94c192 | ||
|
036e519865 | ||
|
9386c91af9 | ||
|
8306cef963 | ||
|
51ea326959 | ||
|
15bed971e6 | ||
|
c896cd8696 | ||
|
54ac83d0b6 | ||
|
1592cd1013 | ||
|
6e8884f190 | ||
|
df8f73bd3d | ||
|
3f0f89afb1 | ||
|
f0f301c5f4 | ||
|
3045bde167 | ||
|
ee7a4627d8 | ||
|
b9fb92d49a | ||
|
b5accca957 | ||
|
798a3486bf | ||
|
89ef14faf1 | ||
|
84cf938379 | ||
|
899cdfda4e | ||
|
72ed4faa83 | ||
|
64ad21e5da | ||
|
14e8e9a7cb | ||
|
ee996e2cae | ||
|
7b35674e4a | ||
|
1d953b3514 | ||
|
b7d8f77529 | ||
|
b343206c7b | ||
|
2aead54aea | ||
|
dbfae0b55e | ||
|
2d86eb9b9f | ||
|
3ac5e58fa1 | ||
|
ffa56595c3 | ||
|
453c44632d | ||
|
117c2f65ac | ||
|
cd9edf656b | ||
|
8c19ba1aaa | ||
|
3f5554816e | ||
|
cb9801a324 | ||
|
fd4c0d30d8 | ||
|
922e294668 | ||
|
1fa37a7a6a | ||
|
1cbb2a365e | ||
|
4c50c8d77a | ||
|
0d57820b50 | ||
|
ecdd7a2b49 | ||
|
30942bdf38 | ||
|
345ad4bc6d | ||
|
03a4997a1c | ||
|
857fd95b5e | ||
|
dd43e49d0a | ||
|
4efdd2a4f2 | ||
|
b286e39429 | ||
|
1cda9dca4f | ||
|
e16d57d4e2 | ||
|
2e2aa8747e | ||
|
44ac2ec8ee | ||
|
6bbd603a41 | ||
|
a4910f3d94 | ||
|
f003f6e04a | ||
|
2fe3987c19 | ||
|
f4ab6f4b1f | ||
|
9ae8bcdffe | ||
|
29bdd471bc | ||
|
fb70826469 | ||
|
bbe7e97ba1 | ||
|
310065fb1c | ||
|
23da7b3373 | ||
|
2ba29a9088 | ||
|
cd8a8e9dd7 | ||
|
6b3d634917 | ||
|
8c6fac62d6 | ||
|
02e72f4b03 | ||
|
e3f1e69748 | ||
|
bc7cea6e61 | ||
|
30c6ea9912 | ||
|
0bc06e1bb1 | ||
|
b10a17529d | ||
|
bc80085ce4 | ||
|
f31fbc5bcf | ||
|
25f772c1ea | ||
|
a13c0bb612 | ||
|
4791cc77b3 | ||
|
e10da825d4 | ||
|
a2d1d2b819 | ||
|
fb277df3b0 | ||
|
a854fa8bcb | ||
|
bc69588785 | ||
|
a2d1245e90 | ||
|
090b05736a | ||
|
4b45ce0a58 | ||
|
9f8ee7dc94 | ||
|
4413848c11 | ||
|
9212330ac2 | ||
|
455d070599 | ||
|
84ae88794f | ||
|
ec3e9c722b | ||
|
2e6cd584aa | ||
|
27b0d1802e | ||
|
2c02702d60 | ||
|
9c3a016123 | ||
|
0d208dc443 | ||
|
e7c71ec445 | ||
|
46e2e17c3c | ||
|
f574f97174 | ||
|
674303b997 | ||
|
0fba2c0c0a | ||
|
0666448ef5 | ||
|
08e40bc693 | ||
|
eaf1767008 | ||
|
9f1eca555f | ||
|
72fd32dbf6 | ||
|
d5ebbab3a7 | ||
|
17257aacaf | ||
|
f87c8c6dcd | ||
|
f9556d9d6a | ||
|
fdfc6c0de2 | ||
|
64e4132c92 | ||
|
4b78a3114f | ||
|
c01eea58b6 | ||
|
19cb74c856 | ||
|
27bcdb949b | ||
|
94fc460240 | ||
|
fc59f0fdd8 | ||
|
b91e903284 | ||
|
711e775de7 | ||
|
736e90d423 | ||
|
2f3d9995d1 | ||
|
ac14f84a9a | ||
|
1d7641cbb0 | ||
|
dce36cbeed | ||
|
7e1087cba5 | ||
|
8877bf97c4 | ||
|
7e74f5a03c | ||
|
02ceed4fed | ||
|
27f159f6b8 | ||
|
499f33fbb6 | ||
|
8dd3bd420b | ||
|
2ca3188eff | ||
|
f275bcdd2c | ||
|
64c702aaf7 | ||
|
833ea4e06e | ||
|
5643ee649b | ||
|
97b2db84c6 | ||
|
4fea25959c | ||
|
8b70cec6a1 | ||
|
a330a8fa28 | ||
|
02899d4f68 | ||
|
76f7d566b2 | ||
|
e296bbf0aa | ||
|
0a4ee93fde | ||
|
1e7fbf1ed9 | ||
|
1170d5c2cf | ||
|
fcbbedd884 | ||
|
4c89e2eb77 | ||
|
809f353c52 | ||
|
d7a5a6ad14 | ||
|
ca0c52d60a | ||
|
2325e33e38 | ||
|
aba291c542 | ||
|
9b00244454 | ||
|
c1878fe29b | ||
|
43c551929b | ||
|
840e740309 | ||
|
babb36d375 | ||
|
25b9d88469 | ||
|
b5c2285167 | ||
|
beaaa21f58 | ||
|
8a1bcf30d2 | ||
|
ff384569f8 | ||
|
f64a253450 | ||
|
da5628d125 | ||
|
a9c18c4753 | ||
|
d492579638 | ||
|
19188e3eda | ||
|
8cc44a695f | ||
|
7bb8b66596 | ||
|
40f709e7c6 | ||
|
264ff9f7bb | ||
|
308f2c9efd | ||
|
2a8ed53934 | ||
|
17c86b82bf | ||
|
63b039a800 | ||
|
213045aa03 | ||
|
6d10dbe9af | ||
|
4adf342ce3 | ||
|
b150636906 | ||
|
4f1adba387 | ||
|
879b801600 | ||
|
c712d23e9c | ||
|
d69c4035d9 | ||
|
b83adbccfd | ||
|
7b6c666e7b | ||
|
8b9f03e998 | ||
|
e69f8965c7 | ||
|
6d2b50722a | ||
|
99a05900b1 | ||
|
cc5e586d38 | ||
|
17074b8aab | ||
|
406c8ff834 | ||
|
1ba4a0b87e | ||
|
36017f89fe | ||
|
1f2566c32c | ||
|
0c74927eab | ||
|
19ecd968c6 | ||
|
fc64660f5d | ||
|
d5d769ebb1 | ||
|
f5ee37b394 | ||
|
54c5c09693 | ||
|
c87ebaef08 | ||
|
9c0d921f49 | ||
|
d7bdc173a4 | ||
|
937354512b | ||
|
2dedfe4b92 | ||
|
9f6ec80432 | ||
|
aa31b8307d | ||
|
dc06d5b5c9 | ||
|
2c867d2292 | ||
|
b09ac3f3f8 | ||
|
21f54c5562 | ||
|
a0ace316e8 | ||
|
6c96c932e0 | ||
|
522e41aa71 | ||
|
bbd891965f | ||
|
0f43445a90 | ||
|
7a684660e9 | ||
|
12dc4fff57 | ||
|
1b5a200781 | ||
|
1bdf7764a9 | ||
|
3417c0cbee | ||
|
738ca405fe | ||
|
09609a9f20 | ||
|
fb0a875c6d | ||
|
74b6b9935b | ||
|
3b5691ee28 | ||
|
18593e0d7d | ||
|
40e1b61fe6 | ||
|
95dcb45d65 | ||
|
bd989df769 | ||
|
538136153c | ||
|
c330662241 | ||
|
588cbaee2d | ||
|
a9557c399a | ||
|
14e7bc73ee | ||
|
edc579fa6f | ||
|
119831e51c | ||
|
2aee020c14 | ||
|
f20696210e | ||
|
ea84fc4836 | ||
|
5ff04d0c68 | ||
|
e2ec52c28c | ||
|
6196081bdf | ||
|
6d8e6583c8 | ||
|
c2b0060852 | ||
|
24964b94bf | ||
|
2b624c935d | ||
|
2e647cdda8 | ||
|
ba175144f4 | ||
|
e9af36ab47 | ||
|
8b3913339a | ||
|
3cced4156f | ||
|
e88a9394ed | ||
|
ba73c59601 | ||
|
0191babb05 | ||
|
ed51f9ff0a | ||
|
0a2bdbaa09 | ||
|
26229db1fd | ||
|
1aef3162be | ||
|
dcf88df0c7 | ||
|
3eb28dfb16 | ||
|
1d43371fa9 | ||
|
4df455cf04 | ||
|
2c43ccf66c | ||
|
1d010efbca | ||
|
2135d5908e | ||
|
9640dd6419 | ||
|
98464a8a33 | ||
|
2b6535525b | ||
|
b60617b849 | ||
|
0836d99e83 | ||
|
279c7df9b9 | ||
|
04bcf1eef6 | ||
|
70e98f810c | ||
|
3851d805f7 | ||
|
085468382a | ||
|
7dbdd8c024 | ||
|
ad25307e68 | ||
|
6c454b8279 | ||
|
9f15e9ba86 | ||
|
41009dba74 | ||
|
a045f8bef9 | ||
|
689d767f82 | ||
|
77fff9fde8 | ||
|
c6c1ae32e6 | ||
|
bdac88b573 | ||
|
31ee65be93 | ||
|
29df6b60be | ||
|
1e6f175379 | ||
|
065ff3456f | ||
|
37e0a5579b | ||
|
cd9aa0096e | ||
|
d06493c61e | ||
|
9f71e2c3cb | ||
|
81a8736ba9 | ||
|
57ce25301d | ||
|
cfbaa84fbc | ||
|
3417f5a77e | ||
|
1035636d0f | ||
|
d00fc2c49d | ||
|
f3b889a665 | ||
|
c5122bf5d5 | ||
|
5ed55195e1 | ||
|
8604ea3020 | ||
|
2648e960a7 | ||
|
f0dff95d62 | ||
|
894db01ad8 | ||
|
bd04023359 | ||
|
f357434a72 | ||
|
c2586586c4 | ||
|
06cd062a1b | ||
|
1502fc98d8 | ||
|
ed97989ca2 | ||
|
7f1048352d | ||
|
d2700961ba | ||
|
b2dbb0439c | ||
|
085c924a07 | ||
|
f31f02d4cc | ||
|
797f6eea50 | ||
|
9fb3a31b51 | ||
|
f8061bbca1 | ||
|
de67927ef2 | ||
|
8c841c3266 | ||
|
2f656146a7 | ||
|
d66d2aa26e | ||
|
f4c0d4ba87 | ||
|
646bd40bca | ||
|
307b912019 | ||
|
cbc896eba7 | ||
|
cc4e4a068b | ||
|
a720f6f651 | ||
|
54d7c0d3b6 | ||
|
b5876d9f31 | ||
|
25ef4c10bd | ||
|
985b87da6c | ||
|
a6aa28566c | ||
|
e3340ba3e1 | ||
|
590b311570 | ||
|
935a968257 | ||
|
fe558eb0ba | ||
|
fda9cba4ce | ||
|
89f6cfd949 | ||
|
803582c437 | ||
|
8e86d38804 | ||
|
fd66722945 | ||
|
ce42b5e035 | ||
|
bfd3a667dd | ||
|
b669490d33 | ||
|
ba956f51ac | ||
|
c126b95451 | ||
|
7f7484e790 | ||
|
5d64056e89 | ||
|
e927254e99 | ||
|
8061bfd74a | ||
|
4c309d2ad7 | ||
|
54876cfbd6 | ||
|
9e2b796a7c | ||
|
7f5bd1e96b | ||
|
5e1680c458 | ||
|
6e26b988bd | ||
|
2e8d4e4b6b | ||
|
14bede2847 | ||
|
54660ebd63 | ||
|
1a62b7e07a | ||
|
4b04c0e0ce | ||
|
4d45ce389b | ||
|
d2674fb309 | ||
|
5eb69e1f59 | ||
|
f42142979d | ||
|
5f79d3b499 | ||
|
8e4d97260f | ||
|
ee3640e160 | ||
|
bd584802e5 | ||
|
f4dd045326 | ||
|
24522943f6 | ||
|
56fde28e43 | ||
|
1c1d661bdd | ||
|
d3be186fb6 | ||
|
8f8cc558be | ||
|
5ea9e1cf5e | ||
|
99a6066f96 | ||
|
8b7db726dc | ||
|
7a44086bf3 | ||
|
12fdfc9b54 | ||
|
ecc3b5e227 | ||
|
bf2c5a5564 | ||
|
d3a5c91845 | ||
|
e17bb0b433 | ||
|
c4ba41f850 | ||
|
0c4de6f163 | ||
|
cd313952c7 | ||
|
ef57b443c1 | ||
|
5154d7e5a6 | ||
|
2bd40b50c7 | ||
|
c50d58d0f4 | ||
|
8c65316961 | ||
|
0a818cfdf3 | ||
|
57204c6ed0 | ||
|
a21052c903 | ||
|
441ebacfd7 | ||
|
46acfb6c23 | ||
|
9910e884fc | ||
|
d292aaabf8 | ||
|
d822a6f84c | ||
|
26c3bcdb74 | ||
|
33deea4791 | ||
|
20227a2201 | ||
|
0493682dba | ||
|
49dda87e86 | ||
|
d959d08561 | ||
|
e8ab49abc1 | ||
|
296677b5fc | ||
|
f3931b537c | ||
|
d0ba4e5268 | ||
|
654ab4cc29 | ||
|
e0becd0c7b | ||
|
ed43ad9c3f | ||
|
4611abe5db | ||
|
df0bd52283 | ||
|
d387a9ff02 | ||
|
5556df54f9 | ||
|
cf746079ed | ||
|
4d10536d35 | ||
|
ba16ebe61f | ||
|
6484dc4982 | ||
|
1a9f1f7a3d | ||
|
4d1411e2a8 | ||
|
968152b740 | ||
|
af185fd3d1 | ||
|
ed9591c16f | ||
|
8ad326941f | ||
|
8e4f02d994 | ||
|
8ae85bc49f | ||
|
7ff9a90204 | ||
|
f4fb1ab272 | ||
|
484c6d2edb | ||
|
8b68ff6232 | ||
|
0a59b7164e | ||
|
0879829dc4 | ||
|
78ba3bd854 | ||
|
44c91099cd | ||
|
4585f03ee3 | ||
|
bc9e8f68f1 | ||
|
f5a21584c2 | ||
|
e0a508a765 | ||
|
89e018a4f0 | ||
|
de6001f4cf | ||
|
790d5beee5 | ||
|
abf224bb0a | ||
|
c7d2e165d7 | ||
|
0ab9facc4b | ||
|
9b68bbe44e | ||
|
862404f052 | ||
|
b9a93f3440 | ||
|
c67fb2d4b6 | ||
|
1b2d4f1e1d | ||
|
54fdc40d87 | ||
|
390148456c | ||
|
167059027e | ||
|
208bad393f | ||
|
8a93f8b6b6 | ||
|
9db04a4e19 | ||
|
ac5e26a254 | ||
|
9794a08f39 | ||
|
ababb8ebef | ||
|
cb3baf9955 | ||
|
dbe8cba14e | ||
|
9fe671c9bb | ||
|
0e5f334a0b | ||
|
e4514d8413 | ||
|
1080d25561 | ||
|
ba594de13a | ||
|
209d13affd | ||
|
671fdec8d0 | ||
|
9eca9192ca | ||
|
3468bdf511 | ||
|
54503d2c29 | ||
|
565131e2a7 | ||
|
f0da3cf304 | ||
|
6acc91577d | ||
|
3e0cd3a21f | ||
|
5c9fa4c62d | ||
|
4b00f7996b | ||
|
b58380e224 | ||
|
b0e996e7ff | ||
|
b41d9fb0e7 | ||
|
ed02eed88f | ||
|
c761802900 | ||
|
16eb1fa63b | ||
|
0ceb8d64df | ||
|
9712eccb03 | ||
|
9684fbc4da | ||
|
67f9375d47 | ||
|
0d16fac001 | ||
|
a8f5ca8d97 | ||
|
bbe62f400f | ||
|
fc5977f86f | ||
|
796b57753d | ||
|
88e43cc694 | ||
|
6370f7ecff | ||
|
d9d7027ed0 | ||
|
1106a2e3a3 | ||
|
b7f666525c | ||
|
40dc6ac604 | ||
|
15e1a154e5 | ||
|
57d5846b64 | ||
|
fb6b51b1cf | ||
|
57f73c9638 | ||
|
7705a2b7d7 | ||
|
f591585d02 | ||
|
45afc33eb0 | ||
|
213fd62be3 | ||
|
66607a5674 | ||
|
546b4f01ac | ||
|
96e282458b | ||
|
24ff8899a0 | ||
|
1b32c7cc51 | ||
|
6c2a4a5ff7 | ||
|
2d31a6f72e | ||
|
9115b9e28f | ||
|
3bff2e0f5c | ||
|
edaca3160e | ||
|
9e63f32105 | ||
|
f3a5a89e12 | ||
|
b074f47298 | ||
|
2ef66ac716 | ||
|
6654894da1 | ||
|
41544c8529 | ||
|
53d1b87daf | ||
|
c11e4720d9 | ||
|
c73d03a427 | ||
|
d0d6c52839 | ||
|
9585d448d2 | ||
|
04d46b9925 | ||
|
c3b2d8e7d8 | ||
|
62a40036d0 | ||
|
2764ad87cc | ||
|
2c1be86857 | ||
|
b26eb8e609 | ||
|
1f0bf33cfb | ||
|
6fadc0b653 | ||
|
0255393108 | ||
|
b474843cc2 | ||
|
7e95b80341 | ||
|
18b90d16e3 | ||
|
64a7e8ac3a | ||
|
e8b8cd0078 | ||
|
3c8262ccde | ||
|
68c6638fb5 | ||
|
105dd093fe | ||
|
920802e5ae | ||
|
882c4aa105 | ||
|
b826f9ce36 | ||
|
c13dfa2228 | ||
|
bf3934d16f | ||
|
a08068b112 | ||
|
60149fbe15 | ||
|
13c593cbaa | ||
|
f7a320bcca | ||
|
b980c6fb7d | ||
|
0f9118fe2b | ||
|
95f0edcd80 | ||
|
222e3c3fe2 | ||
|
903c1e329d | ||
|
295a007cd5 | ||
|
66404e1f61 | ||
|
64e43ec9a4 | ||
|
bf8bc0222f | ||
|
6d14d0c39b | ||
|
17fdc0ebe0 | ||
|
9de786d1e6 | ||
|
b92ae61294 | ||
|
e99f7fcdac | ||
|
009ddcdb63 | ||
|
d021dc2399 | ||
|
1fff5dbbbc | ||
|
4de5accfc1 | ||
|
bb0fc165af | ||
|
fb185f70cd | ||
|
3b630cb03e | ||
|
f283f0c7bd | ||
|
4b915c688c | ||
|
83e99ed5a5 | ||
|
a09894a906 | ||
|
1c7d2c3d27 | ||
|
7db6cfac3f | ||
|
c446c5cc61 | ||
|
bd10ebff24 | ||
|
104d3982fe | ||
|
a1948e7bb8 | ||
|
4775535421 | ||
|
2392e06c0e | ||
|
e3e10a8fc3 | ||
|
b4dbab5df1 | ||
|
f062f69f00 | ||
|
0e3b0fc5a0 | ||
|
21b3737dc2 | ||
|
4ed1d0aaee | ||
|
86b26ce2fb | ||
|
eafc637475 | ||
|
a03f243ca5 | ||
|
5f5e1c512c | ||
|
a4d540f74b | ||
|
c23af9550e | ||
|
8fd3e8908c | ||
|
cc95faa07d | ||
|
f1add1798b | ||
|
81f6ce220e | ||
|
9fa7258325 | ||
|
4841661ee2 | ||
|
7b34636636 | ||
|
8474dfd6e2 | ||
|
737d33826e | ||
|
3006f58274 | ||
|
d3a5e2451a | ||
|
f03c954dcc | ||
|
3b70b9e76e | ||
|
b823a9df47 | ||
|
8621500501 | ||
|
f48101dc04 | ||
|
3c827d2fce | ||
|
1f0e9b6280 | ||
|
bf61753361 | ||
|
70cb2f6f2e | ||
|
1736fe3f58 | ||
|
ae81c4f0f1 | ||
|
8c679c715c | ||
|
4236f3d49e | ||
|
e3afb38427 | ||
|
26a5b107bd | ||
|
aa70441632 | ||
|
0fb9aa3940 | ||
|
b6a60721e2 | ||
|
fe868af224 | ||
|
0984979403 | ||
|
5dfb9b474c | ||
|
fcb97b256f | ||
|
4429cf4eba | ||
|
c54f739484 | ||
|
6c0d73afa0 | ||
|
1cd8ec6873 | ||
|
4ed1c7a1bb | ||
|
c67f673819 | ||
|
7917ce130e | ||
|
2b174b17fc | ||
|
550d35e86f | ||
|
64686ef7a9 | ||
|
5ce702a5d0 | ||
|
fdf5fdbaf3 | ||
|
184ffcd991 | ||
|
ba946237e4 | ||
|
8ec0578ddf | ||
|
c34fcecf03 | ||
|
9e487b4e41 | ||
|
986d811a16 | ||
|
c32af6559e | ||
|
07298e165d | ||
|
c15bbc9c5e | ||
|
6c8c525e57 | ||
|
93dfb6dec9 | ||
|
750d9d05a6 | ||
|
0ce289be4c | ||
|
7fcf556f13 | ||
|
68afdd21ae | ||
|
54aa9ee3cf | ||
|
5635bd26f9 | ||
|
622bf26510 | ||
|
2367a45c18 | ||
|
4770a5c906 | ||
|
c61badd754 | ||
|
14393ec53e | ||
|
96045ca390 | ||
|
eb228dd7b9 | ||
|
092e5b9d23 | ||
|
7e3980744e | ||
|
85c965afbc | ||
|
43fc2c0952 | ||
|
5a9b47cdf7 | ||
|
5374f8a352 | ||
|
5f5af894ab | ||
|
d7c283a48f | ||
|
c5a3715701 | ||
|
710c63ba6a | ||
|
5142c639c1 | ||
|
eb3232dfc9 | ||
|
72d46bc461 | ||
|
9792576464 | ||
|
5c0c2d1c40 | ||
|
ce39bc9070 | ||
|
37ffd04b3f | ||
|
d8d2de438f | ||
|
70058652b5 | ||
|
d9d8162684 | ||
|
2ee10bb49f | ||
|
64abbde8b2 | ||
|
ca93c8c609 | ||
|
8248ba2f7a | ||
|
dd65801d57 | ||
|
07c08ca798 | ||
|
ae75f21b6b | ||
|
d47c406d9c | ||
|
29cef23404 | ||
|
a7ffed6778 | ||
|
9074f7e592 | ||
|
99e5013ac3 | ||
|
1e9d9b8322 | ||
|
568006fe73 | ||
|
4756bf1daf | ||
|
8ecb67d49d | ||
|
e241518506 | ||
|
cbd0643540 | ||
|
7716944616 | ||
|
e135b7830e | ||
|
950ab30c29 | ||
|
582054c61c | ||
|
280d51e678 | ||
|
e9ed935ed1 | ||
|
809b161d71 | ||
|
6bc434c918 | ||
|
4c8908c005 | ||
|
2d4dc02ca1 | ||
|
869ec54bd0 | ||
|
59b1ca9b7c | ||
|
7439069fe2 | ||
|
973fbcf17b | ||
|
034000ea0b | ||
|
fb5740926a | ||
|
32ad926cbc | ||
|
626d16de6f | ||
|
0222692c53 | ||
|
b4b72fe62f | ||
|
5b821b21b1 | ||
|
7a6e208554 | ||
|
3a0e982df1 | ||
|
6057151c29 | ||
|
fcb9a049e9 | ||
|
190bcb5e9a | ||
|
e72ac76997 | ||
|
62e7a7188c | ||
|
ca16a23383 | ||
|
195f77ff10 | ||
|
5f83285442 | ||
|
c16ddf24f2 | ||
|
394fc4232e | ||
|
fd36d18729 | ||
|
2e16b0ce2e | ||
|
af47558ec2 | ||
|
641079b3fe | ||
|
df0f4e8462 | ||
|
da0adf83ae | ||
|
0452838802 | ||
|
40e4c0acdd | ||
|
ef9ed7f4fa | ||
|
24fd2f32c7 | ||
|
076288db21 | ||
|
df7e24cba6 | ||
|
5438aea759 | ||
|
e46ce861dc | ||
|
01d8bd5344 | ||
|
7f5080d9d5 | ||
|
640b93af36 | ||
|
cfc207f255 | ||
|
d4a80eae5d | ||
|
254d5d0c5b | ||
|
1709d4ef2c | ||
|
baeaa73597 | ||
|
677a7dd2d3 | ||
|
c562302161 | ||
|
d42f33971a | ||
|
aa4c4b07bd | ||
|
420bb2efc8 | ||
|
ed57d6e51c | ||
|
b4f6c4be1f | ||
|
ff7f7681c7 | ||
|
7d0813bce4 | ||
|
4f259425b0 | ||
|
22fadb7f9c | ||
|
d0115de771 | ||
|
9d790c18d6 | ||
|
6f3eb3f335 | ||
|
2d667d885d | ||
|
0fb81958cb | ||
|
87bba2186e | ||
|
93b9513df2 | ||
|
fb6b2d05b1 | ||
|
fe73cf3237 | ||
|
c7efbba0da | ||
|
be266a73cd | ||
|
e1a282e0e1 | ||
|
5a04635e7a | ||
|
7d59b4da7e | ||
|
f89c111f13 | ||
|
6a6b9ac6b1 | ||
|
cbe328cdc5 | ||
|
7b802a76ba | ||
|
90792abf10 | ||
|
5139ce647e | ||
|
8779e40ec5 | ||
|
1e372f3881 | ||
|
b2e873ba7b | ||
|
d6414cfb44 | ||
|
85ceda0b0b | ||
|
dc7c31e1c2 | ||
|
177ac74596 | ||
|
c76d9e1298 | ||
|
dee54a008f | ||
|
551ca23267 | ||
|
3d845d5730 | ||
|
9b03f8c71a | ||
|
665f241348 | ||
|
ce90d2bbdd | ||
|
6e5b0f5289 | ||
|
fba31995b1 | ||
|
604c789ee8 | ||
|
034b82f4d4 | ||
|
9be13cb1cd | ||
|
823f0263ae | ||
|
fb9a89f67f | ||
|
7f63923aa0 | ||
|
73f8922553 | ||
|
2c15597ec9 | ||
|
7e43de25a6 | ||
|
d42205a8ff | ||
|
00bc8fc0bc | ||
|
c17af492a9 | ||
|
0e73785498 | ||
|
0f5ddc1c43 | ||
|
eaf98ccfc5 | ||
|
9c7cc0548e | ||
|
cf80cf86e5 | ||
|
a23dc0dfcd | ||
|
54678e8d5c | ||
|
4d2d7465ee | ||
|
b78129b36b | ||
|
afe06ea750 | ||
|
9449fb4f1f | ||
|
aac194616a | ||
|
d9b357c18e | ||
|
48ac132959 | ||
|
b3f8379a15 | ||
|
1800f4290a | ||
|
2234a81ee7 | ||
|
805768a9e0 | ||
|
e3c2ddad2e | ||
|
520e1550d1 | ||
|
79b00cb94f | ||
|
0bcc6e74a8 | ||
|
542e8715ea | ||
|
3f683dd6ee | ||
|
d234badbb2 | ||
|
2cd77e231f | ||
|
07eda60561 | ||
|
2bc7c4134a | ||
|
050496a20e | ||
|
3b87778fbb | ||
|
569e6c9a92 | ||
|
c498e68530 | ||
|
305841ddd4 | ||
|
066baed5b9 | ||
|
211e7681cc | ||
|
22f2df3670 | ||
|
3bf74eaf10 | ||
|
d301d260c1 | ||
|
a1c3da9f8f | ||
|
26b02adc9d | ||
|
3ec32f8430 | ||
|
f137eb358f | ||
|
b91a3573db | ||
|
b3d45fd6f8 | ||
|
e6b8ff856e | ||
|
4a2aa7e892 | ||
|
b3cbc5d1bd | ||
|
3680d88fb7 | ||
|
5d0ebde6b8 | ||
|
0278d52cbe | ||
|
c293fdaec0 | ||
|
4654c3eb50 | ||
|
68d1edb8fd | ||
|
30358e98c0 | ||
|
e226f81515 | ||
|
09938f8633 | ||
|
4c55d278b6 | ||
|
808e7a226a | ||
|
3e3e1cbde5 | ||
|
a9501ceae0 | ||
|
2a721926d3 | ||
|
81cb67e87c | ||
|
fd859e33be | ||
|
7539c25048 | ||
|
0058067b1b | ||
|
3c41011548 | ||
|
49c643609c | ||
|
4df0101f56 | ||
|
b8632d693a | ||
|
684985c25c | ||
|
a2927b725f | ||
|
efa3b62fb8 | ||
|
406be427ad | ||
|
665ac178e9 | ||
|
c4a501f82a | ||
|
beec59232f | ||
|
371f33e99e | ||
|
264664ac79 | ||
|
17f29f83c9 | ||
|
c9c8fa0fde | ||
|
45f28b6f72 | ||
|
3ef7843540 | ||
|
7d41c4e510 | ||
|
8574b57fc5 | ||
|
dcb3821777 | ||
|
043aa0f2d9 | ||
|
f51e29ff4c | ||
|
46f80e8d72 | ||
|
29d6b3306f | ||
|
4e2c32b108 | ||
|
92c8c616e1 | ||
|
f42273e679 | ||
|
44376da57d | ||
|
2b6ee50c58 | ||
|
155582831d | ||
|
baa11acfa8 | ||
|
4aad293284 | ||
|
c60d6e9dee | ||
|
0cf351edb9 | ||
|
447ca18558 | ||
|
a70e6e639c | ||
|
fdbf0c86a1 | ||
|
6ea60248e3 | ||
|
8d2d987680 | ||
|
bb49e24ffe | ||
|
5672819549 | ||
|
b4e093ab8a | ||
|
5645bcbf9c | ||
|
9d30be1617 | ||
|
d5ef9486d0 | ||
|
0c5c322cb4 | ||
|
3f45d18383 | ||
|
aba22b9119 | ||
|
26dfc8ae21 | ||
|
db7cd71f19 | ||
|
2967b5d106 | ||
|
e8d915850d | ||
|
a061eda019 | ||
|
cc22d1d6f8 | ||
|
9b1c338b68 | ||
|
571938a677 | ||
|
1a2df2d581 | ||
|
0f554585ac | ||
|
0b478379fc | ||
|
ba96f311a9 | ||
|
34588a74e2 | ||
|
58ffa27bfb | ||
|
f0cea11e79 | ||
|
160493e725 | ||
|
5561e28286 | ||
|
c9cfcee12a | ||
|
479fafbf5c | ||
|
aff1b9a5e4 | ||
|
a53d95a3d9 | ||
|
ff10ed0ea9 | ||
|
7721288ac6 | ||
|
c3a6f7daef | ||
|
a4704b1e2e | ||
|
0ab1f2cfe7 | ||
|
322a84f516 | ||
|
80ec3b5e17 | ||
|
636e965873 | ||
|
419adb748b | ||
|
25154dabff | ||
|
2eae968a70 | ||
|
7651907f56 | ||
|
557a65aadd | ||
|
ad4bd3d001 | ||
|
4b1a299b3c | ||
|
fa14be591c | ||
|
072f4f89cb | ||
|
ed3e833181 | ||
|
7f92838225 | ||
|
2159107214 | ||
|
d357364712 | ||
|
26449a64fe | ||
|
ae2e07e94c | ||
|
801d619a36 | ||
|
0befe041c7 | ||
|
1c87c33d4d | ||
|
3480a65989 | ||
|
2979e8bc28 | ||
|
8c470954eb | ||
|
6913e8f0f8 | ||
|
30a04e7bf9 | ||
|
0af97c427c | ||
|
1c1cefa017 | ||
|
c6747b6910 | ||
|
1ce70eeb6e | ||
|
714775cfed | ||
|
bdc1a100cd | ||
|
f3e29ab801 | ||
|
26e53fc51b | ||
|
848293a7d5 | ||
|
5af65dede1 | ||
|
e774baf32f | ||
|
2ac04aac8e | ||
|
c6aeeeb3c1 | ||
|
f247d8c2a6 | ||
|
0380536cb4 | ||
|
6dc96b38af | ||
|
9342723f64 | ||
|
8d049126d0 | ||
|
63be95fb8a | ||
|
1d47047d45 | ||
|
4e30ad5891 | ||
|
f4ad227576 | ||
|
5b6d043f80 | ||
|
c41e58531a | ||
|
b760d113d2 | ||
|
936f67e953 | ||
|
3f7e7520b0 | ||
|
7ccb263926 | ||
|
fe56f57e8f | ||
|
fa9d89d690 | ||
|
292fe06001 | ||
|
f2ce4e11e2 | ||
|
47eb853bf9 | ||
|
d807171c44 | ||
|
57a91c966d | ||
|
0525f467c7 | ||
|
b791790d2f | ||
|
ea353a2f2c | ||
|
ac7d28ca58 | ||
|
8a6344cfcf | ||
|
a0621e85bf | ||
|
56231d61be | ||
|
c8a1a5fcfa | ||
|
8d5132fd5a | ||
|
6efe2ee91a | ||
|
bfd98e3371 | ||
|
e9da1ba2f5 | ||
|
b2fe76c819 | ||
|
2b8c66354d | ||
|
4cf481355f | ||
|
2ec183778c | ||
|
1b94ef90ea | ||
|
183a22b5c5 | ||
|
654fca9c2c | ||
|
7be01119f5 | ||
|
28d4cff156 | ||
|
2aba61668c | ||
|
fccadcc6ab | ||
|
f65e4dcba3 | ||
|
c6ce172caa | ||
|
d2f187716e | ||
|
93aa3d77b5 | ||
|
a0c95e6705 | ||
|
77c25ab725 | ||
|
3dcfe32b27 | ||
|
617a76dda8 | ||
|
1a239ee93a | ||
|
f8d065bd78 | ||
|
0a654d1f31 | ||
|
b806dc3db2 | ||
|
92de751154 | ||
|
5b46ec801d | ||
|
1ef9217f4d | ||
|
79fee3686f | ||
|
8edc8223e5 | ||
|
6280fb2498 | ||
|
ba769e0c7e | ||
|
ea2fc1ff8a | ||
|
b7c1cc5cef | ||
|
e8a4e5e0ef | ||
|
295193eb3c | ||
|
a065c8e6f5 | ||
|
9392e1bec3 | ||
|
479e44dd04 | ||
|
c01268d9ae | ||
|
f4fc245df4 | ||
|
ac0b4cb2d2 | ||
|
55b945f129 | ||
|
4d9e2335c7 | ||
|
642332ffe2 | ||
|
7b2663e0bc | ||
|
e2d61d1aeb | ||
|
7796580210 | ||
|
0aa74c987c | ||
|
55a09a2f57 | ||
|
a28dbccf1a | ||
|
3c80612d80 | ||
|
dedcbae359 | ||
|
9cc67fe312 | ||
|
5f7060fb4a | ||
|
1a102766a9 | ||
|
abda11c6c5 | ||
|
b43fdec2e9 | ||
|
3321b4e829 | ||
|
a8826dcd88 | ||
|
15b573bd93 | ||
|
41df32f66e | ||
|
094345de85 | ||
|
54597ee5eb | ||
|
3be98b9f8e | ||
|
257ee936f4 | ||
|
1a928ca0ab | ||
|
0b1043c9d2 | ||
|
72c232d5a3 | ||
|
914b94b62d | ||
|
1b7cb3bec2 | ||
|
55b763a570 | ||
|
d7306dedb7 | ||
|
3eede757bb | ||
|
a8f8ca7f87 | ||
|
44e9007790 | ||
|
eeed7c20e1 | ||
|
40ebc13292 | ||
|
2754a8c867 | ||
|
123c8bb73d | ||
|
f3b4d35587 | ||
|
42ecee42a1 | ||
|
5a43f915cb | ||
|
6a10b4484f | ||
|
06aa37d2fd | ||
|
cb92deee98 | ||
|
bedf710768 | ||
|
3b1f8771c4 | ||
|
2dbcfe02d8 | ||
|
a7f3ca4b22 | ||
|
3f9dc81874 | ||
|
1c01145a14 | ||
|
2037371886 | ||
|
87bf84ef6e | ||
|
096e552c74 | ||
|
8ccfb1aac6 | ||
|
ea4d889b76 | ||
|
19d15d4302 | ||
|
43d8a9d0e8 | ||
|
02377663ce | ||
|
4f688fc9a2 | ||
|
fddd134fa0 | ||
|
1215a1ef9b | ||
|
4aad7c1040 | ||
|
e46b3d54d1 | ||
|
051bb5acb8 | ||
|
69b517ea79 | ||
|
b4c7f8ca89 | ||
|
3ece3ca976 | ||
|
9c77475985 | ||
|
b72407440d | ||
|
f824dcfb53 | ||
|
78f3681fc1 | ||
|
5b0d0d9f0f | ||
|
95dc7e71f4 | ||
|
a39ed6edec | ||
|
66f35d2530 | ||
|
e6e4531771 | ||
|
e71deed8d5 | ||
|
8a16c1ecc8 | ||
|
b38ade986d | ||
|
4a13cd8088 | ||
|
920a4071b6 | ||
|
c22219ec20 | ||
|
11bf3e34b7 | ||
|
4fb38d6458 | ||
|
66abd7ecfd | ||
|
3481798875 | ||
|
9abc63aaac | ||
|
6cd6a7d260 | ||
|
6c9ff72efb | ||
|
33e3d757c3 | ||
|
551e76f296 | ||
|
564ea8ff73 | ||
|
fda8c597f4 | ||
|
5d1564e0fc | ||
|
1f1b63363e | ||
|
bdf455c0c6 | ||
|
927e4ca3ed | ||
|
0be13d50bd | ||
|
6268c3010f | ||
|
acf43606a8 | ||
|
5ae2e9d232 | ||
|
d897839097 | ||
|
c6985ae31c | ||
|
9b24fbd5e5 | ||
|
5127514d35 | ||
|
eb72307a54 | ||
|
f917d1438c | ||
|
ef89b1f6b1 | ||
|
377d439c47 | ||
|
37cf95f94d | ||
|
c31c1ea062 | ||
|
084f74946d | ||
|
1e8399c014 | ||
|
ad153c8484 | ||
|
cbe4464178 | ||
|
7df9594a04 | ||
|
c037658675 | ||
|
287085dc25 | ||
|
a2e0daed70 | ||
|
57f828b3f7 | ||
|
7ec92813e3 | ||
|
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
|
||||
[*.{php,xml}]
|
||||
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
|
||||
}
|
||||
}
|
9
.gitattributes
vendored
9
.gitattributes
vendored
@@ -1,4 +1,13 @@
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.gitmodules export-ignore
|
||||
.github export-ignore
|
||||
.travis export-ignore
|
||||
.travis.yml export-ignore
|
||||
.editorconfig export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
||||
phpunit.xml export-ignore
|
||||
tests export-ignore
|
||||
|
||||
js/dist/* -diff
|
||||
|
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
github: flarum
|
||||
open_collective: flarum
|
||||
tidelift: packagist/flarum/core
|
39
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
39
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "🐛 Bug Report"
|
||||
about: "If something isn't working as expected"
|
||||
|
||||
---
|
||||
## Bug Report
|
||||
|
||||
**Current Behavior**
|
||||
A clear and concise description of the behavior.
|
||||
|
||||
**Steps to Reproduce**
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected Behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment**
|
||||
- Flarum version: x.y.z
|
||||
- Website URL: http://example.com
|
||||
- Webserver: [e.g. apache, nginx]
|
||||
- Hosting environment: [e.g. shared, vps]
|
||||
- PHP version: x.y.z
|
||||
- Browser: [e.g. chrome 67, safari 11]
|
||||
|
||||
```
|
||||
Output of "php flarum info", run this in terminal in your Flarum directory.
|
||||
```
|
||||
|
||||
**Possible Solution**
|
||||
<!--- Only if you have suggestions or a fix for the bug -->
|
||||
|
||||
**Additional Context**
|
||||
Add any other context about the problem here.
|
26
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: "🚀 Feature Request"
|
||||
about: "I have a suggestion (and may want to implement it!)"
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
IMPORTANT: Feature requests on this GitHub issue tracker are only accepted in case they have been approved by a core developer or contain extensive argumentation and directions for implementation. For all other feature requests, ideas and feedback please post in the Flarum Community: https://discuss.flarum.org/t/feedback.
|
||||
-->
|
||||
|
||||
## Feature Request
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. eg. I have an issue when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A detailed description of your proposed solution. Include:
|
||||
- How the feature would work/behave
|
||||
- Any potential drawbacks
|
||||
- Maybe a screenshot, design, or example code
|
||||
|
||||
**Justify why this feature belongs in Flarum's core, rather than in a third-party extension**
|
||||
Consider who this change will be useful to – most Flarum forums, or just a few?
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
11
.github/ISSUE_TEMPLATE/support-question.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/support-question.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: "🙋 Support Question"
|
||||
about: "If you have a question, please check out our forum or Discord!"
|
||||
|
||||
---
|
||||
|
||||
We primarily use GitHub as an issue tracker; for usage and support questions, please check out these resources below. Thanks!
|
||||
|
||||
* Flarum Community: https://discuss.flarum.org/
|
||||
* Discord Chat: https://flarum.org/discord/
|
||||
* Twitter: https://twitter.com/Flarum
|
24
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
24
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
IMPORTANT: We applaud pull requests, they excite us every single time. As we have an obligation to maintain a healthy code standard and quality, we take sufficient time for reviews. Please do create a separate pull request per change/issue/feature; we will ask you to split bundled pull requests.
|
||||
-->
|
||||
|
||||
**Fixes #0000**
|
||||
|
||||
**Changes proposed in this pull request:**
|
||||
<!-- fill this out, mention the pages and/or components which have been impacted -->
|
||||
|
||||
**Reviewers should focus on:**
|
||||
<!-- fill this out, ask for feedback on specific changes you are unsure about -->
|
||||
|
||||
**Screenshot**
|
||||
<!-- include an image of the most relevant user-facing change, if any -->
|
||||
|
||||
**Confirmed**
|
||||
|
||||
- [ ] Frontend changes: tested on a local Flarum installation.
|
||||
- [ ] Backend changes: tests are green (run `composer test`).
|
||||
|
||||
**Required changes:**
|
||||
|
||||
- [ ] Related documentation PR: (Remove if irrelevant)
|
||||
- [ ] Related core extension PRs: (Remove if irrelevant)
|
13
.github/SECURITY.md
vendored
Normal file
13
.github/SECURITY.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
During the beta phase, we will only patch security vulnerabilities in the latest beta release.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability within Flarum, please send an email to security@flarum.org so we can address it promptly.
|
||||
|
||||
We will get back to you as time allows.
|
||||
Discussions may commence internally, so you may not hear back immediately.
|
||||
When reporting a vulnerability, please provide your GitHub username (if available), so that we can invite you to collaborate on a [security advisory on GitHub](https://help.github.com/en/articles/about-maintainer-security-advisories).
|
26
.github/stale.yml
vendored
Normal file
26
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
daysUntilStale: 90
|
||||
daysUntilClose: 30
|
||||
|
||||
staleLabel: stale
|
||||
|
||||
exemptLabels:
|
||||
- org/keep
|
||||
- type/bug
|
||||
- type/regression
|
||||
- critical
|
||||
- security
|
||||
exemptAssignees: true
|
||||
exemptMilestones: true
|
||||
exemptProjects: true
|
||||
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. We do this
|
||||
to keep the amount of open issues to a manageable minimum.
|
||||
|
||||
In any case, thanks for taking an interest in this software and contributing
|
||||
by opening the issue in the first place!
|
||||
|
||||
closeComment: >
|
||||
We are closing this issue as it seems to have grown stale. If you still
|
||||
encounter this problem with the latest version, feel free to re-open it.
|
16
.github/workflows/build.yml
vendored
Normal file
16
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: JavaScript
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: flarum/action-build@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
31
.github/workflows/lint.yml
vendored
Normal file
31
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'js/src/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'js/src/**'
|
||||
|
||||
jobs:
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
name: JS / Prettier
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "12"
|
||||
|
||||
- name: Install JS dependencies
|
||||
run: npm ci
|
||||
working-directory: ./js
|
||||
|
||||
- name: Check JS code for formatting
|
||||
run: node_modules/.bin/prettier --check src
|
||||
working-directory: ./js
|
67
.github/workflows/test.yml
vendored
Normal file
67
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php: [7.2, 7.3, 7.4]
|
||||
service: ['mysql:5.7', mariadb]
|
||||
prefix: ['', flarum_]
|
||||
|
||||
include:
|
||||
- service: 'mysql:5.7'
|
||||
db: MySQL
|
||||
- service: mariadb
|
||||
db: MariaDB
|
||||
- prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
|
||||
exclude:
|
||||
- php: 7.2
|
||||
service: 'mysql:5.7'
|
||||
prefix: flarum_
|
||||
- php: 7.2
|
||||
service: mariadb
|
||||
prefix: flarum_
|
||||
- php: 7.3
|
||||
service: 'mysql:5.7'
|
||||
prefix: flarum_
|
||||
- php: 7.3
|
||||
service: mariadb
|
||||
prefix: flarum_
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: ${{ matrix.service }}
|
||||
ports:
|
||||
- 13306:3306
|
||||
|
||||
name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Select PHP version
|
||||
run: sudo update-alternatives --set php $(which php${{ matrix.php }})
|
||||
|
||||
- name: Create MySQL Database
|
||||
run: |
|
||||
sudo systemctl start mysql
|
||||
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
|
||||
|
||||
- name: Install Composer dependencies
|
||||
run: composer install
|
||||
|
||||
- name: Setup Composer tests
|
||||
run: composer test:setup
|
||||
env:
|
||||
DB_PORT: 13306
|
||||
DB_PASSWORD: root
|
||||
DB_PREFIX: ${{ matrix.prefix }}
|
||||
|
||||
- name: Run Composer tests
|
||||
run: composer test
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
/vendor
|
||||
composer.lock
|
||||
composer.phar
|
||||
node_modules
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
tests/_output/*
|
||||
/tests/integration/tmp
|
||||
.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"
|
30
.travis.yml
30
.travis.yml
@@ -1,30 +0,0 @@
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 5.5
|
||||
- 5.6
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- php: hhvm
|
||||
fast_finish: true
|
||||
|
||||
before_script:
|
||||
- curl -s http://getcomposer.org/installer | php
|
||||
- php composer.phar install
|
||||
|
||||
script:
|
||||
- vendor/bin/phpcs --standard=PSR2 -np src
|
||||
- vendor/bin/phpspec run
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_failure: change
|
||||
webhooks:
|
||||
urls:
|
||||
- https://webhooks.gitter.im/e/7b9e9827a03b44a16588
|
||||
on_success: always
|
||||
on_failure: always
|
||||
on_start: false
|
||||
|
||||
sudo: false
|
188
CHANGELOG.md
Normal file
188
CHANGELOG.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Changelog
|
||||
|
||||
## [0.1.0-beta.13](https://github.com/flarum/core/compare/v0.1.0-beta.12...v0.1.0-beta.13)
|
||||
|
||||
### Added
|
||||
- Middleware extender (#2017, #2063, #2084)
|
||||
- Console extender (#2057)
|
||||
- CSRF extender (#2095)
|
||||
- Event extender (#2097)
|
||||
- Mail extender (#2012)
|
||||
- Model extender (#2100)
|
||||
- Show discussion start user as html class on post
|
||||
- PHPUnit 8 compatibility.
|
||||
- Composer 2 compatibility
|
||||
- Permission groups can now be hidden (#2129)
|
||||
- Confirmation popup when hiding or deleting posts (#2135)
|
||||
|
||||
### Changed
|
||||
- Updated less.php dependency version to 3.0.
|
||||
- All notifications now processed through the queue (#1931)
|
||||
- Updated JS dependencies
|
||||
- Simplified uploads, removing need to store intermediate files (#2117)
|
||||
- Improved date handling for dates older than 1 year (#2034)
|
||||
- Linting and automatic formatting for JS (#2099)
|
||||
- Translation files from Language Packs are only loaded for extensions that are enabled (#2020)
|
||||
|
||||
### Fixed
|
||||
- Users can no longer restore discussions hidden by others (#2037)
|
||||
- Issues of the Modal not showing or auto hiding (#2080)
|
||||
- Extensions page in admin showning columns incorrectly (#2111)
|
||||
- Non dismissable modals can be dismissed using the ESC key (#1917)
|
||||
- New post injected above unread sticky (#1868)
|
||||
- New discussions not visible to users when using Pusher (#2077)
|
||||
- Icons on admin permissions page (#2016, #2018)
|
||||
- Notification bubble contrast on mobile with colored header (#2109)
|
||||
- PostStreamScrubber click jumps back to first position (#1945)
|
||||
- Loading state of Switch toggle component is hard to see (#2039, #1491)
|
||||
- Allowing permission check to use class name based gate checks (#1977)
|
||||
|
||||
### Removed
|
||||
- Zend compatibility bridge (#2010)
|
||||
- SES mail support (#2011)
|
||||
- Backward compatibility dropped for mail drivers
|
||||
- Support for PHP 7.1
|
||||
- Deprecated Flarum\Util\Str helper class
|
||||
- Deprecated ConfigureMiddleware event
|
||||
|
||||
## [0.1.0-beta.12](https://github.com/flarum/core/compare/v0.1.0-beta.11.1...v0.1.0-beta.12)
|
||||
|
||||
### Added
|
||||
- Full support for PHP 7.4 (#1980)
|
||||
- Mail settings: Configure region for the Mailgun driver (#1834, #1850)
|
||||
- Mail settings: Alert admins about incomplete settings (#1763, #1921)
|
||||
- New permission that allows users to post without throttling (#1255, #1938)
|
||||
- Basic transliteration of discussion "slugs" / pretty URLs (#194, #1975)
|
||||
- User profiles: Render basic content on server side (#1901)
|
||||
- New extender for configuring middleware (#1919, #1952, #1957, #1971)
|
||||
- New extender for configuring error handling (#1781, #1970)
|
||||
- Automated tests for PHP extenders to guarantee their backwards compatibility
|
||||
|
||||
### Changed
|
||||
- Profile URLs for non-existing users properly return HTTP 404 (#1846, #1901)
|
||||
- Confirmation email subject no longer contains the forum title (#1613)
|
||||
- Improved error handling during Flarum's early boot phase (#1607)
|
||||
- Updated deprecated "Zend" libraries to their new "Laminas" equivalents (#1963)
|
||||
|
||||
### Fixed
|
||||
- Update page did not work when installed in subdirectories (#1947)
|
||||
- Avatar upload did not work in IE11 / Edge (#1125, #1570)
|
||||
- Translation fallback was ignored for client-rendered pages (#1774, #1961)
|
||||
- The success alert when posting replies was invisible (#1976)
|
||||
|
||||
## [0.1.0-beta.11.1](https://github.com/flarum/core/compare/v0.1.0-beta.11...v0.1.0-beta.11.1)
|
||||
|
||||
### Fixed
|
||||
- Saving custom css in admin failed (#1946)
|
||||
|
||||
## [0.1.0-beta.11](https://github.com/flarum/core/compare/v0.1.0-beta.10...v0.1.0-beta.11)
|
||||
|
||||
### Added
|
||||
- Comments have an additional class `Post--by-actor` when posted by the user (#1927)
|
||||
|
||||
### Changed
|
||||
- Improved support for URL identification during installation (#1861)
|
||||
- KeyboardNavigatable now has a callback ability (#1922)
|
||||
- Links are no longer opened with target `_blank` but in the same window (#859)
|
||||
- Links now have `nofollow ugc` by default as their `rel` attribute (#859, #1884)
|
||||
- Improved performance of the full text gambit when searching for users (#1877)
|
||||
- The Queue implementation is now available under its Illuminate contract
|
||||
|
||||
### Fixed
|
||||
- No error handling was possible in the console/cli (#1789)
|
||||
- Enable scrollbars in log in modals so it fits for GitHub (#1716)
|
||||
- Reduce log in modal for SSO so it fits for Facebook (#1727)
|
||||
- Deleting discussions permanently did not delete its posts (#1909)
|
||||
- Fixed the queue:restart command (#1932)
|
||||
- Deleted posts were visible to all visitors (#1827)
|
||||
- Old avatars weren't being deleted when replaced (#1918)
|
||||
- The search performance regression was reverted (#1764)
|
||||
- No profile background could be set for remote images (#445)
|
||||
- Back button sends to home even though it could actually go back (#1942)
|
||||
- Debug button no longer visible (#1687)
|
||||
- Modals on smaller screens use the whole width of the page
|
||||
|
||||
## [0.1.0-beta.10](https://github.com/flarum/core/compare/v0.1.0-beta.9...v0.1.0-beta.10)
|
||||
|
||||
### Added
|
||||
- Initial queue support: Infrastructure for offloading long-running tasks (e.g. email sending) to background workers (#1773)
|
||||
- Notifications can now be marked as read without visiting a discussion (#151)
|
||||
- SEO: The discussion list now has a `rel="canonical"` meta tag, preventing duplicate content (#1134, #1814)
|
||||
- The "Edit User" permission can now be edited in the UI (#1845)
|
||||
- New status message and redirect after user deletion (#1750, #1777)
|
||||
- Errors in Flarum's boot process are now presented with more detailed information (#1607)
|
||||
|
||||
### Changed
|
||||
- Better, more detailed and extensible error handling (#1641, #1843)
|
||||
- Error pages in debug mode now return the same HTTP status codes as in production (#1648)
|
||||
- Tweak HTTP status codes for authentication / authorization errors (#1854)
|
||||
- Already-used links from account activation emails now show a better error message (#1337)
|
||||
|
||||
### Fixed
|
||||
- Security vulnerabilities in dependencies
|
||||
- Performance: High CPU usage when scrolling in a discussion (#1222)
|
||||
- Special characters crashed the search (#1498)
|
||||
- Missing declarations for language and text direction in HTML output (#1772)
|
||||
- Private messages were counted in user post counts (#1695)
|
||||
- Extensions could not change the forum's default page (#1819)
|
||||
- API requests authenticated using access tokens needed to provide a CSRF token (#1828)
|
||||
- Accessibility: Screenreaders did not read the "Back to discussion list" link (#1835)
|
||||
|
||||
## [0.1.0-beta.9](https://github.com/flarum/core/compare/v0.1.0-beta.8.2...v0.1.0-beta.9)
|
||||
|
||||
### Added
|
||||
- New `hasPermission()` helper method for `Group` objects ([9684fbc](https://github.com/flarum/core/commit/9684fbc4da07d32aa322d9228302a23418412cb9))
|
||||
- Expose supported mail drivers in IoC container ([208bad3](https://github.com/flarum/core/commit/208bad393f37bfdb76007afcddfa4b7451563e9d))
|
||||
- More test for some API endpoints ([1670590](https://github.com/flarum/core/commit/167059027e5a066d618599c90164ef1b5a509148))
|
||||
- The `Formatter\Rendering` event now receives the HTTP request instance as well ([0ab9fac](https://github.com/flarum/core/commit/0ab9facc4bd59a260575e6fc650793c663e5866a))
|
||||
- More and better validation in installer UIs
|
||||
- Check and enforce minimum MariaDB ([7ff9a90](https://github.com/flarum/core/commit/7ff9a90204923293adc520d3c02dc984845d4f9f))
|
||||
- Revert publication of assets when installation fails ([ed9591c](https://github.com/flarum/core/commit/ed9591c16fb2ea7a4be3387b805d855a53e0a7d5))
|
||||
- Benefit from Laravel's database reconnection logic in long-running tasks ([e0becd0](https://github.com/flarum/core/commit/e0becd0c7bda939048923c1f86648793feee78d5))
|
||||
- The "vendor path" (where Composer dependencies can be found) can now be configured ([5e1680c](https://github.com/flarum/core/commit/5e1680c458cd3ba274faeb92de3ac2053789131e))
|
||||
|
||||
### Changed
|
||||
- Performance: Actually cache translations on disk ([0d16fac](https://github.com/flarum/core/commit/0d16fac001bb735ee66e82871183516aeac269b7))
|
||||
- Allow per-site extenders to override extension extenders ([ba594de](https://github.com/flarum/core/commit/ba594de13a033480834d53d73f747b05fe9796f8))
|
||||
- Do not resolve objects from the IoC container (in service providers and extenders) until they are actually used
|
||||
- Replace event subscribers (that resolve objects from the IoC container) with listeners (that resolve lazily)
|
||||
- Use custom service provider for Mail component ([ac5e26a](https://github.com/flarum/core/commit/ac5e26a254d89e21bd4c115b6cbd40338e2e4b4b))
|
||||
- Update to Laravel 5.7, revert custom logic for building database index names
|
||||
- Refactored installer, extracted Installation class and pipeline for reuse in CLI and web installers ([790d5be](https://github.com/flarum/core/commit/790d5beee5e283178716bc8f9901c758d9e5b6a0))
|
||||
- Use whitelist for enabling pre-installed extensions during installation ([4585f03](https://github.com/flarum/core/commit/4585f03ee356c92942fbc2ae8c683c651b473954))
|
||||
- Update minimum MySQL version ([7ff9a90](https://github.com/flarum/core/commit/7ff9a90204923293adc520d3c02dc984845d4f9f))
|
||||
|
||||
### Fixed
|
||||
- Signing up via OAuth providers was broken ([67f9375](https://github.com/flarum/core/commit/67f9375d4745add194ae3249d526197c32fd5461))
|
||||
- Group badges were overlapping ([16eb1fa](https://github.com/flarum/core/commit/16eb1fa63b6d7b80ec30c24c0e406a2b7ab09934))
|
||||
- API: Endpoint for uninstalling extensions returned an error ([c761802](https://github.com/flarum/core/commit/c76180290056ddbab67baf5ede814fcedf1dcf14))
|
||||
- Documentation links in installer were outdated ([b58380e](https://github.com/flarum/core/commit/b58380e224ee54abdade3d0a4cc107ef5c91c9a9))
|
||||
- Event posts where counted when aggregating user posts ([671fdec](https://github.com/flarum/core/commit/671fdec8d0a092ccceb5d4d5f657d0f4287fc4c7))
|
||||
- Admins could not reset user passwords ([c67fb2d](https://github.com/flarum/core/commit/c67fb2d4b6a128c71d65dc6703310c0b62f91be2))
|
||||
- Several down migrations were invalid
|
||||
- Validation errors on reset password page resulted in HTTP 404 ([4611abe](https://github.com/flarum/core/commit/4611abe5db8b94ca3dc7bf9c447fca7c67358ee3))
|
||||
- `is:unread` gambit generated an invalid query ([e17bb0b](https://github.com/flarum/core/commit/e17bb0b4331f2c92459292195c6b7db8cde1f9f3))
|
||||
- Entire forum was breaking when the `custom_less` setting was missing from the database ([bf2c5a5](https://github.com/flarum/core/commit/bf2c5a5564dff3f5ef13efe7a8d69f2617570ce6))
|
||||
- Dropdown icon was not showing in user card when on user page ([12fdfc9](https://github.com/flarum/core/commit/12fdfc9b544a27f6fe59c82ad6bddd3420cc0181))
|
||||
- Requests were missing the `original*` attributes, which broke installations in subfolders ([56fde28](https://github.com/flarum/core/commit/56fde28e436f52fee0c03c538f0a6049bc584b53))
|
||||
- Special characters such as `%` and `_` could return incorrect results ([ee3640e](https://github.com/flarum/core/commit/ee3640e1605ff67fef4b3d5cd0596f14a6ae73c9))
|
||||
- FontAwesome component package changed paths in version 5.9.0 ([5eb69e1](https://github.com/flarum/core/commit/5eb69e1f59fa73fdfd5badbf41a05a6a040e7426))
|
||||
- Some server environments had problems accessing the system-wide tmp path for storing JS file maps ([54660eb](https://github.com/flarum/core/commit/54660ebd6311f9ea142f1b573263d0d907400786))
|
||||
- Content length of posts.content was not migrated to mediumText in 2017 ([590b311](https://github.com/flarum/core/commit/590b3115708bf94a9c7f169d98c6126380c7056e))
|
||||
- An error occurred when going to the previous route if there was no previous route found ([985b87da](https://github.com/flarum/core/commit/985b87da6c9942c568a1a192e2fdcfde72e030ee))
|
||||
|
||||
### Removed
|
||||
- `php flarum install --defaults` - this was meant to be used in our old development VM ([44c9109](https://github.com/flarum/core/commit/44c91099cd77138bb5fc29f14fb1e81a9781272d))
|
||||
- Obsolete `id` attributes in JSON-API responses ([ecc3b5e](https://github.com/flarum/core/commit/ecc3b5e2271f8d9b38d52cd54476d86995dbe32e) and [7a44086](https://github.com/flarum/core/commit/7a44086bf3a0e3ba907dceb13d07ac695eca05ea))
|
||||
|
||||
## [0.1.0-beta.8.1](https://github.com/flarum/core/compare/v0.1.0-beta.8...v0.1.0-beta.8.1)
|
||||
|
||||
### Fixed
|
||||
- Fix live output in `migrate:reset` command ([f591585](https://github.com/flarum/core/commit/f591585d02f8c4ff0211c5bf4413dd6baa724c05))
|
||||
- Fix search with database prefix ([7705a2b](https://github.com/flarum/core/commit/7705a2b7d751943ef9d0c7379ec34f8530b99310))
|
||||
- Fix invalid join time of admin user created by installer ([57f73c9](https://github.com/flarum/core/commit/57f73c9638eeb825f9e336ed3c443afccfd8995e))
|
||||
- Ensure InnoDB engine is used for all tables ([fb6b51b](https://github.com/flarum/core/commit/fb6b51b1cfef0af399607fe038603c8240800b2b), [6370f7e](https://github.com/flarum/core/commit/6370f7ecffa9ea7d5fb64d9551400edbc63318db))
|
||||
- Fix dropping foreign keys in `down` migrations ([57d5846](https://github.com/flarum/core/commit/57d5846b647881009d9e60f9ffca20b1bb77776e))
|
||||
- Fix discussion list scroll position not being maintained when hero is not visible ([40dc6ac](https://github.com/flarum/core/commit/40dc6ac604c2a0973356b38217aa8d09352daae5))
|
||||
- Fix empty meta description tag ([88e43cc](https://github.com/flarum/core/commit/88e43cc6940ee30d6529e9ce659471ec4fb1c474))
|
||||
- Remove empty attributes on `<html>` tag ([796b577](https://github.com/flarum/core/commit/796b57753d34d4ea741dbebcbc550b17808f6c94))
|
3
LICENSE
3
LICENSE
@@ -1,6 +1,7 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2015 Toby Zerner
|
||||
Copyright (c) 2019-2020 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
34
README.md
34
README.md
@@ -1,7 +1,35 @@
|
||||
# Flarum Core
|
||||
<p align="center"><img src="https://flarum.org/img/logo.png"></p>
|
||||
|
||||
This repository contains the core code of Flarum. If you want to install Flarum, visit the [main Flarum repository](http://github.com/flarum/flarum).
|
||||
<p align="center">
|
||||
<a href="https://travis-ci.org/flarum/core"><img src="https://travis-ci.org/flarum/core.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/d/total.svg" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/v/stable.svg" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/license.svg" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## About Flarum
|
||||
|
||||
**[Flarum](https://flarum.org/) is a delightfully simple discussion platform for your website.** It's fast and easy to use, with all the features you need to run a successful community. It is designed to be:
|
||||
|
||||
* **Fast and simple.** No clutter, no bloat, no complex dependencies. Flarum is built with PHP so it’s quick and easy to deploy. The interface is powered by Mithril, a performant JavaScript framework with a tiny footprint.
|
||||
|
||||
* **Beautiful and responsive.** This is forum software for humans. Flarum is carefully designed to be consistent and intuitive across platforms, out-of-the-box.
|
||||
|
||||
* **Powerful and extensible.** Customize, extend, and integrate Flarum to suit your community. Flarum’s architecture is amazingly flexible, with a powerful Extension API.
|
||||
|
||||
## Installation
|
||||
|
||||
This repository contains Flarum's core code. If you want to set up a forum, visit the [Flarum skeleton repository](https://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.
|
||||
Thank you for considering contributing to Flarum! Please read the **[Contributing guide](https://flarum.org/docs/contributing.html)** to learn how you can help.
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability within Flarum, please send an e-mail to [security@flarum.org](mailto:security@flarum.org). All security vulnerabilities will be promptly addressed. More details can be found in our [security policy](https://github.com/flarum/core/security/policy).
|
||||
|
||||
## License
|
||||
|
||||
Flarum is open-source software licensed under the [MIT License](https://github.com/flarum/flarum/blob/master/LICENSE).
|
||||
|
||||
|
129
composer.json
129
composer.json
@@ -1,42 +1,87 @@
|
||||
{
|
||||
"name": "flarum/core",
|
||||
"description": "",
|
||||
"description": "Delightfully simple forum software.",
|
||||
"keywords": ["forum", "discussion"],
|
||||
"homepage": "https://flarum.org/",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Toby Zerner",
|
||||
"email": "toby@flarum.org"
|
||||
"name": "Franz Liedke",
|
||||
"email": "franz@develophp.org"
|
||||
},
|
||||
{
|
||||
"name": "Daniel Klabbers",
|
||||
"email": "daniel@klabbers.email",
|
||||
"homepage": "https://luceos.com"
|
||||
},
|
||||
{
|
||||
"name": "David Sevilla Martin",
|
||||
"email": "me+flarum@datitisev.me",
|
||||
"homepage": "https://datitisev.me"
|
||||
},
|
||||
{
|
||||
"name": "Clark Winkelmann",
|
||||
"email": "clark.winkelmann@gmail.com",
|
||||
"homepage": "https://clarkwinkelmann.com"
|
||||
},
|
||||
{
|
||||
"name": "Matthew Kilgore",
|
||||
"email": "matthew@kilgore.dev"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/flarum/core/issues",
|
||||
"source": "https://github.com/flarum/core",
|
||||
"docs": "https://flarum.org/docs/"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.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/events": "5.1.*",
|
||||
"illuminate/filesystem": "5.1.*",
|
||||
"illuminate/hashing": "5.1.*",
|
||||
"illuminate/mail": "5.1.*",
|
||||
"illuminate/support": "5.1.*",
|
||||
"illuminate/validation": "5.1.*",
|
||||
"illuminate/view": "5.1.*",
|
||||
"php": ">=7.2",
|
||||
"axy/sourcemap": "^0.1.4",
|
||||
"components/font-awesome": "5.9.*",
|
||||
"dflydev/fig-cookies": "^1.0.2",
|
||||
"doctrine/dbal": "^2.7",
|
||||
"franzl/whoops-middleware": "^0.4.0",
|
||||
"illuminate/bus": "5.7.*",
|
||||
"illuminate/cache": "5.7.*",
|
||||
"illuminate/config": "5.7.*",
|
||||
"illuminate/container": "5.7.*",
|
||||
"illuminate/contracts": "5.7.*",
|
||||
"illuminate/database": "5.7.*",
|
||||
"illuminate/events": "5.7.*",
|
||||
"illuminate/filesystem": "5.7.*",
|
||||
"illuminate/hashing": "5.7.*",
|
||||
"illuminate/mail": "5.7.*",
|
||||
"illuminate/queue": "5.7.*",
|
||||
"illuminate/session": "5.7.*",
|
||||
"illuminate/support": "5.7.*",
|
||||
"illuminate/validation": "5.7.*",
|
||||
"illuminate/view": "5.7.*",
|
||||
"intervention/image": "^2.5.0",
|
||||
"laminas/laminas-diactoros": "^1.8.4",
|
||||
"laminas/laminas-httphandlerrunner": "^1.0",
|
||||
"laminas/laminas-stratigility": "^3.0",
|
||||
"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",
|
||||
"matthiasmullie/minify": "^1.3",
|
||||
"middlewares/base-path": "^1.1",
|
||||
"middlewares/base-path-router": "^0.2.1",
|
||||
"middlewares/request-handler": "^1.2",
|
||||
"monolog/monolog": "^1.16.0",
|
||||
"nikic/fast-route": "^0.6",
|
||||
"dflydev/fig-cookies": "^1.0",
|
||||
"symfony/console": "^2.7",
|
||||
"symfony/yaml": "^2.7"
|
||||
"psr/http-message": "^1.0",
|
||||
"psr/http-server-handler": "^1.0",
|
||||
"psr/http-server-middleware": "^1.0",
|
||||
"s9e/text-formatter": "^2.3.6",
|
||||
"symfony/config": "^3.3",
|
||||
"symfony/console": "^4.2",
|
||||
"symfony/event-dispatcher": "^4.3.2",
|
||||
"symfony/translation": "^3.3",
|
||||
"symfony/yaml": "^3.3",
|
||||
"tobscure/json-api": "^0.3.0",
|
||||
"wikimedia/less.php": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"squizlabs/php_codesniffer": "2.*",
|
||||
"phpspec/phpspec": "^2.2"
|
||||
"mockery/mockery": "^1.0",
|
||||
"phpunit/phpunit": "^7.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -45,5 +90,33 @@
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Flarum\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "0.1.x-dev"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": [
|
||||
"@test:unit",
|
||||
"@test:integration"
|
||||
],
|
||||
"test:unit": "phpunit -c tests/phpunit.unit.xml",
|
||||
"test:integration": "phpunit -c tests/phpunit.integration.xml",
|
||||
"test:setup": "@php tests/integration/setup.php"
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"test": "Runs all tests.",
|
||||
"test:unit": "Runs all unit tests.",
|
||||
"test:integration": "Runs all integration tests.",
|
||||
"test:setup": "Sets up a database for use with integration tests. Execute this only once."
|
||||
}
|
||||
}
|
||||
|
2392
composer.lock
generated
2392
composer.lock
generated
File diff suppressed because it is too large
Load Diff
1
js/.gitignore
vendored
1
js/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
bower_components
|
6
js/.prettierrc.json
Normal file
6
js/.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"printWidth": 150,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
@@ -8,10 +7,5 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Core\Users;
|
||||
|
||||
use Flarum\Core\Model;
|
||||
|
||||
class Permission extends Model
|
||||
{
|
||||
}
|
||||
export * from './src/common';
|
||||
export * from './src/admin';
|
3
js/admin/.gitignore
vendored
3
js/admin/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
node_modules
|
||||
mithril.js
|
||||
dist
|
@@ -1,33 +0,0 @@
|
||||
var gulp = require('flarum-gulp');
|
||||
|
||||
var nodeDir = 'node_modules';
|
||||
var bowerDir = '../bower_components';
|
||||
|
||||
gulp({
|
||||
files: [
|
||||
nodeDir + '/babel-core/external-helpers.js',
|
||||
|
||||
bowerDir + '/es6-micro-loader/dist/system-polyfill.js',
|
||||
|
||||
bowerDir + '/mithril/mithril.js',
|
||||
bowerDir + '/jquery/dist/jquery.js',
|
||||
bowerDir + '/moment/moment.js',
|
||||
|
||||
bowerDir + '/bootstrap/js/affix.js',
|
||||
bowerDir + '/bootstrap/js/dropdown.js',
|
||||
bowerDir + '/bootstrap/js/modal.js',
|
||||
bowerDir + '/bootstrap/js/tooltip.js',
|
||||
bowerDir + '/bootstrap/js/transition.js',
|
||||
|
||||
bowerDir + '/spin.js/spin.js',
|
||||
bowerDir + '/spin.js/jquery.spin.js'
|
||||
],
|
||||
modules: {
|
||||
'flarum': [
|
||||
'src/**/*.js',
|
||||
'../lib/**/*.js'
|
||||
]
|
||||
},
|
||||
externalHelpers: true,
|
||||
outputFile: 'dist/app.js'
|
||||
});
|
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"gulp": "^3.8.11",
|
||||
"flarum-gulp": "git+https://github.com/flarum/gulp.git",
|
||||
"babel-core": "^5.0.0"
|
||||
}
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
import App from 'flarum/App';
|
||||
import store from 'flarum/initializers/store';
|
||||
import preload from 'flarum/initializers/preload';
|
||||
import routes from 'flarum/initializers/routes';
|
||||
import boot from 'flarum/initializers/boot';
|
||||
|
||||
const app = new App();
|
||||
|
||||
app.initializers.add('store', store);
|
||||
app.initializers.add('routes', routes);
|
||||
|
||||
app.initializers.add('preload', preload, -100);
|
||||
app.initializers.add('boot', boot, -100);
|
||||
|
||||
app.extensionSettings = {};
|
||||
|
||||
export default app;
|
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import Modal from 'flarum/components/Modal';
|
||||
|
||||
export default class AddExtensionModal extends Modal {
|
||||
className() {
|
||||
return 'AddExtensionModal Modal--small';
|
||||
}
|
||||
|
||||
title() {
|
||||
return 'Add Extension';
|
||||
}
|
||||
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import Component from 'flarum/Component';
|
||||
import AdminLinkButton from 'flarum/components/AdminLinkButton';
|
||||
import SelectDropdown from 'flarum/components/SelectDropdown';
|
||||
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
|
||||
export default class AdminNav extends Component {
|
||||
view() {
|
||||
return (
|
||||
<SelectDropdown
|
||||
className="AdminNav App-titleControl"
|
||||
buttonClassName="Button"
|
||||
children={this.items().toArray()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list of links to show in the admin navigation.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('dashboard', AdminLinkButton.component({
|
||||
href: app.route('dashboard'),
|
||||
icon: 'bar-chart',
|
||||
children: 'Dashboard',
|
||||
description: 'Your forum at a glance.'
|
||||
}));
|
||||
|
||||
items.add('basics', AdminLinkButton.component({
|
||||
href: app.route('basics'),
|
||||
icon: 'pencil',
|
||||
children: 'Basics',
|
||||
description: 'Set your forum title, language, and other basic settings.'
|
||||
}));
|
||||
|
||||
items.add('permissions', AdminLinkButton.component({
|
||||
href: app.route('permissions'),
|
||||
icon: 'key',
|
||||
children: 'Permissions',
|
||||
description: 'Configure who can see and do what.'
|
||||
}));
|
||||
|
||||
items.add('appearance', AdminLinkButton.component({
|
||||
href: app.route('appearance'),
|
||||
icon: 'paint-brush',
|
||||
children: 'Appearance',
|
||||
description: 'Customize your forum\'s colors, logos, and other variables.'
|
||||
}));
|
||||
|
||||
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.'
|
||||
}));
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
@@ -1,88 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Button from 'flarum/components/Button';
|
||||
import Switch from 'flarum/components/Switch';
|
||||
import EditCustomCssModal from 'flarum/components/EditCustomCssModal';
|
||||
import saveConfig from 'flarum/utils/saveConfig';
|
||||
|
||||
export default class AppearancePage extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
view() {
|
||||
return (
|
||||
<div className="AppearancePage">
|
||||
<div className="container">
|
||||
<form onsubmit={this.onsubmit.bind(this)}>
|
||||
<fieldset className="AppearancePage-colors">
|
||||
<legend>Colors</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.
|
||||
</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)}/>
|
||||
</div>
|
||||
|
||||
{Switch.component({
|
||||
state: this.darkMode(),
|
||||
children: 'Dark Mode',
|
||||
onchange: this.darkMode
|
||||
})}
|
||||
|
||||
{Switch.component({
|
||||
state: this.coloredHeader(),
|
||||
children: 'Colored Header',
|
||||
onchange: this.coloredHeader
|
||||
})}
|
||||
|
||||
{Button.component({
|
||||
className: 'Button Button--primary',
|
||||
children: 'Save Changes',
|
||||
loading: this.loading
|
||||
})}
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<fieldset>
|
||||
<legend>Custom Styles</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.
|
||||
</div>
|
||||
{Button.component({
|
||||
className: 'Button',
|
||||
children: 'Edit Custom CSS',
|
||||
onclick: () => app.modal.show(new EditCustomCssModal())
|
||||
})}
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
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.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
saveConfig({
|
||||
theme_primary_color: this.primaryColor(),
|
||||
theme_secondary_color: this.secondaryColor(),
|
||||
theme_dark_mode: this.darkMode(),
|
||||
theme_colored_header: this.coloredHeader()
|
||||
}).then(() => window.location.reload());
|
||||
}
|
||||
}
|
@@ -1,144 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
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';
|
||||
|
||||
export default class BasicsPage extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.loading = false;
|
||||
|
||||
this.fields = [
|
||||
'forum_title',
|
||||
'forum_description',
|
||||
'default_locale',
|
||||
'default_route',
|
||||
'welcome_title',
|
||||
'welcome_message'
|
||||
];
|
||||
this.values = {};
|
||||
|
||||
const config = app.config;
|
||||
this.fields.forEach(key => this.values[key] = m.prop(config[key]));
|
||||
|
||||
this.localeOptions = {};
|
||||
const locales = app.locales;
|
||||
for (const i in locales) {
|
||||
this.localeOptions[i] = `${locales[i]} (${i})`;
|
||||
}
|
||||
}
|
||||
|
||||
view() {
|
||||
return (
|
||||
<div className="BasicsPage">
|
||||
<div className="container">
|
||||
<form onsubmit={this.onsubmit.bind(this)}>
|
||||
{FieldSet.component({
|
||||
label: 'Forum Title',
|
||||
children: [
|
||||
<input className="FormControl" value={this.values.forum_title()} oninput={m.withAttr('value', this.values.forum_title)}/>
|
||||
]
|
||||
})}
|
||||
|
||||
{FieldSet.component({
|
||||
label: 'Forum Description',
|
||||
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.
|
||||
</div>,
|
||||
<textarea className="FormControl" value={this.values.forum_description()} oninput={m.withAttr('value', this.values.forum_description)}/>
|
||||
]
|
||||
})}
|
||||
|
||||
{Object.keys(this.localeOptions).length > 1
|
||||
? FieldSet.component({
|
||||
label: 'Default Language',
|
||||
children: [
|
||||
Select.component({
|
||||
options: this.localeOptions,
|
||||
onchange: this.values.default_locale
|
||||
})
|
||||
]
|
||||
})
|
||||
: ''}
|
||||
|
||||
{FieldSet.component({
|
||||
label: 'Home Page',
|
||||
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.
|
||||
</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>
|
||||
]
|
||||
})}
|
||||
|
||||
{FieldSet.component({
|
||||
label: 'Welcome Banner',
|
||||
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.
|
||||
</div>,
|
||||
<div className="BasicsPage-welcomeBanner-input">
|
||||
<input className="FormControl" value={this.values.welcome_title()} oninput={m.withAttr('value', this.values.welcome_title)}/>
|
||||
<textarea className="FormControl" value={this.values.welcome_message()} oninput={m.withAttr('value', this.values.welcome_message)}/>
|
||||
</div>
|
||||
]
|
||||
})}
|
||||
|
||||
{Button.component({
|
||||
type: 'submit',
|
||||
className: 'Button Button--primary',
|
||||
children: 'Save Changes',
|
||||
loading: this.loading,
|
||||
disabled: !this.changed()
|
||||
})}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
changed() {
|
||||
const config = app.config;
|
||||
|
||||
return this.fields.some(key => this.values[key]() !== config[key]);
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.loading) return;
|
||||
|
||||
this.loading = true;
|
||||
app.alerts.dismiss(this.successAlert);
|
||||
|
||||
const config = {};
|
||||
|
||||
this.fields.forEach(key => config[key] = this.values[key]());
|
||||
|
||||
saveConfig(config)
|
||||
.then(() => {
|
||||
app.alerts.show(this.successAlert = new Alert({type: 'success', children: 'Your changes were saved.'}));
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
import SelectDropdown from 'flarum/components/SelectDropdown';
|
||||
import Button from 'flarum/components/Button';
|
||||
import saveConfig from 'flarum/utils/saveConfig';
|
||||
|
||||
export default class ConfigDropdown extends SelectDropdown {
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.className = 'ConfigDropdown';
|
||||
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;
|
||||
|
||||
return Button.component({
|
||||
children: label,
|
||||
icon: active ? 'check' : true,
|
||||
onclick: saveConfig.bind(this, {[props.key]: value}),
|
||||
active
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
|
||||
export default class DashboardPage extends Component {
|
||||
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>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,51 +0,0 @@
|
||||
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 || '');
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'EditCustomCssModal Modal--large';
|
||||
}
|
||||
|
||||
title() {
|
||||
return 'Edit Custom CSS';
|
||||
}
|
||||
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
saveConfig({
|
||||
custom_less: this.customLess()
|
||||
}).then(() => window.location.reload());
|
||||
}
|
||||
}
|
@@ -1,105 +0,0 @@
|
||||
import Modal from 'flarum/components/Modal';
|
||||
import Button from 'flarum/components/Button';
|
||||
import Badge from 'flarum/components/Badge';
|
||||
import Group from 'flarum/models/Group';
|
||||
|
||||
/**
|
||||
* The `EditGroupModal` component shows a modal dialog which allows the user
|
||||
* to create or edit a group.
|
||||
*/
|
||||
export default class EditGroupModal extends Modal {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.group = this.props.group || app.store.createRecord('groups');
|
||||
|
||||
this.nameSingular = m.prop(this.group.nameSingular() || '');
|
||||
this.namePlural = m.prop(this.group.namePlural() || '');
|
||||
this.icon = m.prop(this.group.icon() || '');
|
||||
this.color = m.prop(this.group.color() || '');
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'EditGroupModal Modal--small';
|
||||
}
|
||||
|
||||
title() {
|
||||
return [
|
||||
this.color() || this.icon() ? Badge.component({
|
||||
icon: this.icon(),
|
||||
style: {backgroundColor: this.color()}
|
||||
}) : '',
|
||||
' ',
|
||||
this.namePlural() || 'Create Group'
|
||||
];
|
||||
}
|
||||
|
||||
content() {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form">
|
||||
<div className="Form-group">
|
||||
<label>Name</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)}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
<label>Color</label>
|
||||
<input className="FormControl" placeholder="#aaaaaa" value={this.color()} oninput={m.withAttr('value', this.color)}/>
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
<label>Icon</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.
|
||||
</div>
|
||||
<input className="FormControl" placeholder="bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)}/>
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
{Button.component({
|
||||
type: 'submit',
|
||||
className: 'Button Button--primary EditGroupModal-save',
|
||||
loading: this.loading,
|
||||
children: 'Save Changes'
|
||||
})}
|
||||
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
|
||||
<button type="button" className="Button EditGroupModal-delete" onclick={this.delete.bind(this)}>
|
||||
Delete Group
|
||||
</button>
|
||||
) : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.group.save({
|
||||
nameSingular: this.nameSingular(),
|
||||
namePlural: this.namePlural(),
|
||||
color: this.color(),
|
||||
icon: this.icon()
|
||||
}).then(
|
||||
() => this.hide(),
|
||||
() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
delete() {
|
||||
if (confirm('Are you sure you want to delete this group? The group members will NOT be deleted.')) {
|
||||
this.group.delete().then(() => m.redraw());
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,115 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import LinkButton from 'flarum/components/LinkButton';
|
||||
import Button from 'flarum/components/Button';
|
||||
import Dropdown from 'flarum/components/Dropdown';
|
||||
import Separator from 'flarum/components/Separator';
|
||||
import AddExtensionModal from 'flarum/components/AddExtensionModal';
|
||||
import LoadingModal from 'flarum/components/LoadingModal';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
export default class ExtensionsPage extends Component {
|
||||
view() {
|
||||
return (
|
||||
<div className="ExtensionsPage">
|
||||
<div className="ExtensionsPage-header">
|
||||
<div className="container">
|
||||
{Button.component({
|
||||
children: 'Add Extension',
|
||||
icon: 'plus',
|
||||
className: 'Button Button--primary',
|
||||
onclick: () => app.modal.show(new AddExtensionModal())
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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'
|
||||
})}
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
controlItems(extension) {
|
||||
const items = new ItemList();
|
||||
const enabled = this.isEnabled(extension.name);
|
||||
|
||||
if (app.extensionSettings[extension.name]) {
|
||||
items.add('settings', Button.component({
|
||||
icon: 'cog',
|
||||
children: 'Settings',
|
||||
onclick: app.extensionSettings[extension.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',
|
||||
onclick: () => {
|
||||
app.request({
|
||||
url: app.forum.attribute('apiUrl') + '/extensions/' + extension.name,
|
||||
method: 'DELETE',
|
||||
}).then(() => window.location.reload());
|
||||
|
||||
app.modal.show(new LoadingModal());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
return enabled.indexOf(name) !== -1;
|
||||
}
|
||||
}
|
@@ -1,26 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
|
||||
/**
|
||||
* The `HeaderPrimary` component displays primary header controls. On the
|
||||
* default skin, these are shown just to the right of the forum title.
|
||||
*/
|
||||
export default class HeaderPrimary extends Component {
|
||||
view() {
|
||||
return (
|
||||
<ul className="Header-controls">
|
||||
{listItems(this.items().toArray())}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the controls.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
items() {
|
||||
return new ItemList();
|
||||
}
|
||||
}
|
@@ -1,30 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import SessionDropdown from 'flarum/components/SessionDropdown';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
|
||||
/**
|
||||
* The `HeaderSecondary` component displays secondary header controls.
|
||||
*/
|
||||
export default class HeaderSecondary extends Component {
|
||||
view() {
|
||||
return (
|
||||
<ul className="Header-controls">
|
||||
{listItems(this.items().toArray())}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the controls.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('session', SessionDropdown.component());
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
@@ -1,115 +0,0 @@
|
||||
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 GroupBadge from 'flarum/components/GroupBadge';
|
||||
|
||||
function badgeForId(id) {
|
||||
const group = app.store.getById('groups', id);
|
||||
|
||||
return group ? GroupBadge.component({group, label: null}) : '';
|
||||
}
|
||||
|
||||
export default class PermissionDropdown extends Dropdown {
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.className = 'PermissionDropdown';
|
||||
props.buttonClassName = 'Button Button--text';
|
||||
}
|
||||
|
||||
view() {
|
||||
this.props.children = [];
|
||||
|
||||
const groupIds = app.permissions[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';
|
||||
} else if (members) {
|
||||
this.props.label = 'Members';
|
||||
} else {
|
||||
this.props.label = [
|
||||
badgeForId(Group.ADMINISTRATOR_ID),
|
||||
groupIds.map(badgeForId)
|
||||
];
|
||||
}
|
||||
|
||||
if (this.props.allowGuest) {
|
||||
this.props.children.push(
|
||||
Button.component({
|
||||
children: 'Everyone',
|
||||
icon: everyone ? 'check' : true,
|
||||
onclick: () => this.save([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.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/permission',
|
||||
data: {permission, groupIds}
|
||||
});
|
||||
}
|
||||
|
||||
toggle(groupId) {
|
||||
const permission = this.props.permission;
|
||||
|
||||
let groupIds = app.permissions[permission] || [];
|
||||
|
||||
const index = groupIds.indexOf(groupId);
|
||||
|
||||
if (index !== -1) {
|
||||
groupIds.splice(index, 1);
|
||||
} else {
|
||||
groupIds.push(groupId);
|
||||
groupIds = groupIds.filter(id => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(id) === -1);
|
||||
}
|
||||
|
||||
this.save(groupIds);
|
||||
}
|
||||
}
|
@@ -1,212 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import PermissionDropdown from 'flarum/components/PermissionDropdown';
|
||||
import ConfigDropdown from 'flarum/components/ConfigDropdown';
|
||||
import Button from 'flarum/components/Button';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
|
||||
export default class PermissionGrid extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.permissions = this.permissionItems().toArray();
|
||||
}
|
||||
|
||||
view() {
|
||||
const scopes = this.scopeItems().toArray();
|
||||
|
||||
const permissionCells = permission => {
|
||||
return scopes.map(scope => (
|
||||
<td>
|
||||
{scope.render(permission)}
|
||||
</td>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<table className="PermissionGrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<td></td>
|
||||
{scopes.map(scope => (
|
||||
<th>
|
||||
{scope.label}{' '}
|
||||
{scope.onremove ? Button.component({icon: 'times', className: 'Button Button--text PermissionGrid-removeScope', onclick: scope.onremove}) : ''}
|
||||
</th>
|
||||
))}
|
||||
<th>{this.scopeControlItems().toArray()}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.permissions.map(section => (
|
||||
<tbody>
|
||||
<tr className="PermissionGrid-section">
|
||||
<th>{section.label}</th>
|
||||
{permissionCells(section)}
|
||||
<td/>
|
||||
</tr>
|
||||
{section.children.map(child => (
|
||||
<tr className="PermissionGrid-child">
|
||||
<th>{child.label}</th>
|
||||
{permissionCells(child)}
|
||||
<td/>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
))}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
permissionItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('view', {
|
||||
label: 'View the forum',
|
||||
children: this.viewItems().toArray()
|
||||
});
|
||||
|
||||
items.add('start', {
|
||||
label: 'Start discussions',
|
||||
children: this.startItems().toArray()
|
||||
});
|
||||
|
||||
items.add('reply', {
|
||||
label: 'Reply to discussions',
|
||||
children: this.replyItems().toArray()
|
||||
});
|
||||
|
||||
items.add('moderate', {
|
||||
label: 'Moderate',
|
||||
children: this.moderateItems().toArray()
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
viewItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('view', {
|
||||
label: 'View discussions',
|
||||
permission: 'forum.view',
|
||||
allowGuest: true
|
||||
});
|
||||
|
||||
items.add('signUp', {
|
||||
label: 'Sign up',
|
||||
setting: () => ConfigDropdown.component({
|
||||
key: 'allow_sign_up',
|
||||
options: [
|
||||
{value: '1', label: 'Open'},
|
||||
{value: '0', label: 'Closed'}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
startItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('start', {
|
||||
label: 'Start discussions',
|
||||
permission: 'forum.startDiscussion'
|
||||
});
|
||||
|
||||
items.add('allowRenaming', {
|
||||
label: 'Allow renaming',
|
||||
setting: () => {
|
||||
const minutes = parseInt(app.config.allow_renaming, 10);
|
||||
|
||||
return ConfigDropdown.component({
|
||||
defaultLabel: minutes ? `For ${minutes} minutes` : 'Indefinitely',
|
||||
key: 'allow_renaming',
|
||||
options: [
|
||||
{value: '-1', label: 'Indefinitely'},
|
||||
{value: '10', label: 'For 10 minutes'},
|
||||
{value: 'reply', label: 'Until next reply'}
|
||||
]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
replyItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('reply', {
|
||||
label: 'Reply to discussions',
|
||||
permission: 'discussion.reply'
|
||||
});
|
||||
|
||||
items.add('allowPostEditing', {
|
||||
label: 'Allow post editing',
|
||||
setting: () => {
|
||||
const minutes = parseInt(app.config.allow_post_editing, 10);
|
||||
|
||||
return ConfigDropdown.component({
|
||||
defaultLabel: minutes ? `For ${minutes} minutes` : 'Indefinitely',
|
||||
key: 'allow_post_editing',
|
||||
options: [
|
||||
{value: '-1', label: 'Indefinitely'},
|
||||
{value: '10', label: 'For 10 minutes'},
|
||||
{value: 'reply', label: 'Until next reply'}
|
||||
]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
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('renameDiscussions', {
|
||||
label: 'Rename discussions',
|
||||
permission: 'discussion.rename'
|
||||
});
|
||||
|
||||
items.add('deleteDiscussions', {
|
||||
label: 'Delete discussions',
|
||||
permission: 'discussion.delete'
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
scopeItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('global', {
|
||||
label: 'Global',
|
||||
render: item => {
|
||||
if (item.setting) {
|
||||
return item.setting();
|
||||
} else if (item.permission) {
|
||||
return PermissionDropdown.component(Object.assign({}, item));
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
scopeControlItems() {
|
||||
return new ItemList();
|
||||
}
|
||||
}
|
@@ -1,41 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
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 {
|
||||
view() {
|
||||
return (
|
||||
<div className="PermissionsPage">
|
||||
<div className="PermissionsPage-groups">
|
||||
<div className="container">
|
||||
{app.store.all('groups')
|
||||
.filter(group => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
||||
.map(group => (
|
||||
<button className="Button Group" onclick={() => app.modal.show(new EditGroupModal({group}))}>
|
||||
{GroupBadge.component({
|
||||
group,
|
||||
className: 'Group-icon',
|
||||
label: null
|
||||
})}
|
||||
<span className="Group-name">{group.namePlural()}</span>
|
||||
</button>
|
||||
))}
|
||||
<button className="Button Group Group--add" onclick={() => app.modal.show(new EditGroupModal())}>
|
||||
{icon('plus', {className: 'Group-icon'})}
|
||||
<span className="Group-name">New Group</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="PermissionsPage-permissions">
|
||||
<div className="container">
|
||||
{PermissionGrid.component()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,57 +0,0 @@
|
||||
/*global FastClick*/
|
||||
|
||||
import ScrollListener from 'flarum/utils/ScrollListener';
|
||||
import Drawer from 'flarum/utils/Drawer';
|
||||
import mapRoutes from 'flarum/utils/mapRoutes';
|
||||
|
||||
import Navigation from 'flarum/components/Navigation';
|
||||
import HeaderPrimary from 'flarum/components/HeaderPrimary';
|
||||
import HeaderSecondary from 'flarum/components/HeaderSecondary';
|
||||
import AdminNav from 'flarum/components/AdminNav';
|
||||
import ModalManager from 'flarum/components/ModalManager';
|
||||
import AlertManager from 'flarum/components/AlertManager';
|
||||
|
||||
/**
|
||||
* The `boot` initializer boots up the admin app. It initializes some app
|
||||
* globals, mounts components to the page, and begins routing.
|
||||
*
|
||||
* @param {ForumApp} app
|
||||
*/
|
||||
export default function boot(app) {
|
||||
m.startComputation();
|
||||
|
||||
m.mount(document.getElementById('app-navigation'), Navigation.component({className: 'App-backControl', drawer: true}));
|
||||
m.mount(document.getElementById('header-navigation'), Navigation.component());
|
||||
m.mount(document.getElementById('header-primary'), HeaderPrimary.component());
|
||||
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
|
||||
m.mount(document.getElementById('admin-navigation'), AdminNav.component());
|
||||
|
||||
app.drawer = new Drawer();
|
||||
app.modal = m.mount(document.getElementById('modal'), ModalManager.component());
|
||||
app.alerts = m.mount(document.getElementById('alerts'), AlertManager.component());
|
||||
app.history = {
|
||||
canGoBack: () => true,
|
||||
backUrl: () => app.forum.attribute('baseUrl'),
|
||||
back: function() {
|
||||
window.location = this.backUrl();
|
||||
}
|
||||
};
|
||||
|
||||
m.route.mode = 'hash';
|
||||
m.route(document.getElementById('content'), '/', mapRoutes(app.routes));
|
||||
|
||||
m.endComputation();
|
||||
|
||||
// Add a class to the body which indicates that the page has been scrolled
|
||||
// down.
|
||||
new ScrollListener(top => {
|
||||
const $app = $('#app');
|
||||
const offset = $app.offset().top;
|
||||
|
||||
$app
|
||||
.toggleClass('affix', top >= offset)
|
||||
.toggleClass('scrolled', top > offset);
|
||||
}).start();
|
||||
|
||||
app.booted = true;
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
import DashboardPage from 'flarum/components/DashboardPage';
|
||||
import BasicsPage from 'flarum/components/BasicsPage';
|
||||
import PermissionsPage from 'flarum/components/PermissionsPage';
|
||||
import AppearancePage from 'flarum/components/AppearancePage';
|
||||
import ExtensionsPage from 'flarum/components/ExtensionsPage';
|
||||
|
||||
/**
|
||||
* The `routes` initializer defines the admin app's routes.
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
export default function(app) {
|
||||
app.routes = {
|
||||
'dashboard': {path: '/', component: DashboardPage.component()},
|
||||
'basics': {path: '/basics', component: BasicsPage.component()},
|
||||
'permissions': {path: '/permissions', component: PermissionsPage.component()},
|
||||
'appearance': {path: '/appearance', component: AppearancePage.component()},
|
||||
'extensions': {path: '/extensions', component: ExtensionsPage.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;
|
||||
});
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "flarum",
|
||||
"dependencies": {
|
||||
"jquery": "~2.1.3",
|
||||
"jquery.hotkeys": "jeresig/jquery.hotkeys#0.2.0",
|
||||
"bootstrap": "~3.3.2",
|
||||
"spin.js": "~2.0.1",
|
||||
"moment": "~2.8.4",
|
||||
"color-thief": "v2.0",
|
||||
"mithril": "lhorie/mithril.js#next",
|
||||
"es6-micro-loader": "caridy/es6-micro-loader#v0.2.1",
|
||||
"fastclick": "~1.0.6",
|
||||
"autolink": "*"
|
||||
}
|
||||
}
|
26
js/dist/admin.js
vendored
Normal file
26
js/dist/admin.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
js/dist/admin.js.map
vendored
Normal file
1
js/dist/admin.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
53
js/dist/forum.js
vendored
Normal file
53
js/dist/forum.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
js/dist/forum.js.map
vendored
Normal file
1
js/dist/forum.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,5 +1,3 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
@@ -9,8 +7,5 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
return [
|
||||
'plural' => function ($count) {
|
||||
return $count == 1 ? 'one' : 'other';
|
||||
}
|
||||
];
|
||||
export * from './src/common';
|
||||
export * from './src/forum';
|
3
js/forum/.gitignore
vendored
3
js/forum/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
node_modules
|
||||
mithril.js
|
||||
dist
|
@@ -1,37 +0,0 @@
|
||||
var gulp = require('flarum-gulp');
|
||||
|
||||
var nodeDir = 'node_modules';
|
||||
var bowerDir = '../bower_components';
|
||||
|
||||
gulp({
|
||||
files: [
|
||||
nodeDir + '/babel-core/external-helpers.js',
|
||||
|
||||
bowerDir + '/es6-micro-loader/dist/system-polyfill.js',
|
||||
|
||||
bowerDir + '/mithril/mithril.js',
|
||||
bowerDir + '/jquery/dist/jquery.js',
|
||||
bowerDir + '/jquery.hotkeys/jquery.hotkeys.js',
|
||||
bowerDir + '/color-thief/js/color-thief.js',
|
||||
bowerDir + '/moment/moment.js',
|
||||
bowerDir + '/autolink/autolink.js',
|
||||
|
||||
bowerDir + '/bootstrap/js/affix.js',
|
||||
bowerDir + '/bootstrap/js/dropdown.js',
|
||||
bowerDir + '/bootstrap/js/modal.js',
|
||||
bowerDir + '/bootstrap/js/tooltip.js',
|
||||
bowerDir + '/bootstrap/js/transition.js',
|
||||
|
||||
bowerDir + '/spin.js/spin.js',
|
||||
bowerDir + '/spin.js/jquery.spin.js',
|
||||
bowerDir + '/fastclick/lib/fastclick.js'
|
||||
],
|
||||
modules: {
|
||||
'flarum': [
|
||||
'src/**/*.js',
|
||||
'../lib/**/*.js'
|
||||
]
|
||||
},
|
||||
externalHelpers: true,
|
||||
outputFile: 'dist/app.js'
|
||||
});
|
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"gulp": "^3.8.11",
|
||||
"flarum-gulp": "git+https://github.com/flarum/gulp.git",
|
||||
"babel-core": "^5.0.0"
|
||||
}
|
||||
}
|
@@ -1,79 +0,0 @@
|
||||
import History from 'flarum/utils/History';
|
||||
import App from 'flarum/App';
|
||||
import Search from 'flarum/components/Search';
|
||||
import Composer from 'flarum/components/Composer';
|
||||
import ReplyComposer from 'flarum/components/ReplyComposer';
|
||||
import DiscussionPage from 'flarum/components/DiscussionPage';
|
||||
|
||||
export default class ForumApp extends App {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* The app's history stack, which keeps track of which routes the user visits
|
||||
* so that they can easily navigate back to the previous route.
|
||||
*
|
||||
* @type {History}
|
||||
*/
|
||||
this.history = new History();
|
||||
|
||||
/**
|
||||
* An object which controls the state of the page's side pane.
|
||||
*
|
||||
* @type {Pane}
|
||||
*/
|
||||
this.pane = null;
|
||||
|
||||
/**
|
||||
* The page's search component instance.
|
||||
*
|
||||
* @type {SearchBox}
|
||||
*/
|
||||
this.search = new Search();
|
||||
|
||||
/**
|
||||
* An object which controls the state of the page's drawer.
|
||||
*
|
||||
* @type {Drawer}
|
||||
*/
|
||||
this.drawer = null;
|
||||
|
||||
/**
|
||||
* A map of post types to their components.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
this.postComponents = {};
|
||||
|
||||
/**
|
||||
* A map of notification types to their components.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
this.notificationComponents = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not the user is currently composing a reply to a
|
||||
* discussion.
|
||||
*
|
||||
* @param {Discussion} discussion
|
||||
* @return {Boolean}
|
||||
*/
|
||||
composingReplyTo(discussion) {
|
||||
return this.composer.component instanceof ReplyComposer &&
|
||||
this.composer.component.props.discussion === discussion &&
|
||||
this.composer.position !== Composer.PositionEnum.HIDDEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not the user is currently viewing a discussion.
|
||||
*
|
||||
* @param {Discussion} discussion
|
||||
* @return {Boolean}
|
||||
*/
|
||||
viewingDiscussion(discussion) {
|
||||
return this.current instanceof DiscussionPage &&
|
||||
this.current.discussion === discussion;
|
||||
}
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
import ForumApp from 'flarum/ForumApp';
|
||||
import store from 'flarum/initializers/store';
|
||||
import preload from 'flarum/initializers/preload';
|
||||
import routes from 'flarum/initializers/routes';
|
||||
import components from 'flarum/initializers/components';
|
||||
import humanTime from 'flarum/initializers/humanTime';
|
||||
import boot from 'flarum/initializers/boot';
|
||||
|
||||
const app = new ForumApp();
|
||||
|
||||
app.initializers.add('store', store);
|
||||
app.initializers.add('routes', routes);
|
||||
app.initializers.add('components', components);
|
||||
app.initializers.add('humanTime', humanTime);
|
||||
|
||||
app.initializers.add('preload', preload, -100);
|
||||
app.initializers.add('boot', boot, -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() {
|
||||
}
|
||||
}
|
@@ -1,169 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import avatar from 'flarum/helpers/avatar';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import Button from 'flarum/components/Button';
|
||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
|
||||
/**
|
||||
* The `AvatarEditor` component displays a user's avatar along with a dropdown
|
||||
* menu which allows the user to upload/remove the avatar.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `className`
|
||||
* - `user`
|
||||
*/
|
||||
export default class AvatarEditor extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* Whether or not an avatar upload is in progress.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.className = props.className || '';
|
||||
}
|
||||
|
||||
view() {
|
||||
const user = this.props.user;
|
||||
|
||||
return (
|
||||
<div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '')}>
|
||||
{avatar(user)}
|
||||
<a className="Dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
onclick={this.quickUpload.bind(this)}>
|
||||
{this.loading ? LoadingIndicator.component() : icon('pencil')}
|
||||
</a>
|
||||
<ul className="Dropdown-menu Menu">
|
||||
{listItems(this.controlItems().toArray())}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the items in the edit avatar dropdown menu.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
controlItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('upload',
|
||||
Button.component({
|
||||
icon: 'upload',
|
||||
children: app.trans('core.upload'),
|
||||
onclick: this.upload.bind(this)
|
||||
})
|
||||
);
|
||||
|
||||
items.add('remove',
|
||||
Button.component({
|
||||
icon: 'times',
|
||||
children: app.trans('core.remove'),
|
||||
onclick: this.remove.bind(this)
|
||||
})
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user doesn't have an avatar, there's no point in showing the
|
||||
* controls dropdown, because only one option would be viable: uploading.
|
||||
* Thus, when the avatar editor's dropdown toggle button is clicked, we prompt
|
||||
* the user to upload an avatar immediately.
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
quickUpload(e) {
|
||||
if (!this.props.user.avatarUrl()) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.upload();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user to upload a new avatar.
|
||||
*/
|
||||
upload() {
|
||||
if (this.loading) return;
|
||||
|
||||
// Create a hidden HTML input element and click on it so the user can select
|
||||
// an avatar file. Once they have, we will upload it via the API.
|
||||
const user = this.props.user;
|
||||
const $input = $('<input type="file">');
|
||||
|
||||
$input.appendTo('body').hide().click().on('change', e => {
|
||||
const data = new FormData();
|
||||
data.append('avatar', $(e.target)[0].files[0]);
|
||||
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
app.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar',
|
||||
serialize: raw => raw,
|
||||
data
|
||||
}).then(
|
||||
this.success.bind(this),
|
||||
this.failure.bind(this)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the user's avatar.
|
||||
*/
|
||||
remove() {
|
||||
const user = this.props.user;
|
||||
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
app.request({
|
||||
method: 'DELETE',
|
||||
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar'
|
||||
}).then(
|
||||
this.success.bind(this),
|
||||
this.failure.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* After a successful upload/removal, push the updated user data into the
|
||||
* store, and force a recomputation of the user's avatar color.
|
||||
*
|
||||
* @param {Object} response
|
||||
* @protected
|
||||
*/
|
||||
success(response) {
|
||||
app.store.pushPayload(response);
|
||||
delete this.props.user.avatarColor;
|
||||
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* If avatar upload/removal fails, stop loading.
|
||||
*
|
||||
* @param {Object} response
|
||||
* @protected
|
||||
*/
|
||||
failure() {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
@@ -1,99 +0,0 @@
|
||||
import Modal from 'flarum/components/Modal';
|
||||
import Button from 'flarum/components/Button';
|
||||
|
||||
/**
|
||||
* The `ChangeEmailModal` component shows a modal dialog which allows the user
|
||||
* to change their email address.
|
||||
*/
|
||||
export default class ChangeEmailModal extends Modal {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* Whether or not the email has been changed successfully.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.success = false;
|
||||
|
||||
/**
|
||||
* The value of the email input.
|
||||
*
|
||||
* @type {function}
|
||||
*/
|
||||
this.email = m.prop(app.session.user.email());
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'ChangeEmailModal Modal--small';
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.trans('core.change_email');
|
||||
}
|
||||
|
||||
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>
|
||||
<div className="Form-group">
|
||||
<a href={'http://' + emailProviderName} className="Button Button--primary Button--block">
|
||||
{app.trans('core.go_to', {location: emailProviderName})}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<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)}
|
||||
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.save_changes')
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// If the user hasn't actually entered a different email address, we don't
|
||||
// need to do anything. Woot!
|
||||
if (this.email() === app.session.user.email()) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
app.session.user.save({email: this.email()}).then(
|
||||
() => {
|
||||
this.loading = false;
|
||||
this.success = true;
|
||||
m.redraw();
|
||||
},
|
||||
() => {
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,189 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import avatar from 'flarum/helpers/avatar';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
import highlight from 'flarum/helpers/highlight';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import humanTime from 'flarum/utils/humanTime';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import abbreviateNumber from 'flarum/utils/abbreviateNumber';
|
||||
import Dropdown from 'flarum/components/Dropdown';
|
||||
import TerminalPost from 'flarum/components/TerminalPost';
|
||||
import PostPreview from 'flarum/components/PostPreview';
|
||||
import SubtreeRetainer from 'flarum/utils/SubtreeRetainer';
|
||||
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
||||
import slidable from 'flarum/utils/slidable';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
|
||||
/**
|
||||
* The `DiscussionListItem` component shows a single discussion in the
|
||||
* discussion list.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `discussion`
|
||||
* - `params`
|
||||
*/
|
||||
export default class DiscussionListItem extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* Set up a subtree retainer so that the discussion will not be redrawn
|
||||
* unless new data comes in.
|
||||
*
|
||||
* @type {SubtreeRetainer}
|
||||
*/
|
||||
this.subtree = new SubtreeRetainer(
|
||||
() => this.props.discussion.freshness,
|
||||
() => {
|
||||
const time = app.session.user && app.session.user.readTime();
|
||||
return time && time.getTime();
|
||||
},
|
||||
() => this.active()
|
||||
);
|
||||
}
|
||||
|
||||
view() {
|
||||
const retain = this.subtree.retain();
|
||||
|
||||
if (retain) return retain;
|
||||
|
||||
const discussion = this.props.discussion;
|
||||
const startUser = discussion.startUser();
|
||||
const isUnread = discussion.isUnread();
|
||||
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();
|
||||
|
||||
return (
|
||||
<div className={'DiscussionListItem ' + (this.active() ? 'active' : '')}>
|
||||
|
||||
{controls.length ? Dropdown.component({
|
||||
icon: 'ellipsis-v',
|
||||
children: controls,
|
||||
className: 'DiscussionListItem-controls',
|
||||
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right'
|
||||
}) : ''}
|
||||
|
||||
<a className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')}
|
||||
onclick={this.markAsRead.bind(this)}>
|
||||
{icon('check')}
|
||||
</a>
|
||||
|
||||
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '')}>
|
||||
<a href={startUser ? app.route.user(startUser) : '#'}
|
||||
className="DiscussionListItem-author"
|
||||
title={extractText(app.trans('core.discussion_started', {user: startUser, ago: humanTime(discussion.startTime())}))}
|
||||
config={function(element) {
|
||||
$(element).tooltip({placement: 'right'});
|
||||
m.route.apply(this, arguments);
|
||||
}}>
|
||||
{avatar(startUser, {title: ''})}
|
||||
</a>
|
||||
|
||||
<ul className="DiscussionListItem-badges badges">
|
||||
{listItems(discussion.badges().toArray())}
|
||||
</ul>
|
||||
|
||||
<a href={app.route.discussion(discussion, jumpTo)}
|
||||
config={m.route}
|
||||
className="DiscussionListItem-main">
|
||||
<h3 className="DiscussionListItem-title">{highlight(discussion.title(), this.props.params.q)}</h3>
|
||||
<ul className="DiscussionListItem-info">{listItems(this.infoItems().toArray())}</ul>
|
||||
</a>
|
||||
|
||||
<span className="DiscussionListItem-count"
|
||||
onclick={this.markAsRead.bind(this)}
|
||||
title={showUnread ? app.trans('core.mark_as_read') : ''}>
|
||||
{abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'repliesCount']())}
|
||||
</span>
|
||||
|
||||
{relevantPosts && relevantPosts.length
|
||||
? <div className="DiscussionListItem-relevantPosts">
|
||||
{relevantPosts.map(post => PostPreview.component({post, highlight: this.props.params.q}))}
|
||||
</div>
|
||||
: ''}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized) {
|
||||
if (isInitialized) return;
|
||||
|
||||
// If we're on a touch device, set up the discussion row to be slidable.
|
||||
// This allows the user to drag the row to either side of the screen to
|
||||
// reveal controls.
|
||||
if ('ontouchstart' in window) {
|
||||
const slidableInstance = slidable(this.$().addClass('Slidable'));
|
||||
|
||||
this.$('.DiscussionListItem-controls')
|
||||
.on('hidden.bs.dropdown', () => slidableInstance.reset());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not the discussion is currently being viewed.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
active() {
|
||||
const idParam = m.route.param('id');
|
||||
|
||||
return idParam && idParam.split('-')[0] === this.props.discussion.id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not information about who started the discussion
|
||||
* should be displayed instead of information about the most recent reply to
|
||||
* the discussion.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
showStartPost() {
|
||||
return ['newest', 'oldest'].indexOf(this.props.params.sort) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not the number of replies should be shown instead of
|
||||
* the number of unread posts.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
showRepliesCount() {
|
||||
return this.props.params.sort === 'replies';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the discussion as read.
|
||||
*/
|
||||
markAsRead() {
|
||||
const discussion = this.props.discussion;
|
||||
|
||||
if (discussion.isUnread()) {
|
||||
discussion.save({readNumber: discussion.lastPostNumber()});
|
||||
m.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list of info for a discussion listing. By default this is
|
||||
* just the first/last post indicator.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
infoItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('terminalPost',
|
||||
TerminalPost.component({
|
||||
discussion: this.props.discussion,
|
||||
lastPost: !this.showStartPost()
|
||||
})
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
@@ -1,54 +0,0 @@
|
||||
import highlight from 'flarum/helpers/highlight';
|
||||
import LinkButton from 'flarum/components/LinkButton';
|
||||
|
||||
/**
|
||||
* The `DiscussionsSearchSource` finds and displays discussion search results in
|
||||
* the search dropdown.
|
||||
*
|
||||
* @implements SearchSource
|
||||
*/
|
||||
export default class DiscussionsSearchSource {
|
||||
constructor() {
|
||||
this.results = {};
|
||||
}
|
||||
|
||||
search(query) {
|
||||
this.results[query] = [];
|
||||
|
||||
const params = {
|
||||
filter: {q: query},
|
||||
page: {limit: 3},
|
||||
include: 'relevantPosts,relevantPosts.discussion,relevantPosts.user'
|
||||
};
|
||||
|
||||
return app.store.find('discussions', params).then(results => this.results[query] = results);
|
||||
}
|
||||
|
||||
view(query) {
|
||||
const results = this.results[query] || [];
|
||||
|
||||
return [
|
||||
<li className="Dropdown-header">{app.trans('core.discussions')}</li>,
|
||||
<li>
|
||||
{LinkButton.component({
|
||||
icon: 'search',
|
||||
children: app.trans('core.search_all_discussions', {query}),
|
||||
href: app.route('index', {q: query})
|
||||
})}
|
||||
</li>,
|
||||
results.map(discussion => {
|
||||
const relevantPosts = discussion.relevantPosts();
|
||||
const post = relevantPosts && relevantPosts[0];
|
||||
|
||||
return (
|
||||
<li className="DiscussionSearchResult" data-index={'discussions' + discussion.id()}>
|
||||
<a href={app.route.discussion(discussion, post && post.number())} config={m.route}>
|
||||
<div className="DiscussionSearchResult-title">{highlight(discussion.title(), query)}</div>
|
||||
{post ? <div className="DiscussionSearchResult-excerpt">{highlight(post.contentPlain(), query, 100)}</div> : ''}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,74 +0,0 @@
|
||||
import ComposerBody from 'flarum/components/ComposerBody';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* edited, and adds a header control to indicate which post is being edited.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - All of the props for ComposerBody
|
||||
* - `post`
|
||||
*/
|
||||
export default class EditPostComposer extends ComposerBody {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.editor.props.preview = () => {
|
||||
m.route(app.route.post(this.props.post));
|
||||
};
|
||||
}
|
||||
|
||||
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.originalContent = props.originalContent || props.post.content();
|
||||
props.user = props.user || props.post.user();
|
||||
|
||||
props.post.editedContent = props.originalContent;
|
||||
}
|
||||
|
||||
headerItems() {
|
||||
const items = super.headerItems();
|
||||
const post = this.props.post;
|
||||
|
||||
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()})}
|
||||
</a>
|
||||
</h3>
|
||||
));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to submit to the server when the post is saved.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
content: this.content()
|
||||
};
|
||||
}
|
||||
|
||||
onsubmit() {
|
||||
this.loading = true;
|
||||
|
||||
const data = this.data();
|
||||
|
||||
this.props.post.save(data).then(
|
||||
() => {
|
||||
app.composer.hide();
|
||||
m.redraw();
|
||||
},
|
||||
() => this.loading = false
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,131 +0,0 @@
|
||||
import Modal from 'flarum/components/Modal';
|
||||
import Button from 'flarum/components/Button';
|
||||
import GroupBadge from 'flarum/components/GroupBadge';
|
||||
import Group from 'flarum/models/Group';
|
||||
|
||||
/**
|
||||
* The `EditUserModal` component displays a modal dialog with a login form.
|
||||
*/
|
||||
export default class EditUserModal extends Modal {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
const user = this.props.user;
|
||||
|
||||
this.username = m.prop(user.username() || '');
|
||||
this.email = m.prop(user.email() || '');
|
||||
this.setPassword = m.prop(false);
|
||||
this.password = m.prop(user.password() || '');
|
||||
this.groups = {};
|
||||
|
||||
app.store.all('groups')
|
||||
.filter(group => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
||||
.forEach(group => this.groups[group.id()] = m.prop(user.groups().indexOf(group) !== -1));
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'EditUserModal Modal--small';
|
||||
}
|
||||
|
||||
title() {
|
||||
return 'Edit User';
|
||||
}
|
||||
|
||||
content() {
|
||||
return (
|
||||
<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)} />
|
||||
</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)} />
|
||||
) : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="Form-group EditUserModal-groups">
|
||||
<label>Groups</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()])}/>
|
||||
{GroupBadge.component({group, label: ''})} {group.nameSingular()}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.trans('core.save_changes')
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
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 (this.setPassword()) {
|
||||
data.password = this.password();
|
||||
}
|
||||
|
||||
this.props.user.save(data).then(
|
||||
() => this.hide(),
|
||||
response => {
|
||||
this.loading = false;
|
||||
this.handleErrors(response);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,68 +0,0 @@
|
||||
import Post from 'flarum/components/Post';
|
||||
import { ucfirst } from 'flarum/utils/string';
|
||||
import usernameHelper from 'flarum/helpers/username';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
/**
|
||||
* The `EventPost` component displays a post which indicating a discussion
|
||||
* event, like a discussion being renamed or stickied. Subclasses must implement
|
||||
* the `icon` and `description` methods.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - All of the props for `Post`
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class EventPost extends Post {
|
||||
attrs() {
|
||||
return {
|
||||
className: 'EventPost ' + ucfirst(this.props.post.contentType()) + 'Post'
|
||||
};
|
||||
}
|
||||
|
||||
content() {
|
||||
const user = this.props.post.user();
|
||||
const username = usernameHelper(user);
|
||||
const data = Object.assign(this.descriptionData(), {
|
||||
user,
|
||||
username: user
|
||||
? <a className="EventPost-user" href={app.route.user(user)} config={m.route}>{username}</a>
|
||||
: username
|
||||
});
|
||||
|
||||
return [
|
||||
icon(this.icon(), {className: 'EventPost-icon'}),
|
||||
<div class="EventPost-info">
|
||||
{app.trans(this.descriptionKey(), data)}
|
||||
</div>
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the event icon.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
icon() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the translation key for the description of the event.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
descriptionKey() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the translation data for the description of the event.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
descriptionData() {
|
||||
return {};
|
||||
}
|
||||
}
|
@@ -1,109 +0,0 @@
|
||||
import Modal from 'flarum/components/Modal';
|
||||
import Alert from 'flarum/components/Alert';
|
||||
import Button from 'flarum/components/Button';
|
||||
|
||||
/**
|
||||
* The `ForgotPasswordModal` component displays a modal which allows the user to
|
||||
* enter their email address and request a link to reset their password.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `email`
|
||||
*/
|
||||
export default class ForgotPasswordModal extends Modal {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* The value of the email input.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.email = m.prop(this.props.email || '');
|
||||
|
||||
/**
|
||||
* Whether or not the password reset email was sent successfully.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.success = false;
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'ForgotPasswordModal Modal--small';
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.trans('core.forgot_password');
|
||||
}
|
||||
|
||||
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>
|
||||
<div className="Form-group">
|
||||
<a href={'http://' + emailProviderName} className="Button Button--primary Button--block">
|
||||
{app.trans('core.go_to', {location: emailProviderName})}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<p className="helpText">{app.trans('core.forgot_password_help')}</p>
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="email" type="email" placeholder={app.trans('core.email')}
|
||||
value={this.email()}
|
||||
onchange={m.withAttr('value', this.email)}
|
||||
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.recover_password')
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
app.request({
|
||||
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;
|
||||
this.success = true;
|
||||
this.alert = null;
|
||||
m.redraw();
|
||||
},
|
||||
response => {
|
||||
this.loading = false;
|
||||
this.handleErrors(response);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
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
|
||||
* default skin, these are shown just to the right of the forum title.
|
||||
*/
|
||||
export default class HeaderPrimary extends Component {
|
||||
view() {
|
||||
return (
|
||||
<ul className="Header-controls">
|
||||
{listItems(this.items().toArray())}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the controls.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
items() {
|
||||
return new ItemList();
|
||||
}
|
||||
}
|
@@ -1,85 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Button from 'flarum/components/Button';
|
||||
import LogInModal from 'flarum/components/LogInModal';
|
||||
import SignUpModal from 'flarum/components/SignUpModal';
|
||||
import SessionDropdown from 'flarum/components/SessionDropdown';
|
||||
import SelectDropdown from 'flarum/components/SelectDropdown';
|
||||
import NotificationsDropdown from 'flarum/components/NotificationsDropdown';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
|
||||
/**
|
||||
* The `HeaderSecondary` component displays secondary header controls, such as
|
||||
* the search box and the user menu. On the default skin, these are shown on the
|
||||
* right side of the header.
|
||||
*/
|
||||
export default class HeaderSecondary extends Component {
|
||||
view() {
|
||||
return (
|
||||
<ul className="Header-controls">
|
||||
{listItems(this.items().toArray())}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the controls.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('search', app.search.render());
|
||||
|
||||
if (Object.keys(app.locales).length > 1) {
|
||||
const locales = [];
|
||||
|
||||
for (const locale in app.locales) {
|
||||
locales.push(Button.component({
|
||||
active: app.locale === locale,
|
||||
children: app.locales[locale],
|
||||
icon: app.locale === locale ? 'check' : true,
|
||||
onclick: () => {
|
||||
if (app.session.user) {
|
||||
app.session.user.savePreferences({locale}).then(() => window.location.reload());
|
||||
} else {
|
||||
document.cookie = `locale=${locale}; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT`;
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
items.add('locale', SelectDropdown.component({
|
||||
children: locales,
|
||||
buttonClassName: 'Button Button--link'
|
||||
}));
|
||||
}
|
||||
|
||||
if (app.session.user) {
|
||||
items.add('notifications', NotificationsDropdown.component());
|
||||
items.add('session', SessionDropdown.component());
|
||||
} else {
|
||||
if (app.forum.attribute('allowSignUp')) {
|
||||
items.add('signUp',
|
||||
Button.component({
|
||||
children: app.trans('core.sign_up'),
|
||||
className: 'Button Button--link',
|
||||
onclick: () => app.modal.show(new SignUpModal())
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
items.add('logIn',
|
||||
Button.component({
|
||||
children: app.trans('core.log_in'),
|
||||
className: 'Button Button--link',
|
||||
onclick: () => app.modal.show(new LogInModal())
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
@@ -1,140 +0,0 @@
|
||||
import Modal from 'flarum/components/Modal';
|
||||
import ForgotPasswordModal from 'flarum/components/ForgotPasswordModal';
|
||||
import SignUpModal from 'flarum/components/SignUpModal';
|
||||
import Alert from 'flarum/components/Alert';
|
||||
import Button from 'flarum/components/Button';
|
||||
|
||||
/**
|
||||
* The `LogInModal` component displays a modal dialog with a login form.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `email`
|
||||
* - `password`
|
||||
*/
|
||||
export default class LogInModal extends Modal {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* The value of the email input.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.email = m.prop(this.props.email || '');
|
||||
|
||||
/**
|
||||
* The value of the password input.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.password = m.prop(this.props.password || '');
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'LogInModal Modal--small';
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.trans('core.log_in');
|
||||
}
|
||||
|
||||
content() {
|
||||
return [
|
||||
<div className="Modal-body">
|
||||
<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)}
|
||||
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)}
|
||||
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.log_in')
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
<div className="Modal-footer">
|
||||
<p className="LogInModal-forgotPassword">
|
||||
<a onclick={this.forgotPassword.bind(this)}>{app.trans('core.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>
|
||||
</p>
|
||||
) : ''}
|
||||
</div>
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the forgot password modal, prefilling it with an email if the user has
|
||||
* entered one.
|
||||
*/
|
||||
forgotPassword() {
|
||||
const email = this.email();
|
||||
const props = email.indexOf('@') !== -1 ? {email} : undefined;
|
||||
|
||||
app.modal.show(new ForgotPasswordModal(props));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the sign up modal, prefilling it with an email/username/password if
|
||||
* the user has entered one.
|
||||
*/
|
||||
signUp() {
|
||||
const props = {password: this.password()};
|
||||
const email = this.email();
|
||||
props[email.indexOf('@') !== -1 ? 'email' : 'username'] = email;
|
||||
|
||||
app.modal.show(new SignUpModal(props));
|
||||
}
|
||||
|
||||
onready() {
|
||||
this.$('[name=' + (this.email() ? 'password' : 'email') + ']').select();
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const email = this.email();
|
||||
const password = this.password();
|
||||
|
||||
app.session.login(email, password).then(
|
||||
null,
|
||||
response => {
|
||||
this.loading = false;
|
||||
|
||||
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')
|
||||
});
|
||||
}
|
||||
|
||||
m.redraw();
|
||||
this.onready();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,79 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import avatar from 'flarum/helpers/avatar';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import humanTime from 'flarum/helpers/humanTime';
|
||||
|
||||
/**
|
||||
* The `Notification` component abstract displays a single notification.
|
||||
* Subclasses should implement the `icon`, `href`, and `content` methods.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `notification`
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class Notification extends Component {
|
||||
view() {
|
||||
const notification = this.props.notification;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the icon that should be displayed in the notification.
|
||||
*
|
||||
* @return {String}
|
||||
* @abstract
|
||||
*/
|
||||
icon() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL that the notification should link to.
|
||||
*
|
||||
* @return {String}
|
||||
* @abstract
|
||||
*/
|
||||
href() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of the notification.
|
||||
*
|
||||
* @return {VirtualElement}
|
||||
* @abstract
|
||||
*/
|
||||
content() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the excerpt of the notification.
|
||||
*
|
||||
* @return {VirtualElement}
|
||||
* @abstract
|
||||
*/
|
||||
excerpt() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the notification as read.
|
||||
*/
|
||||
markAsRead() {
|
||||
this.props.notification.save({isRead: true});
|
||||
}
|
||||
}
|
@@ -1,141 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
import Button from 'flarum/components/Button';
|
||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
import Discussion from 'flarum/models/Discussion';
|
||||
|
||||
/**
|
||||
* The `NotificationList` component displays a list of the logged-in user's
|
||||
* notifications, grouped by discussion.
|
||||
*/
|
||||
export default class NotificationList extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* Whether or not the notifications are loading.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
view() {
|
||||
const groups = [];
|
||||
|
||||
if (app.cache.notifications) {
|
||||
const discussions = {};
|
||||
|
||||
// Build an array of discussions which the notifications are related to,
|
||||
// and add the notifications as children.
|
||||
app.cache.notifications.forEach(notification => {
|
||||
const subject = notification.subject();
|
||||
|
||||
if (typeof subject === 'undefined') return;
|
||||
|
||||
// Get the discussion that this notification is related to. If it's not
|
||||
// directly related to a discussion, it may be related to a post or
|
||||
// other entity which is related to a discussion.
|
||||
let discussion = false;
|
||||
if (subject instanceof Discussion) discussion = subject;
|
||||
else if (subject && subject.discussion) discussion = subject.discussion();
|
||||
|
||||
// If the notification is not related to a discussion directly or
|
||||
// indirectly, then we will assign it to a neutral group.
|
||||
const key = discussion ? discussion.id() : 0;
|
||||
discussions[key] = discussions[key] || {discussion: discussion, notifications: []};
|
||||
discussions[key].notifications.push(notification);
|
||||
|
||||
if (groups.indexOf(discussions[key]) === -1) {
|
||||
groups.push(discussions[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="NotificationList">
|
||||
<div className="NotificationList-header">
|
||||
<div className="App-primaryControl">
|
||||
{Button.component({
|
||||
className: 'Button Button--icon Button--link',
|
||||
icon: 'check',
|
||||
title: app.trans('core.mark_all_as_read'),
|
||||
onclick: this.markAllAsRead.bind(this)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<h4 className="App-titleControl App-titleControl--text">{app.trans('core.notifications')}</h4>
|
||||
</div>
|
||||
|
||||
<div className="NotificationList-content">
|
||||
{groups.length
|
||||
? groups.map(group => {
|
||||
const badges = group.discussion && group.discussion.badges().toArray();
|
||||
|
||||
return (
|
||||
<div className="NotificationGroup">
|
||||
{group.discussion
|
||||
? (
|
||||
<a className="NotificationGroup-header"
|
||||
href={app.route.discussion(group.discussion)}
|
||||
config={m.route}>
|
||||
{badges && badges.length ? <ul className="NotificationGroup-badges badges">{listItems(badges)}</ul> : ''}
|
||||
{group.discussion.title()}
|
||||
</a>
|
||||
) : (
|
||||
<div className="NotificationGroup-header">
|
||||
{app.forum.attribute('title')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ul className="NotificationGroup-content">
|
||||
{group.notifications.map(notification => {
|
||||
const NotificationComponent = app.notificationComponents[notification.contentType()];
|
||||
return NotificationComponent ? <li>{NotificationComponent.component({notification})}</li> : '';
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: !this.loading
|
||||
? <div className="NotificationList-empty">{app.trans('core.no_notifications')}</div>
|
||||
: LoadingIndicator.component({className: 'LoadingIndicator--block'})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load notifications into the application's cache if they haven't already
|
||||
* been loaded.
|
||||
*/
|
||||
load() {
|
||||
if (app.cache.notifications && !app.session.user.unreadNotificationsCount()) {
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all of the notifications as read.
|
||||
*/
|
||||
markAllAsRead() {
|
||||
if (!app.cache.notifications) return;
|
||||
|
||||
app.cache.notifications.forEach(notification => {
|
||||
if (!notification.isRead()) {
|
||||
notification.save({isRead: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,53 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import NotificationList from 'flarum/components/NotificationList';
|
||||
|
||||
export default class NotificationsDropdown extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* Whether or not the notifications dropdown is visible.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.showing = false;
|
||||
|
||||
this.list = new NotificationList();
|
||||
}
|
||||
|
||||
view() {
|
||||
const user = app.session.user;
|
||||
const unread = user.unreadNotificationsCount();
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
onclick() {
|
||||
if (app.drawer.isOpen()) {
|
||||
m.route(app.route('notifications'));
|
||||
} else {
|
||||
this.showing = true;
|
||||
this.list.load();
|
||||
}
|
||||
}
|
||||
|
||||
menuClick(e) {
|
||||
// Don't close the notifications dropdown if the user is opening a link in a
|
||||
// new tab or window.
|
||||
if (e.shiftKey || e.metaKey || e.ctrlKey || e.which === 2) e.stopPropagation();
|
||||
}
|
||||
}
|
@@ -1,81 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import SubtreeRetainer from 'flarum/utils/SubtreeRetainer';
|
||||
import Dropdown from 'flarum/components/Dropdown';
|
||||
import PostControls from 'flarum/utils/PostControls';
|
||||
|
||||
/**
|
||||
* The `Post` component displays a single post. The basic post template just
|
||||
* includes a controls dropdown; subclasses must implement `content` and `attrs`
|
||||
* methods.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `post`
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class Post extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* Set up a subtree retainer so that the post will not be redrawn
|
||||
* unless new data comes in.
|
||||
*
|
||||
* @type {SubtreeRetainer}
|
||||
*/
|
||||
this.subtree = new SubtreeRetainer(
|
||||
() => this.props.post.freshness,
|
||||
() => {
|
||||
const user = this.props.post.user();
|
||||
return user && user.freshness;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
view() {
|
||||
const attrs = this.attrs();
|
||||
|
||||
attrs.className = 'Post ' + (attrs.className || '');
|
||||
|
||||
return (
|
||||
<article {...attrs}>
|
||||
{this.subtree.retain() || (() => {
|
||||
const controls = PostControls.controls(this.props.post, this).toArray();
|
||||
|
||||
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()}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attributes for the post element.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
attrs() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the post's content.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
content() {
|
||||
return '';
|
||||
}
|
||||
}
|
@@ -1,30 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import humanTime from 'flarum/utils/humanTime';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
|
||||
/**
|
||||
* The `PostEdited` component displays information about when and by whom a post
|
||||
* was edited.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `post`
|
||||
*/
|
||||
export default class PostEdited extends Component {
|
||||
view() {
|
||||
const post = this.props.post;
|
||||
const editUser = post.editUser();
|
||||
const title = extractText(app.trans('core.post_edited', {user: editUser, ago: humanTime(post.editTime())}));
|
||||
|
||||
return (
|
||||
<span className="PostEdited" title={title}>{icon('pencil')}</span>
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized) {
|
||||
if (isInitialized) return;
|
||||
|
||||
this.$().tooltip();
|
||||
}
|
||||
}
|
@@ -1,45 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import humanTime from 'flarum/helpers/humanTime';
|
||||
import fullTime from 'flarum/helpers/fullTime';
|
||||
|
||||
/**
|
||||
* The `PostMeta` component displays the time of a post, and when clicked, shows
|
||||
* a dropdown containing more information about the post (number, full time,
|
||||
* permalink).
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `post`
|
||||
*/
|
||||
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 touch = 'ontouchstart' in document.documentElement;
|
||||
|
||||
// When the dropdown menu is shown, select the contents of the permalink
|
||||
// input so that the user can quickly copy the URL.
|
||||
const selectPermalink = function() {
|
||||
setTimeout(() => $(this).parent().find('.PostMeta-permalink').select());
|
||||
|
||||
m.redraw.strategy('none');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="Dropdown PostMeta">
|
||||
<a className="Dropdown-toggle" onclick={selectPermalink} data-toggle="dropdown">
|
||||
{humanTime(time)}
|
||||
</a>
|
||||
|
||||
<div className="Dropdown-menu dropdown-menu">
|
||||
<span className="PostMeta-number">{app.trans('core.post_number', {number: post.number()})}</span>{' '}
|
||||
{fullTime(time)}
|
||||
{touch
|
||||
? <a className="Button PostMeta-permalink" href={permalink}>{permalink}</a>
|
||||
: <input className="FormControl PostMeta-permalink" value={permalink} onclick={e => e.stopPropagation()} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,102 +0,0 @@
|
||||
import ComposerBody from 'flarum/components/ComposerBody';
|
||||
import Alert from 'flarum/components/Alert';
|
||||
import Button from 'flarum/components/Button';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
/**
|
||||
* The `ReplyComposer` component displays the composer content for replying to a
|
||||
* discussion.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - All of the props of ComposerBody
|
||||
* - `discussion`
|
||||
*/
|
||||
export default class ReplyComposer extends ComposerBody {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.editor.props.preview = () => {
|
||||
m.route(app.route.discussion(this.props.discussion, 'reply'));
|
||||
};
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
headerItems() {
|
||||
const items = super.headerItems();
|
||||
const discussion = this.props.discussion;
|
||||
|
||||
items.add('title', (
|
||||
<h3>
|
||||
{icon('reply')}{' '}<a href={app.route.discussion(discussion)} config={m.route}>{discussion.title()}</a>
|
||||
</h3>
|
||||
));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to submit to the server when the reply is saved.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
content: this.content(),
|
||||
relationships: {discussion: this.props.discussion}
|
||||
};
|
||||
}
|
||||
|
||||
onsubmit() {
|
||||
const discussion = this.props.discussion;
|
||||
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
const data = this.data();
|
||||
|
||||
app.store.createRecord('posts').save(data).then(
|
||||
post => {
|
||||
// If we're currently viewing the discussion which this reply was made
|
||||
// in, then we can update the post stream.
|
||||
if (app.viewingDiscussion(discussion)) {
|
||||
app.current.stream.update();
|
||||
} else {
|
||||
// Otherwise, we'll create an alert message to inform the user that
|
||||
// their reply has been posted, containing a button which will
|
||||
// transition to their new post when clicked.
|
||||
let alert;
|
||||
const viewButton = Button.component({
|
||||
className: 'Button Button--link',
|
||||
children: app.trans('core.view'),
|
||||
onclick: () => {
|
||||
m.route(app.route.post(post));
|
||||
app.alerts.dismiss(alert);
|
||||
}
|
||||
});
|
||||
app.alerts.show(
|
||||
alert = new Alert({
|
||||
type: 'success',
|
||||
message: app.trans('core.reply_posted'),
|
||||
controls: [viewButton]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
app.composer.hide();
|
||||
},
|
||||
response => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
app.alertErrors(response.errors);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,91 +0,0 @@
|
||||
import avatar from 'flarum/helpers/avatar';
|
||||
import username from 'flarum/helpers/username';
|
||||
import Dropdown from 'flarum/components/Dropdown';
|
||||
import LinkButton from 'flarum/components/LinkButton';
|
||||
import Button from 'flarum/components/Button';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import Separator from 'flarum/components/Separator';
|
||||
import Group from 'flarum/models/Group';
|
||||
|
||||
/**
|
||||
* The `SessionDropdown` component shows a button with the current user's
|
||||
* avatar/name, with a dropdown of session controls.
|
||||
*/
|
||||
export default class SessionDropdown extends Dropdown {
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.className = 'SessionDropdown';
|
||||
props.buttonClassName = 'Button Button--user Button--flat';
|
||||
props.menuClassName = 'Dropdown-menu--right';
|
||||
}
|
||||
|
||||
view() {
|
||||
this.props.children = this.items().toArray();
|
||||
|
||||
return super.view();
|
||||
}
|
||||
|
||||
getButtonContent() {
|
||||
const user = app.session.user;
|
||||
|
||||
return [
|
||||
avatar(user), ' ',
|
||||
<span className="Button-label">{username(user)}</span>
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the contents of the dropdown menu.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
const user = app.session.user;
|
||||
|
||||
items.add('profile',
|
||||
LinkButton.component({
|
||||
icon: 'user',
|
||||
children: app.trans('core.profile'),
|
||||
href: app.route.user(user)
|
||||
}),
|
||||
100
|
||||
);
|
||||
|
||||
items.add('settings',
|
||||
LinkButton.component({
|
||||
icon: 'cog',
|
||||
children: app.trans('core.settings'),
|
||||
href: app.route('settings')
|
||||
}),
|
||||
50
|
||||
);
|
||||
|
||||
if (user.groups().some(group => group.id() === Group.ADMINISTRATOR_ID)) {
|
||||
items.add('administration',
|
||||
LinkButton.component({
|
||||
icon: 'wrench',
|
||||
children: app.trans('core.administration'),
|
||||
href: app.forum.attribute('baseUrl') + '/admin',
|
||||
target: '_blank',
|
||||
config: () => {}
|
||||
}),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
items.add('separator', Separator.component(), -90);
|
||||
|
||||
items.add('logOut',
|
||||
Button.component({
|
||||
icon: 'sign-out',
|
||||
children: app.trans('core.log_out'),
|
||||
onclick: app.session.logout.bind(app.session)
|
||||
}),
|
||||
-100
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
@@ -1,132 +0,0 @@
|
||||
import UserPage from 'flarum/components/UserPage';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import Switch from 'flarum/components/Switch';
|
||||
import Button from 'flarum/components/Button';
|
||||
import FieldSet from 'flarum/components/FieldSet';
|
||||
import NotificationGrid from 'flarum/components/NotificationGrid';
|
||||
import ChangePasswordModal from 'flarum/components/ChangePasswordModal';
|
||||
import ChangeEmailModal from 'flarum/components/ChangeEmailModal';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
|
||||
/**
|
||||
* The `SettingsPage` component displays the user's settings control panel, in
|
||||
* the context of their user profile.
|
||||
*/
|
||||
export default class SettingsPage extends UserPage {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.init(app.session.user);
|
||||
app.setTitle(app.trans('core.settings'));
|
||||
}
|
||||
|
||||
content() {
|
||||
return (
|
||||
<div className="SettingsPage">
|
||||
<ul>{listItems(this.settingsItems().toArray())}</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the user's settings controls.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
settingsItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('account',
|
||||
FieldSet.component({
|
||||
label: app.trans('core.account'),
|
||||
className: 'Settings-account',
|
||||
children: this.accountItems().toArray()
|
||||
})
|
||||
);
|
||||
|
||||
items.add('notifications',
|
||||
FieldSet.component({
|
||||
label: app.trans('core.notifications'),
|
||||
className: 'Settings-notifications',
|
||||
children: [NotificationGrid.component({user: this.user})]
|
||||
})
|
||||
);
|
||||
|
||||
items.add('privacy',
|
||||
FieldSet.component({
|
||||
label: app.trans('core.privacy'),
|
||||
className: 'Settings-privacy',
|
||||
children: this.privacyItems().toArray()
|
||||
})
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the user's account settings.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
accountItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('changePassword',
|
||||
Button.component({
|
||||
children: app.trans('core.change_password'),
|
||||
className: 'Button',
|
||||
onclick: () => app.modal.show(new ChangePasswordModal())
|
||||
})
|
||||
);
|
||||
|
||||
items.add('changeEmail',
|
||||
Button.component({
|
||||
children: app.trans('core.change_email'),
|
||||
className: 'Button',
|
||||
onclick: () => app.modal.show(new ChangeEmailModal())
|
||||
})
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a callback that will save a value to the given preference.
|
||||
*
|
||||
* @param {String} key
|
||||
* @return {Function}
|
||||
*/
|
||||
preferenceSaver(key) {
|
||||
return (value, component) => {
|
||||
if (component) component.loading = true;
|
||||
m.redraw();
|
||||
|
||||
this.user.savePreferences({[key]: value}).then(() => {
|
||||
if (component) component.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the user's privacy settings.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
privacyItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('discloseOnline',
|
||||
Switch.component({
|
||||
children: app.trans('core.disclose_online'),
|
||||
state: this.user.preferences().discloseOnline,
|
||||
onchange: (value, component) => {
|
||||
this.user.pushAttributes({lastSeenTime: null});
|
||||
this.preferenceSaver('discloseOnline')(value, component);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
@@ -1,194 +0,0 @@
|
||||
import Modal from 'flarum/components/Modal';
|
||||
import LogInModal from 'flarum/components/LogInModal';
|
||||
import avatar from 'flarum/helpers/avatar';
|
||||
import Button from 'flarum/components/Button';
|
||||
|
||||
/**
|
||||
* The `SignUpModal` component displays a modal dialog with a singup form.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `username`
|
||||
* - `email`
|
||||
* - `password`
|
||||
*/
|
||||
export default class SignUpModal extends Modal {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* The value of the username input.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.username = m.prop(this.props.username || '');
|
||||
|
||||
/**
|
||||
* The value of the email input.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
this.email = m.prop(this.props.email || '');
|
||||
|
||||
/**
|
||||
* The value of the password input.
|
||||
*
|
||||
* @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() {
|
||||
return 'Modal--small SignUpModal' + (this.welcomeUser ? ' SignUpModal--success' : '');
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.trans('core.sign_up');
|
||||
}
|
||||
|
||||
content() {
|
||||
return [
|
||||
<div className="Modal-body">
|
||||
{this.body()}
|
||||
</div>,
|
||||
<div className="Modal-footer">
|
||||
{this.footer()}
|
||||
</div>
|
||||
];
|
||||
}
|
||||
|
||||
body() {
|
||||
const body = [(
|
||||
<div className="Form Form--centered">
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="username" placeholder={app.trans('core.username')}
|
||||
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')}
|
||||
value={this.email()}
|
||||
onchange={m.withAttr('value', this.email)}
|
||||
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)}
|
||||
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')
|
||||
})}
|
||||
</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>
|
||||
</p>
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the log in modal, prefilling it with an email/username/password if
|
||||
* the user has entered one.
|
||||
*/
|
||||
logIn() {
|
||||
const props = {
|
||||
email: this.email() || this.username(),
|
||||
password: this.password()
|
||||
};
|
||||
|
||||
app.modal.show(new LogInModal(props));
|
||||
}
|
||||
|
||||
onready() {
|
||||
if (this.props.username) {
|
||||
this.$('[name=email]').select();
|
||||
} else {
|
||||
super.onready();
|
||||
}
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const data = {
|
||||
username: this.username(),
|
||||
email: this.email(),
|
||||
password: this.password()
|
||||
};
|
||||
|
||||
app.store.createRecord('users').save(data).then(
|
||||
user => {
|
||||
this.welcomeUser = user;
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
},
|
||||
response => {
|
||||
this.loading = false;
|
||||
this.handleErrors(response.errors);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import humanTime from 'flarum/helpers/humanTime';
|
||||
|
||||
/**
|
||||
* Displays information about a the first or last post in a discussion.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `discussion`
|
||||
* - `lastPost`
|
||||
*/
|
||||
export default class TerminalPost extends Component {
|
||||
view() {
|
||||
const discussion = this.props.discussion;
|
||||
const lastPost = this.props.lastPost && discussion.repliesCount();
|
||||
|
||||
const user = discussion[lastPost ? 'lastUser' : 'startUser']();
|
||||
const time = discussion[lastPost ? 'lastTime' : 'startTime']();
|
||||
|
||||
return (
|
||||
<span>
|
||||
{app.trans('core.discussion_' + (lastPost ? 'replied' : 'started'), {
|
||||
user,
|
||||
ago: humanTime(time)
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,104 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
import classList from 'flarum/utils/classList';
|
||||
|
||||
/**
|
||||
* The `UserBio` component displays a user's bio, optionally letting the user
|
||||
* edit it.
|
||||
*/
|
||||
export default class UserBio extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* Whether or not the bio is currently being edited.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.editing = false;
|
||||
|
||||
/**
|
||||
* Whether or not the bio is currently being saved.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
view() {
|
||||
const user = this.props.user;
|
||||
let content;
|
||||
|
||||
if (this.editing) {
|
||||
content = <textarea className="FormControl" placeholder={app.trans('core.bio_placeholder')} rows="3" value={user.bio()}/>;
|
||||
} else {
|
||||
let subContent;
|
||||
|
||||
if (this.loading) {
|
||||
subContent = <p className="UserBio-placeholder">{LoadingIndicator.component({size: 'tiny'})}</p>;
|
||||
} else {
|
||||
const bioHtml = user.bioHtml();
|
||||
|
||||
if (bioHtml) {
|
||||
subContent = m.trust(bioHtml);
|
||||
} else if (this.props.editable) {
|
||||
subContent = <p className="UserBio-placeholder">{app.trans('core.bio_placeholder')}</p>;
|
||||
}
|
||||
}
|
||||
|
||||
content = <div className="UserBio-content">{subContent}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'UserBio ' + classList({
|
||||
editable: this.props.editable,
|
||||
editing: this.editing
|
||||
})}
|
||||
onclick={this.edit.bind(this)}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the bio.
|
||||
*/
|
||||
edit() {
|
||||
if (!this.props.editable) return;
|
||||
|
||||
this.editing = true;
|
||||
m.redraw();
|
||||
|
||||
const bio = this;
|
||||
const save = function(e) {
|
||||
if (e.shiftKey) return;
|
||||
e.preventDefault();
|
||||
bio.save($(this).val());
|
||||
};
|
||||
|
||||
this.$('textarea').focus()
|
||||
.bind('blur', save)
|
||||
.bind('keydown', 'return', save);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the bio.
|
||||
*
|
||||
* @param {String} value
|
||||
*/
|
||||
save(value) {
|
||||
const user = this.props.user;
|
||||
|
||||
if (user.bio() !== value) {
|
||||
this.loading = true;
|
||||
|
||||
user.save({bio: value}).then(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
this.editing = false;
|
||||
m.redraw();
|
||||
}
|
||||
}
|
@@ -1,107 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import humanTime from 'flarum/utils/humanTime';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import UserControls from 'flarum/utils/UserControls';
|
||||
import avatar from 'flarum/helpers/avatar';
|
||||
import username from 'flarum/helpers/username';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import Dropdown from 'flarum/components/Dropdown';
|
||||
import UserBio from 'flarum/components/UserBio';
|
||||
import AvatarEditor from 'flarum/components/AvatarEditor';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
|
||||
/**
|
||||
* The `UserCard` component displays a user's profile card. This is used both on
|
||||
* the `UserPage` (in the hero) and in discussions, shown when hovering over a
|
||||
* post author.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `user`
|
||||
* - `className`
|
||||
* - `editable`
|
||||
* - `controlsButtonClassName`
|
||||
*/
|
||||
export default class UserCard extends Component {
|
||||
view() {
|
||||
const user = this.props.user;
|
||||
const controls = UserControls.controls(user, this).toArray();
|
||||
const color = user.color();
|
||||
const badges = user.badges().toArray();
|
||||
|
||||
return (
|
||||
<div className={'UserCard ' + (this.props.className || '')}
|
||||
style={color ? {backgroundColor: color} : ''}>
|
||||
<div className="darkenBackground">
|
||||
|
||||
<div className="container">
|
||||
{controls.length ? Dropdown.component({
|
||||
children: controls,
|
||||
className: 'UserCard-controls App-primaryControl',
|
||||
menuClassName: 'Dropdown-menu--right',
|
||||
buttonClassName: this.props.controlsButtonClassName,
|
||||
icon: 'ellipsis-v'
|
||||
}) : ''}
|
||||
|
||||
<div className="UserCard-profile">
|
||||
<h2 className="UserCard-identity">
|
||||
{this.props.editable
|
||||
? [AvatarEditor.component({user, className: 'UserCard-avatar'}), username(user)]
|
||||
: (
|
||||
<a href={app.route.user(user)} config={m.route}>
|
||||
<div className="UserCard-avatar">{avatar(user)}</div>
|
||||
{username(user)}
|
||||
</a>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
{badges.length ? (
|
||||
<ul className="UserCard-badges badges">
|
||||
{listItems(badges)}
|
||||
</ul>
|
||||
) : ''}
|
||||
|
||||
<ul className="UserCard-info">
|
||||
{listItems(this.infoItems().toArray())}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list of tidbits of info to show on this user's profile.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
infoItems() {
|
||||
const items = new ItemList();
|
||||
const user = this.props.user;
|
||||
const lastSeenTime = user.lastSeenTime();
|
||||
|
||||
items.add('bio',
|
||||
UserBio.component({
|
||||
user,
|
||||
editable: this.props.editable
|
||||
})
|
||||
);
|
||||
|
||||
if (lastSeenTime) {
|
||||
const online = user.isOnline();
|
||||
|
||||
items.add('lastSeen', (
|
||||
<span className={'UserCard-lastSeen' + (online ? ' online' : '')}>
|
||||
{online
|
||||
? [icon('circle'), ' ', app.trans('core.online')]
|
||||
: [icon('clock-o'), ' ', humanTime(lastSeenTime)]}
|
||||
</span>
|
||||
));
|
||||
}
|
||||
|
||||
items.add('joined', app.trans('core.joined', {ago: humanTime(user.joinTime())}));
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
@@ -1,168 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import affixSidebar from 'flarum/utils/affixSidebar';
|
||||
import UserCard from 'flarum/components/UserCard';
|
||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
import SelectDropdown from 'flarum/components/SelectDropdown';
|
||||
import LinkButton from 'flarum/components/LinkButton';
|
||||
import Separator from 'flarum/components/Separator';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
|
||||
/**
|
||||
* The `UserPage` component shows a user's profile. It can be extended to show
|
||||
* content inside of the content area. See `ActivityPage` and `SettingsPage` for
|
||||
* examples.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class UserPage extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* The user this page is for.
|
||||
*
|
||||
* @type {User}
|
||||
*/
|
||||
this.user = null;
|
||||
|
||||
app.history.push('user');
|
||||
app.current = this;
|
||||
app.drawer.hide();
|
||||
app.modal.close();
|
||||
}
|
||||
|
||||
view() {
|
||||
return (
|
||||
<div className="UserPage">
|
||||
{this.user ? [
|
||||
UserCard.component({
|
||||
user: this.user,
|
||||
className: 'Hero UserHero',
|
||||
editable: this.user.canEdit() || this.user === app.session.user,
|
||||
controlsButtonClassName: 'Button'
|
||||
}),
|
||||
<div className="container">
|
||||
<nav className="sideNav UserPage-nav" config={affixSidebar}>
|
||||
<ul>{listItems(this.sidebarItems().toArray())}</ul>
|
||||
</nav>
|
||||
<div className="sideNavOffset UserPage-content">
|
||||
{this.content()}
|
||||
</div>
|
||||
</div>
|
||||
] : [
|
||||
LoadingIndicator.component({className: 'LoadingIndicator--block'})
|
||||
]}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
if (isInitialized) return;
|
||||
|
||||
$('#app').addClass('App--user');
|
||||
context.onunload = () => $('#app').removeClass('App--user');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content to display in the user page.
|
||||
*
|
||||
* @return {VirtualElement}
|
||||
*/
|
||||
content() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component with a user, and trigger the loading of their
|
||||
* activity feed.
|
||||
*
|
||||
* @param {User} user
|
||||
* @protected
|
||||
*/
|
||||
init(user) {
|
||||
this.user = user;
|
||||
|
||||
app.setTitle(user.username());
|
||||
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a username, load the user's profile from the store, or make a request
|
||||
* if we don't have it yet. Then initialize the profile page with that user.
|
||||
*
|
||||
* @param {String} username
|
||||
*/
|
||||
loadUser(username) {
|
||||
const lowercaseUsername = username.toLowerCase();
|
||||
|
||||
app.store.all('users').some(user => {
|
||||
if (user.username().toLowerCase() === lowercaseUsername && user.joinTime()) {
|
||||
this.init(user);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.user) {
|
||||
app.store.find('users', username).then(this.init.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the content of the sidebar.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
sidebarItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('nav',
|
||||
SelectDropdown.component({
|
||||
children: this.navItems().toArray(),
|
||||
className: 'App-titleControl',
|
||||
buttonClassName: 'Button'
|
||||
})
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the navigation in the sidebar.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
navItems() {
|
||||
const items = new ItemList();
|
||||
const user = this.user;
|
||||
|
||||
items.add('posts',
|
||||
LinkButton.component({
|
||||
href: app.route('user.posts', {username: user.username()}),
|
||||
children: [app.trans('core.posts'), <span className="Button-badge">{user.commentsCount()}</span>],
|
||||
icon: 'comment-o'
|
||||
})
|
||||
);
|
||||
|
||||
items.add('discussions',
|
||||
LinkButton.component({
|
||||
href: app.route('user.discussions', {username: user.username()}),
|
||||
children: [app.trans('core.discussions'), <span className="Button-badge">{user.discussionsCount()}</span>],
|
||||
icon: 'reorder'
|
||||
})
|
||||
);
|
||||
|
||||
if (app.session.user === user) {
|
||||
items.add('separator', Separator.component());
|
||||
items.add('settings',
|
||||
LinkButton.component({
|
||||
href: app.route('settings'),
|
||||
children: app.trans('core.settings'),
|
||||
icon: 'cog'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
@@ -1,36 +0,0 @@
|
||||
import highlight from 'flarum/helpers/highlight';
|
||||
import avatar from 'flarum/helpers/avatar';
|
||||
|
||||
/**
|
||||
* The `UsersSearchSource` finds and displays user search results in the search
|
||||
* dropdown.
|
||||
*
|
||||
* @implements SearchSource
|
||||
*/
|
||||
export default class UsersSearchResults {
|
||||
search(query) {
|
||||
return app.store.find('users', {
|
||||
filter: {q: query},
|
||||
page: {limit: 5}
|
||||
});
|
||||
}
|
||||
|
||||
view(query) {
|
||||
const results = app.store.all('users')
|
||||
.filter(user => user.username().toLowerCase().substr(0, query.length) === query);
|
||||
|
||||
if (!results.length) return '';
|
||||
|
||||
return [
|
||||
<li className="Dropdown-header">{app.trans('core.users')}</li>,
|
||||
results.map(user => (
|
||||
<li className="UserSearchResult" data-index={'users' + user.id()}>
|
||||
<a href={app.route.user(user)} config={m.route}>
|
||||
{avatar(user)}
|
||||
{highlight(user.username(), query)}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,82 +0,0 @@
|
||||
/*global FastClick*/
|
||||
|
||||
import ScrollListener from 'flarum/utils/ScrollListener';
|
||||
import Pane from 'flarum/utils/Pane';
|
||||
import Drawer from 'flarum/utils/Drawer';
|
||||
import mapRoutes from 'flarum/utils/mapRoutes';
|
||||
|
||||
import Navigation from 'flarum/components/Navigation';
|
||||
import HeaderPrimary from 'flarum/components/HeaderPrimary';
|
||||
import HeaderSecondary from 'flarum/components/HeaderSecondary';
|
||||
import Composer from 'flarum/components/Composer';
|
||||
import ModalManager from 'flarum/components/ModalManager';
|
||||
import AlertManager from 'flarum/components/AlertManager';
|
||||
|
||||
/**
|
||||
* The `boot` initializer boots up the forum app. It initializes some app
|
||||
* globals, mounts components to the page, and begins routing.
|
||||
*
|
||||
* @param {ForumApp} app
|
||||
*/
|
||||
export default function boot(app) {
|
||||
// Get the configured default route and update that route's path to be '/'.
|
||||
// Push the homepage as the first route, so that the user will always be
|
||||
// able to click on the 'back' button to go home, regardless of which page
|
||||
// they started on.
|
||||
const defaultRoute = app.forum.attribute('defaultRoute');
|
||||
|
||||
for (const i in app.routes) {
|
||||
if (app.routes[i].path === defaultRoute) {
|
||||
app.routes[i].path = '/';
|
||||
app.history.push(i, '/');
|
||||
}
|
||||
}
|
||||
|
||||
m.startComputation();
|
||||
|
||||
m.mount(document.getElementById('app-navigation'), Navigation.component({className: 'App-backControl', drawer: true}));
|
||||
m.mount(document.getElementById('header-navigation'), Navigation.component());
|
||||
m.mount(document.getElementById('header-primary'), HeaderPrimary.component());
|
||||
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
|
||||
|
||||
app.pane = new Pane(document.getElementById('app'));
|
||||
app.drawer = new Drawer();
|
||||
app.composer = m.mount(document.getElementById('composer'), Composer.component());
|
||||
app.modal = m.mount(document.getElementById('modal'), ModalManager.component());
|
||||
app.alerts = m.mount(document.getElementById('alerts'), AlertManager.component());
|
||||
|
||||
const basePath = app.forum.attribute('basePath');
|
||||
m.route.mode = 'pathname';
|
||||
m.route(
|
||||
document.getElementById('content'),
|
||||
basePath + '/',
|
||||
mapRoutes(app.routes, basePath)
|
||||
);
|
||||
|
||||
m.endComputation();
|
||||
|
||||
// Route the home link back home when clicked. We do not want it to register
|
||||
// if the user is opening it in a new tab, however.
|
||||
$('#home-link').click(e => {
|
||||
if (e.ctrlKey || e.metaKey || e.which === 2) return;
|
||||
e.preventDefault();
|
||||
app.history.home();
|
||||
});
|
||||
|
||||
// Add a class to the body which indicates that the page has been scrolled
|
||||
// down.
|
||||
new ScrollListener(top => {
|
||||
const $app = $('#app');
|
||||
const offset = $app.offset().top;
|
||||
|
||||
$app
|
||||
.toggleClass('affix', top >= offset)
|
||||
.toggleClass('scrolled', top > offset);
|
||||
}).start();
|
||||
|
||||
// Initialize FastClick, which makes links and buttons much more responsive on
|
||||
// touch devices.
|
||||
$(() => FastClick.attach(document.body));
|
||||
|
||||
app.booted = true;
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
import CommentPost from 'flarum/components/CommentPost';
|
||||
import DiscussionRenamedPost from 'flarum/components/DiscussionRenamedPost';
|
||||
import PostedActivity from 'flarum/components/PostedActivity';
|
||||
import JoinedActivity from 'flarum/components/JoinedActivity';
|
||||
import DiscussionRenamedNotification from 'flarum/components/DiscussionRenamedNotification';
|
||||
|
||||
/**
|
||||
* The `components` initializer registers components to display the default post
|
||||
* types, activity types, and notifications type with the application.
|
||||
*
|
||||
* @param {ForumApp} app
|
||||
*/
|
||||
export default function components(app) {
|
||||
app.postComponents.comment = CommentPost;
|
||||
app.postComponents.discussionRenamed = DiscussionRenamedPost;
|
||||
|
||||
app.notificationComponents.discussionRenamed = DiscussionRenamedNotification;
|
||||
}
|
@@ -1,64 +0,0 @@
|
||||
import IndexPage from 'flarum/components/IndexPage';
|
||||
import DiscussionPage from 'flarum/components/DiscussionPage';
|
||||
import PostsUserPage from 'flarum/components/PostsUserPage';
|
||||
import DiscussionsUserPage from 'flarum/components/DiscussionsUserPage';
|
||||
import SettingsPage from 'flarum/components/SettingsPage';
|
||||
import NotificationsPage from 'flarum/components/NotificationsPage';
|
||||
|
||||
/**
|
||||
* The `routes` initializer defines the forum app's routes.
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
export default function(app) {
|
||||
app.routes = {
|
||||
'index': {path: '/all', component: IndexPage.component()},
|
||||
'index.filter': {path: '/:filter', component: IndexPage.component()},
|
||||
|
||||
'discussion': {path: '/d/:id', component: DiscussionPage.component()},
|
||||
'discussion.near': {path: '/d/:id/:near', component: DiscussionPage.component()},
|
||||
|
||||
'user': {path: '/u/:username', component: PostsUserPage.component()},
|
||||
'user.posts': {path: '/u/:username', component: PostsUserPage.component()},
|
||||
'user.discussions': {path: '/u/:username/discussions', component: DiscussionsUserPage.component()},
|
||||
|
||||
'settings': {path: '/settings', component: SettingsPage.component()},
|
||||
'notifications': {path: '/notifications', component: NotificationsPage.component()}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a URL to a discussion.
|
||||
*
|
||||
* @param {Discussion} discussion
|
||||
* @param {Integer} [near]
|
||||
* @return {String}
|
||||
*/
|
||||
app.route.discussion = (discussion, near) => {
|
||||
return app.route(near > 1 ? 'discussion.near' : 'discussion', {
|
||||
id: discussion.id() + '-' + discussion.slug(),
|
||||
near: near > 1 ? near : undefined
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a URL to a post.
|
||||
*
|
||||
* @param {Post} post
|
||||
* @return {String}
|
||||
*/
|
||||
app.route.post = post => {
|
||||
return app.route.discussion(post.discussion(), post.number());
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a URL to a user.
|
||||
*
|
||||
* @param {User} user
|
||||
* @return {String}
|
||||
*/
|
||||
app.route.user = user => {
|
||||
return app.route('user', {
|
||||
username: user.username()
|
||||
});
|
||||
};
|
||||
}
|
@@ -1,215 +0,0 @@
|
||||
import DiscussionPage from 'flarum/components/DiscussionPage';
|
||||
import ReplyComposer from 'flarum/components/ReplyComposer';
|
||||
import LogInModal from 'flarum/components/LogInModal';
|
||||
import Button from 'flarum/components/Button';
|
||||
import Separator from 'flarum/components/Separator';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
|
||||
/**
|
||||
* The `DiscussionControls` utility constructs a list of buttons for a
|
||||
* discussion which perform actions on it.
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Get a list of controls for a discussion.
|
||||
*
|
||||
* @param {Discussion} discussion
|
||||
* @param {*} context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @return {ItemList}
|
||||
* @public
|
||||
*/
|
||||
controls(discussion, context) {
|
||||
const items = new ItemList();
|
||||
|
||||
['user', 'moderation', 'destructive'].forEach(section => {
|
||||
const controls = this[section + 'Controls'](discussion, context).toArray();
|
||||
if (controls.length) {
|
||||
controls.forEach(item => items.add(item.itemName, item));
|
||||
items.add(section + 'Separator', Separator.component());
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get controls for a discussion pertaining to the current user (e.g. reply,
|
||||
* follow).
|
||||
*
|
||||
* @param {Discussion} discussion
|
||||
* @param {*} context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @return {ItemList}
|
||||
* @protected
|
||||
*/
|
||||
userControls(discussion, context) {
|
||||
const items = new ItemList();
|
||||
|
||||
// Only add a reply control if this is the discussion's controls dropdown
|
||||
// for the discussion page itself. We don't want it to show up for
|
||||
// discussions in the discussion list, etc.
|
||||
if (context instanceof DiscussionPage) {
|
||||
items.add('reply',
|
||||
!app.session.user || discussion.canReply()
|
||||
? Button.component({
|
||||
icon: 'reply',
|
||||
children: app.session.user ? app.trans('core.reply') : app.trans('core.log_in_to_reply'),
|
||||
onclick: this.replyAction.bind(discussion, true, false)
|
||||
})
|
||||
: Button.component({
|
||||
icon: 'reply',
|
||||
children: app.trans('core.cannot_reply'),
|
||||
className: 'disabled',
|
||||
title: app.trans('core.cannot_reply_help')
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get controls for a discussion pertaining to moderation (e.g. rename, lock).
|
||||
*
|
||||
* @param {Discussion} discussion
|
||||
* @param {*} context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @return {ItemList}
|
||||
* @protected
|
||||
*/
|
||||
moderationControls(discussion) {
|
||||
const items = new ItemList();
|
||||
|
||||
if (discussion.canRename()) {
|
||||
items.add('rename', Button.component({
|
||||
icon: 'pencil',
|
||||
children: app.trans('core.rename'),
|
||||
onclick: this.renameAction.bind(discussion)
|
||||
}));
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get controls for a discussion which are destructive (e.g. delete).
|
||||
*
|
||||
* @param {Discussion} discussion
|
||||
* @param {*} context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @return {ItemList}
|
||||
* @protected
|
||||
*/
|
||||
destructiveControls(discussion) {
|
||||
const items = new ItemList();
|
||||
|
||||
if (discussion.canDelete()) {
|
||||
items.add('delete', Button.component({
|
||||
icon: 'times',
|
||||
children: app.trans('core.delete'),
|
||||
onclick: this.deleteAction.bind(discussion)
|
||||
}));
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the reply composer for the discussion. A promise will be returned,
|
||||
* which resolves when the composer opens successfully. If the user is not
|
||||
* logged in, they will be prompted and then the reply composer will open (and
|
||||
* the promise will resolve) after they do. If they don't have permission to
|
||||
* reply, the promise will be rejected.
|
||||
*
|
||||
* @param {Boolean} goToLast Whether or not to scroll down to the last post if
|
||||
* the discussion is being viewed.
|
||||
* @param {Boolean} forceRefresh Whether or not to force a reload of the
|
||||
* composer component, even if it is already open for this discussion.
|
||||
* @return {Promise}
|
||||
*/
|
||||
replyAction(goToLast, forceRefresh) {
|
||||
const deferred = m.deferred();
|
||||
|
||||
// Define a function that will check the user's permission to reply, and
|
||||
// either open the reply composer for this discussion and resolve the
|
||||
// promise, or reject it.
|
||||
const reply = () => {
|
||||
if (this.canReply()) {
|
||||
if (goToLast && app.viewingDiscussion(this)) {
|
||||
app.current.stream.goToLast();
|
||||
}
|
||||
|
||||
let component = app.composer.component;
|
||||
if (!app.composingReplyTo(this) || forceRefresh) {
|
||||
component = new ReplyComposer({
|
||||
user: app.session.user,
|
||||
discussion: this
|
||||
});
|
||||
app.composer.load(component);
|
||||
}
|
||||
app.composer.show();
|
||||
|
||||
deferred.resolve(component);
|
||||
} else {
|
||||
deferred.reject();
|
||||
}
|
||||
};
|
||||
|
||||
// If the user is logged in, then we can run that function right away. But
|
||||
// if they're not, we'll prompt them to log in and then run the function
|
||||
// after the discussion has reloaded.
|
||||
if (app.session.user) {
|
||||
reply();
|
||||
} else {
|
||||
app.modal.show(
|
||||
new LogInModal({
|
||||
onlogin: () => app.current.one('loaded', reply)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the discussion after confirming with the user.
|
||||
*/
|
||||
deleteAction() {
|
||||
if (confirm(extractText(app.trans('core.confirm_delete_discussion')))) {
|
||||
this.delete();
|
||||
|
||||
// If there is a discussion list in the cache, remove this discussion.
|
||||
if (app.cache.discussionList) {
|
||||
app.cache.discussionList.removeDiscussion(this);
|
||||
}
|
||||
|
||||
// If we're currently viewing the discussion that was deleted, go back
|
||||
// to the previous page.
|
||||
if (app.viewingDiscussion(this)) {
|
||||
app.history.back();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Rename the discussion.
|
||||
*/
|
||||
renameAction() {
|
||||
const currentTitle = this.title();
|
||||
const title = prompt(extractText(app.trans('core.prompt_rename_discussion')), 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) {
|
||||
this.save({title}).then(() => {
|
||||
if (app.viewingDiscussion(this)) {
|
||||
app.current.stream.update();
|
||||
}
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@@ -1,140 +0,0 @@
|
||||
import EditPostComposer from 'flarum/components/EditPostComposer';
|
||||
import Button from 'flarum/components/Button';
|
||||
import Separator from 'flarum/components/Separator';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
|
||||
/**
|
||||
* The `PostControls` utility constructs a list of buttons for a post which
|
||||
* perform actions on it.
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Get a list of controls for a post.
|
||||
*
|
||||
* @param {Post} post
|
||||
* @param {*} context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @return {ItemList}
|
||||
* @public
|
||||
*/
|
||||
controls(post, context) {
|
||||
const items = new ItemList();
|
||||
|
||||
['user', 'moderation', 'destructive'].forEach(section => {
|
||||
const controls = this[section + 'Controls'](post, context).toArray();
|
||||
if (controls.length) {
|
||||
controls.forEach(item => items.add(item.itemName, item));
|
||||
items.add(section + 'Separator', Separator.component());
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get controls for a post pertaining to the current user (e.g. report).
|
||||
*
|
||||
* @param {Post} post
|
||||
* @param {*} context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @return {ItemList}
|
||||
* @protected
|
||||
*/
|
||||
userControls() {
|
||||
return new ItemList();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get controls for a post pertaining to moderation (e.g. edit).
|
||||
*
|
||||
* @param {Post} post
|
||||
* @param {*} context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @return {ItemList}
|
||||
* @protected
|
||||
*/
|
||||
moderationControls(post) {
|
||||
const items = new ItemList();
|
||||
|
||||
if (post.contentType() === 'comment' && post.canEdit()) {
|
||||
if (post.isHidden()) {
|
||||
items.add('restore', Button.component({
|
||||
icon: 'reply',
|
||||
children: app.trans('core.restore'),
|
||||
onclick: this.restoreAction.bind(post)
|
||||
}));
|
||||
} else {
|
||||
items.add('edit', Button.component({
|
||||
icon: 'pencil',
|
||||
children: app.trans('core.edit'),
|
||||
onclick: this.editAction.bind(post)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get controls for a post that are destructive (e.g. delete).
|
||||
*
|
||||
* @param {Post} post
|
||||
* @param {*} context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @return {ItemList}
|
||||
* @protected
|
||||
*/
|
||||
destructiveControls(post) {
|
||||
const items = new ItemList();
|
||||
|
||||
if (post.number() !== 1) {
|
||||
if (post.contentType() === 'comment' && !post.isHidden() && post.canEdit()) {
|
||||
items.add('hide', Button.component({
|
||||
icon: 'times',
|
||||
children: app.trans('core.delete'),
|
||||
onclick: this.hideAction.bind(post)
|
||||
}));
|
||||
} else if ((post.contentType() !== 'comment' || post.isHidden()) && post.canDelete()) {
|
||||
items.add('delete', Button.component({
|
||||
icon: 'times',
|
||||
children: app.trans('core.delete_forever'),
|
||||
onclick: this.deleteAction.bind(post)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the composer to edit a post.
|
||||
*/
|
||||
editAction() {
|
||||
app.composer.load(new EditPostComposer({ post: this }));
|
||||
app.composer.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide a post.
|
||||
*/
|
||||
hideAction() {
|
||||
this.save({ isHidden: true });
|
||||
this.pushAttributes({ hideTime: new Date(), hideUser: app.session.user });
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore a post.
|
||||
*/
|
||||
restoreAction() {
|
||||
this.save({ isHidden: false });
|
||||
this.pushAttributes({ hideTime: null, hideUser: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a post.
|
||||
*/
|
||||
deleteAction() {
|
||||
this.delete();
|
||||
this.discussion().removePost(this.id());
|
||||
}
|
||||
};
|
@@ -1,115 +0,0 @@
|
||||
import Button from 'flarum/components/Button';
|
||||
import Separator from 'flarum/components/Separator';
|
||||
import EditUserModal from 'flarum/components/EditUserModal';
|
||||
import UserPage from 'flarum/components/UserPage';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
|
||||
/**
|
||||
* The `UserControls` utility constructs a list of buttons for a user which
|
||||
* perform actions on it.
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Get a list of controls for a user.
|
||||
*
|
||||
* @param {User} user
|
||||
* @param {*} context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @return {ItemList}
|
||||
* @public
|
||||
*/
|
||||
controls(discussion, context) {
|
||||
const items = new ItemList();
|
||||
|
||||
['user', 'moderation', 'destructive'].forEach(section => {
|
||||
const controls = this[section + 'Controls'](discussion, context).toArray();
|
||||
if (controls.length) {
|
||||
controls.forEach(item => items.add(item.itemName, item));
|
||||
items.add(section + 'Separator', Separator.component());
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get controls for a user pertaining to the current user (e.g. poke, follow).
|
||||
*
|
||||
* @param {User} user
|
||||
* @param {*} context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @return {ItemList}
|
||||
* @protected
|
||||
*/
|
||||
userControls() {
|
||||
return new ItemList();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get controls for a user pertaining to moderation (e.g. suspend, edit).
|
||||
*
|
||||
* @param {User} user
|
||||
* @param {*} context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @return {ItemList}
|
||||
* @protected
|
||||
*/
|
||||
moderationControls(user) {
|
||||
const items = new ItemList();
|
||||
|
||||
if (user.canEdit()) {
|
||||
items.add('edit', Button.component({
|
||||
icon: 'pencil',
|
||||
children: app.trans('core.edit'),
|
||||
onclick: this.editAction.bind(user)
|
||||
}));
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get controls for a user which are destructive (e.g. delete).
|
||||
*
|
||||
* @param {User} user
|
||||
* @param {*} context The parent component under which the controls menu will
|
||||
* be displayed.
|
||||
* @return {ItemList}
|
||||
* @protected
|
||||
*/
|
||||
destructiveControls(user) {
|
||||
const items = new ItemList();
|
||||
|
||||
if (user.id() !== '1' && user.canDelete()) {
|
||||
items.add('delete', Button.component({
|
||||
icon: 'times',
|
||||
children: app.trans('core.delete'),
|
||||
onclick: this.deleteAction.bind(user)
|
||||
}));
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the user.
|
||||
*/
|
||||
deleteAction() {
|
||||
if (confirm('Are you sure you want to delete this user? All of the user\'s posts will be deleted.')) {
|
||||
this.delete().then(() => {
|
||||
if (app.current instanceof UserPage && app.current.user === this) {
|
||||
app.history.back();
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit the user.
|
||||
*/
|
||||
editAction() {
|
||||
app.modal.show(new EditUserModal({user: this}));
|
||||
}
|
||||
};
|
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Setup the sidebar DOM element to be affixed to the top of the viewport
|
||||
* using Bootstrap's affix plugin.
|
||||
*
|
||||
* @param {DOMElement} element
|
||||
* @param {Boolean} isInitialized
|
||||
*/
|
||||
export default function affixSidebar(element, isInitialized) {
|
||||
if (isInitialized) return;
|
||||
|
||||
const $sidebar = $(element);
|
||||
const $header = $('#header');
|
||||
const $footer = $('#footer');
|
||||
|
||||
// Don't affix the sidebar if it is taller than the viewport (otherwise
|
||||
// there would be no way to scroll through its content).
|
||||
if ($sidebar.outerHeight(true) > $(window).height() - $header.outerHeight(true)) return;
|
||||
|
||||
$sidebar.find('> ul').affix({
|
||||
offset: {
|
||||
top: () => $sidebar.offset().top - $header.outerHeight(true) - parseInt($sidebar.css('margin-top'), 10),
|
||||
bottom: () => this.bottom = $footer.outerHeight(true)
|
||||
}
|
||||
});
|
||||
}
|
276
js/lib/App.js
276
js/lib/App.js
@@ -1,276 +0,0 @@
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import Alert from 'flarum/components/Alert';
|
||||
import Translator from 'flarum/Translator';
|
||||
import extract from 'flarum/utils/extract';
|
||||
import patchMithril from 'flarum/utils/patchMithril';
|
||||
|
||||
/**
|
||||
* The `App` class provides a container for an application, as well as various
|
||||
* utilities for the rest of the app to use.
|
||||
*/
|
||||
export default class App {
|
||||
constructor() {
|
||||
patchMithril(window);
|
||||
|
||||
/**
|
||||
* The forum model for this application.
|
||||
*
|
||||
* @type {Forum}
|
||||
* @public
|
||||
*/
|
||||
this.forum = null;
|
||||
|
||||
/**
|
||||
* A map of routes, keyed by a unique route name. Each route is an object
|
||||
* containing the following properties:
|
||||
*
|
||||
* - `path` The path that the route is accessed at.
|
||||
* - `component` The Mithril component to render when this route is active.
|
||||
*
|
||||
* @example
|
||||
* app.routes.discussion = {path: '/d/:id', component: DiscussionPage.component()};
|
||||
*
|
||||
* @type {Object}
|
||||
* @public
|
||||
*/
|
||||
this.routes = {};
|
||||
|
||||
/**
|
||||
* An object containing data to preload into the application.
|
||||
*
|
||||
* @type {Object}
|
||||
* @property {Object} preload.data An array of resource objects to preload
|
||||
* into the data store.
|
||||
* @property {Object} preload.document An API response document to be used
|
||||
* by the route that is first activated.
|
||||
* @property {Object} preload.session A response from the /api/token
|
||||
* endpoint containing the session's authentication token and user ID.
|
||||
* @public
|
||||
*/
|
||||
this.preload = {
|
||||
data: null,
|
||||
document: null,
|
||||
session: null
|
||||
};
|
||||
|
||||
/**
|
||||
* An ordered list of initializers to bootstrap the application.
|
||||
*
|
||||
* @type {ItemList}
|
||||
* @public
|
||||
*/
|
||||
this.initializers = new ItemList();
|
||||
|
||||
/**
|
||||
* The app's session.
|
||||
*
|
||||
* @type {Session}
|
||||
* @public
|
||||
*/
|
||||
this.session = null;
|
||||
|
||||
/**
|
||||
* The app's translator.
|
||||
*
|
||||
* @type {Translator}
|
||||
* @public
|
||||
*/
|
||||
this.translator = new Translator();
|
||||
|
||||
/**
|
||||
* The app's data store.
|
||||
*
|
||||
* @type {Store}
|
||||
* @public
|
||||
*/
|
||||
this.store = null;
|
||||
|
||||
/**
|
||||
* A local cache that can be used to store data at the application level, so
|
||||
* that is persists between different routes.
|
||||
*
|
||||
* @type {Object}
|
||||
* @public
|
||||
*/
|
||||
this.cache = {};
|
||||
|
||||
/**
|
||||
* Whether or not the app has been booted.
|
||||
*
|
||||
* @type {Boolean}
|
||||
* @public
|
||||
*/
|
||||
this.booted = false;
|
||||
|
||||
/**
|
||||
* An Alert that was shown as a result of an AJAX request error. If present,
|
||||
* it will be dismissed on the next successful request.
|
||||
*
|
||||
* @type {null|Alert}
|
||||
* @private
|
||||
*/
|
||||
this.requestError = null;
|
||||
|
||||
this.title = '';
|
||||
this.titleCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot the application by running all of the registered initializers.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
boot() {
|
||||
this.initializers.toArray().forEach(initializer => initializer(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the API response document that has been preloaded into the application.
|
||||
*
|
||||
* @return {Object|null}
|
||||
* @public
|
||||
*/
|
||||
preloadedDocument() {
|
||||
if (app.preload.document) {
|
||||
const results = app.store.pushPayload(app.preload.document);
|
||||
app.preload.document = null;
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the <title> of the page.
|
||||
*
|
||||
* @param {String} title
|
||||
* @param {Boolean} [separator] Whether or not to separate the given title and
|
||||
* the forum's title.
|
||||
* @public
|
||||
*/
|
||||
setTitle(title) {
|
||||
this.title = title;
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a number to display in the <title> of the page.
|
||||
*
|
||||
* @param {Integer} count
|
||||
*/
|
||||
setTitleCount(count) {
|
||||
this.titleCount = count;
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
updateTitle() {
|
||||
document.title = (this.titleCount ? `(${this.titleCount}) ` : '') +
|
||||
(this.title ? this.title + ' - ' : '') +
|
||||
this.forum.attribute('title');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an AJAX request, handling any low-level errors that may occur.
|
||||
*
|
||||
* @see https://lhorie.github.io/mithril/mithril.request.html
|
||||
* @param {Object} options
|
||||
* @return {Promise}
|
||||
* @public
|
||||
*/
|
||||
request(options) {
|
||||
// Set some default options if they haven't been overridden. We want to
|
||||
// authenticate all requests with the session token. We also want all
|
||||
// requests to run asynchronously in the background, so that they don't
|
||||
// prevent redraws from occurring.
|
||||
options.config = options.config || this.session.authorize.bind(this.session);
|
||||
options.background = options.background || true;
|
||||
|
||||
// When we deserialize JSON data, if for some reason the server has provided
|
||||
// a dud response, we don't want the application to crash. We'll show an
|
||||
// error message to the user instead.
|
||||
options.deserialize = options.deserialize || (responseText => {
|
||||
try {
|
||||
return JSON.parse(responseText);
|
||||
} catch (e) {
|
||||
throw new Error('Oops! Something went wrong on the server. Please reload the page and try again.');
|
||||
}
|
||||
});
|
||||
|
||||
// When extracting the data from the response, we can check the server
|
||||
// response code and show an error message to the user if something's gone
|
||||
// awry.
|
||||
const original = options.extract;
|
||||
options.extract = xhr => {
|
||||
const status = xhr.status;
|
||||
|
||||
if (status >= 500 && status <= 599) {
|
||||
throw new Error('Oops! Something went wrong on the server. Please reload the page and try again.');
|
||||
}
|
||||
|
||||
if (original) {
|
||||
return original(xhr.responseText);
|
||||
}
|
||||
|
||||
return xhr.responseText.length > 0 ? xhr.responseText : null;
|
||||
};
|
||||
|
||||
this.alerts.dismiss(this.requestError);
|
||||
|
||||
// Now make the request. If it's a failure, inspect the error that was
|
||||
// returned and show an alert containing its contents.
|
||||
return m.request(options).then(null, response => {
|
||||
if (response instanceof Error) {
|
||||
this.alerts.show(this.requestError = new Alert({
|
||||
type: 'error',
|
||||
children: response.message
|
||||
}));
|
||||
}
|
||||
|
||||
throw response;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show alert error messages for each error returned in an API response.
|
||||
*
|
||||
* @param {Array} errors
|
||||
* @public
|
||||
*/
|
||||
alertErrors(errors) {
|
||||
errors.forEach(error => {
|
||||
this.alerts.show(new Alert({
|
||||
type: 'error',
|
||||
children: error.detail
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a URL to the route with the given name.
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {Object} params
|
||||
* @return {String}
|
||||
* @public
|
||||
*/
|
||||
route(name, params = {}) {
|
||||
const url = this.routes[name].path.replace(/:([^\/]+)/g, (m, key) => extract(params, key));
|
||||
const queryString = m.route.buildQueryString(params);
|
||||
const prefix = m.route.mode === 'pathname' ? app.forum.attribute('basePath') : '';
|
||||
|
||||
return prefix + url + (queryString ? '?' + queryString : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to translate the given key.
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {Object} input
|
||||
* @return {String}
|
||||
* @public
|
||||
*/
|
||||
trans(key, input) {
|
||||
return this.translator.trans(key, input);
|
||||
}
|
||||
}
|
@@ -1,86 +0,0 @@
|
||||
import User from 'flarum/models/User';
|
||||
import username from 'flarum/helpers/username';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
import extract from 'flarum/utils/extract';
|
||||
|
||||
/**
|
||||
* The `Translator` class translates strings using the loaded localization.
|
||||
*/
|
||||
export default class Translator {
|
||||
constructor() {
|
||||
/**
|
||||
* A map of translation keys to their translated values.
|
||||
*
|
||||
* @type {Object}
|
||||
* @public
|
||||
*/
|
||||
this.translations = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the key of a translation that should be used for the given count.
|
||||
* The default implementation is for English plurals. It should be overridden
|
||||
* by a locale's JavaScript file if necessary.
|
||||
*
|
||||
* @param {Integer} count
|
||||
* @return {String}
|
||||
* @public
|
||||
*/
|
||||
plural(count) {
|
||||
return count === 1 ? 'one' : 'other';
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a string.
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {Object} input
|
||||
* @param {VirtualElement} fallback
|
||||
* @return {VirtualElement}
|
||||
*/
|
||||
trans(key, input = {}, fallback) {
|
||||
const parts = key.split('.');
|
||||
let translation = this.translations;
|
||||
|
||||
// Drill down into the translation tree to find the translation for this
|
||||
// key.
|
||||
parts.forEach(part => {
|
||||
translation = translation && translation[part];
|
||||
});
|
||||
|
||||
// If this translation has multiple options and a 'count' has been provided
|
||||
// in the input, we'll work out which option to choose using the `plural`
|
||||
// method.
|
||||
if (typeof translation === 'object' && typeof input.count !== 'undefined') {
|
||||
translation = translation[this.plural(extractText(input.count))];
|
||||
}
|
||||
|
||||
// If we've been given a user model as one of the input parameters, then
|
||||
// we'll extract the username and use that for the translation. In the
|
||||
// future there should be a hook here to inspect the user and change the
|
||||
// translation key. This will allow a gender property to determine which
|
||||
// translation key is used.
|
||||
if ('user' in input) {
|
||||
const user = extract(input, 'user');
|
||||
|
||||
if (!input.username) input.username = username(user);
|
||||
}
|
||||
|
||||
// If we've found the appropriate translation string, then we'll sub in the
|
||||
// input.
|
||||
if (typeof translation === 'string') {
|
||||
translation = translation.split(new RegExp('({[^}]+})', 'gi'));
|
||||
|
||||
translation.forEach((part, i) => {
|
||||
const match = part.match(/^{(.+)}$/i);
|
||||
if (match) {
|
||||
translation[i] = input[match[1]];
|
||||
}
|
||||
});
|
||||
|
||||
return translation;
|
||||
}
|
||||
|
||||
return fallback || [key];
|
||||
}
|
||||
}
|
@@ -1,27 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
|
||||
/**
|
||||
* The `LoadingIndicator` component displays a loading spinner with spin.js. It
|
||||
* may have the following special props:
|
||||
*
|
||||
* - `size` The spin.js size preset to use. Defaults to 'small'.
|
||||
*
|
||||
* All other props will be assigned as attributes on the element.
|
||||
*/
|
||||
export default class LoadingIndicator extends Component {
|
||||
view() {
|
||||
const attrs = Object.assign({}, this.props);
|
||||
|
||||
attrs.className = 'LoadingIndicator ' + (attrs.className || '');
|
||||
delete attrs.size;
|
||||
|
||||
return <div {...attrs}>{m.trust(' ')}</div>;
|
||||
}
|
||||
|
||||
config() {
|
||||
const size = this.props.size || 'small';
|
||||
|
||||
$.fn.spin.presets[size].zIndex = 'auto';
|
||||
this.$().spin(size);
|
||||
}
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
/**
|
||||
* The `Select` component displays a <select> input, surrounded with some extra
|
||||
* elements for styling. It accepts the following props:
|
||||
*
|
||||
* - `options` A map of option values to labels.
|
||||
* - `onchange` A callback to run when the selected value is changed.
|
||||
* - `value` The value of the selected option.
|
||||
*/
|
||||
export default class Select extends Component {
|
||||
view() {
|
||||
const {options, onchange, value} = this.props;
|
||||
|
||||
return (
|
||||
<span className="Select">
|
||||
<select className="Select-input FormControl" onchange={onchange ? m.withAttr('value', onchange.bind(this)) : undefined} value={value}>
|
||||
{Object.keys(options).map(key => <option value={key}>{options[key]}</option>)}
|
||||
</select>
|
||||
{icon('sort', {className: 'Select-caret'})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* The `icon` helper displays a FontAwesome icon. The fa-fw class is applied.
|
||||
*
|
||||
* @param {String} name The name of the icon class, without the `fa-` prefix.
|
||||
* @param {Object} attrs Any other attributes to apply.
|
||||
* @return {Object}
|
||||
*/
|
||||
export default function icon(name, attrs = {}) {
|
||||
attrs.className = 'icon fa fa-fw fa-' + name + ' ' + (attrs.className || '');
|
||||
|
||||
return <i {...attrs}/>;
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* The `punctuate` helper formats a list of strings (e.g. names) to read
|
||||
* fluently in the application's locale.
|
||||
*
|
||||
* @example
|
||||
* punctuate(['Toby', 'Franz', 'Dominion'])
|
||||
* // Toby, Franz, and Dominion
|
||||
*
|
||||
* @param {Array} items
|
||||
* @return {Array}
|
||||
*/
|
||||
export default function punctuate(items) {
|
||||
const punctuated = [];
|
||||
|
||||
// FIXME: update to use translation
|
||||
items.forEach((item, i) => {
|
||||
punctuated.push(item);
|
||||
|
||||
// If this item is not the last one, then we will follow it with some
|
||||
// punctuation. If the list is more than 2 items long, we'll add a comma.
|
||||
// And if this is the second-to-last item, we'll add 'and'.
|
||||
if (i < items.length - 1) {
|
||||
punctuated.push((items.length > 2 ? ', ' : '') + (i === items.length - 2 ? ' and ' : ''));
|
||||
}
|
||||
});
|
||||
|
||||
return punctuated;
|
||||
};
|
@@ -1,24 +0,0 @@
|
||||
import Session from 'flarum/Session';
|
||||
|
||||
/**
|
||||
* The `preload` initializer creates the application session and preloads it
|
||||
* with data that has been set on the application's `preload` property. It also
|
||||
* preloads any data on the application's `preload` property into the store.
|
||||
* Finally, it sets the application's `forum` instance to the one that was
|
||||
* preloaded.
|
||||
*
|
||||
* `app.preload.session` should be the same as the response from the /api/token
|
||||
* endpoint: it should contain `token` and `userId` keys.
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
export default function preload(app) {
|
||||
app.store.pushPayload({data: app.preload.data});
|
||||
|
||||
app.forum = app.store.getById('forums', 1);
|
||||
|
||||
app.session = new Session(
|
||||
app.preload.session.token,
|
||||
app.store.getById('users', app.preload.session.userId)
|
||||
);
|
||||
}
|
@@ -1,26 +0,0 @@
|
||||
import Store from 'flarum/Store';
|
||||
import Forum from 'flarum/models/Forum';
|
||||
import User from 'flarum/models/User';
|
||||
import Discussion from 'flarum/models/Discussion';
|
||||
import Post from 'flarum/models/Post';
|
||||
import Group from 'flarum/models/Group';
|
||||
import Activity from 'flarum/models/Activity';
|
||||
import Notification from 'flarum/models/Notification';
|
||||
|
||||
/**
|
||||
* The `store` initializer creates the application's data store and registers
|
||||
* the default resource types to their models.
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
export default function store(app) {
|
||||
app.store = new Store({
|
||||
forums: Forum,
|
||||
users: User,
|
||||
discussions: Discussion,
|
||||
posts: Post,
|
||||
groups: Group,
|
||||
activity: Activity,
|
||||
notifications: Notification
|
||||
});
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user