Compare commits
1356 Commits
chapril-1.
...
chapril
Author | SHA1 | Date | |
---|---|---|---|
c2a61801c1 | |||
a89aaf9d09 | |||
0ea9cc416f | |||
d21ec65cc6 | |||
e827c0face | |||
6198507117 | |||
b0b5251211 | |||
|
98229e8165 | ||
|
5c39fd2852 | ||
|
6ea060291c | ||
|
f82e96a5fe | ||
|
79a51e5237 | ||
|
f86b82f0d5 | ||
|
63e56b5b24 | ||
|
57d4abae68 | ||
|
2b852c622e | ||
|
4213e1f1ec | ||
|
c05243f839 | ||
|
ab622cb424 | ||
|
90980bc2c5 | ||
|
ef26f1a684 | ||
|
18ceeb450a | ||
|
123eee675a | ||
|
46236dbe1d | ||
|
1eb111f52f | ||
|
3fea2d0395 | ||
|
44b2ca26bd | ||
|
5bc63185fd | ||
|
071185204e | ||
|
5bc9245dce | ||
|
4fc5554142 | ||
|
f07d6d63de | ||
|
f6cd91ad1f | ||
|
5b4e6ed473 | ||
|
9be4ed84da | ||
|
7a6a013d93 | ||
|
3b8b150d48 | ||
|
888d2ef4b8 | ||
|
3982977121 | ||
|
aff1959030 | ||
|
0b49021f8b | ||
|
f58d6829da | ||
|
d3bc76afbf | ||
|
578bd99be2 | ||
|
c9ebd76cc9 | ||
|
05724476aa | ||
|
6eb2b6d31a | ||
|
cd3d0c5fc4 | ||
|
6a937d6ede | ||
|
2651f4cf40 | ||
|
8549a16ade | ||
|
3fcb44642a | ||
|
41dfc2bc1c | ||
|
a84ec21aba | ||
|
61e6200b02 | ||
|
b1205b6912 | ||
|
26cf892758 | ||
|
d85f708c37 | ||
|
a400499a57 | ||
|
87214b038f | ||
|
90158f1112 | ||
|
75502e2a4b | ||
|
ec849b1488 | ||
|
1e9a7c2cbb | ||
|
63a23748ac | ||
|
999a33c7c3 | ||
|
f6a17d8b3a | ||
|
a18c4daf4c | ||
|
c0d8b2d39a | ||
|
b3e7f23604 | ||
|
20542d3b68 | ||
|
1299b3baf1 | ||
|
d7887ccb83 | ||
|
62248b2f81 | ||
|
ada22e24ef | ||
|
bf7ebfd989 | ||
|
a4cc566ee7 | ||
|
cd61a98f4d | ||
|
926dd01272 | ||
|
d87261a6fb | ||
|
93805dca11 | ||
|
f071f70d1f | ||
|
6596e2d6bd | ||
|
7e71399065 | ||
|
d2ebe86c9f | ||
|
c91e8f6bf3 | ||
|
f997f573ba | ||
|
82255b46eb | ||
|
9ab0768684 | ||
|
303a6f8bd8 | ||
|
da80fa6235 | ||
|
2aedf3027e | ||
|
d308752f25 | ||
|
ead459d79b | ||
|
1323c20162 | ||
|
33e26466a6 | ||
|
4b8cfffede | ||
|
ce6a25048c | ||
|
030a149a61 | ||
|
e4a22bdb83 | ||
|
3ce8c142b8 | ||
|
3ff9ee59cc | ||
|
76eeceecbd | ||
|
e26364f973 | ||
|
279badb2bf | ||
|
d05eb96ac6 | ||
|
2749491467 | ||
|
f23f438763 | ||
|
7b3ce8d812 | ||
|
ca860273a0 | ||
|
95062df343 | ||
|
adfe7b2af2 | ||
|
e36f8f53ab | ||
|
dc75a9beb3 | ||
|
0c49ddc65b | ||
|
74cece5796 | ||
|
99dacb6038 | ||
|
04ec8fe2d3 | ||
|
b79f918d51 | ||
|
0bc7864cb3 | ||
|
3f32b3409f | ||
|
74eeaccdc6 | ||
|
7c8de2b87f | ||
|
f6ac72f08d | ||
|
19052ba48b | ||
|
10b92705e8 | ||
|
46c0693829 | ||
|
993f4a7dcb | ||
|
79872b47df | ||
|
c216172d44 | ||
|
409439a916 | ||
|
28f5cdd03a | ||
|
0ebb797740 | ||
|
00f4c0b02c | ||
|
4b869a6015 | ||
|
df8721670a | ||
|
11bde88a02 | ||
|
69b88c9238 | ||
|
15418bfb8d | ||
|
852be15a3e | ||
|
14545fd983 | ||
|
f5791d1413 | ||
|
eebcd48447 | ||
|
48f1d3efc6 | ||
|
62f4f2bd24 | ||
|
5bbb9713d4 | ||
|
74e8dd1eb2 | ||
|
54afbd395f | ||
|
7275a530a2 | ||
|
4ee1efd4b9 | ||
|
48d17c122d | ||
|
d7aa432cca | ||
|
24331d75b0 | ||
|
b1a4f1b581 | ||
|
f389b060c8 | ||
|
a731a96b58 | ||
|
0b04980332 | ||
|
2eac639200 | ||
|
83f2880dd9 | ||
|
714e9216c1 | ||
|
cc8a115974 | ||
|
75f549170d | ||
|
7d5581bf73 | ||
|
2917eea50f | ||
|
086aa0df8b | ||
|
c49200f82b | ||
|
e8c4382901 | ||
|
c5edc1a0c3 | ||
|
58c05d57f2 | ||
|
55071c71cb | ||
|
64efd47cdc | ||
|
7a18d0b2bb | ||
|
698f78ee9a | ||
|
5bb1247f37 | ||
|
d01bbcad98 | ||
|
d11929d231 | ||
|
837aec4de7 | ||
|
f788839665 | ||
|
de8231858a | ||
|
70cb96dc96 | ||
|
febb68e702 | ||
|
38579d9dc2 | ||
|
4f92add78d | ||
|
bde831299d | ||
|
c77b979a97 | ||
|
d45c8b194b | ||
|
c9427a77f2 | ||
|
ca6db74a73 | ||
|
257050eabc | ||
|
68c24e2425 | ||
|
822a16fc53 | ||
|
e2682c570d | ||
|
8b3278b60f | ||
|
0abb9a13e5 | ||
|
927d2296eb | ||
|
2ed69f9484 | ||
|
999f2f8e14 | ||
|
e3adc0684f | ||
|
da2254089c | ||
|
501e0d12e4 | ||
|
2978ae5d1e | ||
|
bcf936d103 | ||
|
3f538ccf95 | ||
|
709d26735b | ||
|
c1a59f5536 | ||
|
990a442c87 | ||
|
46c38fad28 | ||
|
cb1e41d90d | ||
|
e1009f76e9 | ||
|
505e45d87d | ||
|
24cd0a23a5 | ||
|
4cde0c058e | ||
|
4b9a38b2a7 | ||
|
e49a8e3905 | ||
|
8ebcba998b | ||
|
e841fb6fbb | ||
|
8e16c64989 | ||
|
ee69ce1972 | ||
|
e30af9adbe | ||
|
cd0d74f832 | ||
|
a5e9ab3273 | ||
|
29ad4dbddf | ||
|
5e8bec0263 | ||
|
b769738f08 | ||
|
f38f3acf9b | ||
|
0fac23cc4b | ||
|
05648fc1f4 | ||
|
e5ccdccbc7 | ||
|
84a5c6f893 | ||
|
a2bb0c0a4d | ||
|
59c39efb80 | ||
|
4e3eb33d01 | ||
|
7fcaa4a151 | ||
|
d2864a22d9 | ||
|
2e1c284565 | ||
|
7c770dd59c | ||
|
2bdce8b2fc | ||
|
a407a56051 | ||
|
a99d66b68f | ||
|
7e7eed290f | ||
|
c0ef41cb71 | ||
|
b1ac997f8a | ||
|
9b7cd5831a | ||
|
34432f9521 | ||
|
44bc441f1b | ||
|
d4ce53646d | ||
|
b8d40a90ab | ||
|
1578fd093b | ||
|
1109ca1bc5 | ||
|
458ab8ef3f | ||
|
3f4d007f0e | ||
|
de5a5345b6 | ||
|
a49bda5e4c | ||
|
c56b898379 | ||
|
4c9065ce68 | ||
|
9ab95efb08 | ||
|
dd97565dd5 | ||
|
4a8064f24f | ||
|
3c8a2a1313 | ||
|
ee037e57fa | ||
|
66055e706a | ||
|
9bc8342a32 | ||
|
2e9bb1e1fd | ||
|
d81e584e24 | ||
|
a5c684b050 | ||
|
faee93fe43 | ||
|
8ec2cfe17f | ||
|
f4bfc8e5f3 | ||
|
874ec0fa49 | ||
|
e1e0cb5030 | ||
|
2b3be8a5d2 | ||
|
81a9c5011b | ||
|
0ee7e649a7 | ||
|
00c3212950 | ||
|
762d20213a | ||
|
13fbc9667d | ||
|
4830750a1d | ||
|
bfc25b115a | ||
|
27ea14f8f2 | ||
|
5f64eff8d3 | ||
|
774e4a514c | ||
|
16fd012e9b | ||
|
c2f0ee7928 | ||
|
29062ff883 | ||
|
d075522f75 | ||
|
96b93ac841 | ||
|
2a2e5808f5 | ||
|
bd074cf9fc | ||
|
11ac1631f0 | ||
|
d5e8330c7f | ||
|
48af135fd6 | ||
|
0994a8bbae | ||
|
25ac2ab5e5 | ||
|
97989f8de1 | ||
|
e3b4d29a43 | ||
|
ef4be72b7f | ||
|
9c53888c30 | ||
|
c1ed270aa6 | ||
|
8d85c3ed8f | ||
|
680463a5f9 | ||
|
aa56724bef | ||
|
5c65a4b892 | ||
|
b849a63cc6 | ||
|
379e97107f | ||
|
2d99fe877c | ||
|
47f188a277 | ||
|
6db5a0e801 | ||
|
ea991cd8f8 | ||
|
0e2d279de1 | ||
|
84b5bf339e | ||
|
8620f2ff4a | ||
|
02032f5ec0 | ||
|
c7e713048f | ||
|
242dff05d0 | ||
|
6211b183f6 | ||
|
e2e6e58900 | ||
|
b864f4ddbb | ||
|
90389bbac8 | ||
|
f2cf046733 | ||
|
7a229f9525 | ||
|
852f730734 | ||
|
2bc23747f0 | ||
|
4ad209c115 | ||
|
1895e19fa1 | ||
|
3d1f1fbcc6 | ||
|
db95bdb5fe | ||
|
0d006c76f2 | ||
|
f3b33e1b4a | ||
|
df0ad6b633 | ||
|
fa6846d2a3 | ||
|
dc740f6380 | ||
|
8e46a70baf | ||
|
976503cd29 | ||
|
32ec220372 | ||
|
72061d9fe2 | ||
|
3962a7cf1e | ||
|
df42cfa537 | ||
|
ff56b77fa6 | ||
|
5dffceebad | ||
|
5c2f60561c | ||
|
792435da91 | ||
|
6894f678c7 | ||
|
2fa3285aca | ||
|
c59487a34c | ||
|
fa60c6c904 | ||
|
b0cbfc52d3 | ||
|
9f60802a15 | ||
|
16f880e56b | ||
|
96f405eb43 | ||
|
748ff7f8e4 | ||
|
37599d416f | ||
|
b2b8356033 | ||
|
89fab03d26 | ||
|
56338a4cfb | ||
|
60586e454a | ||
|
8eedf93090 | ||
|
3dd1667ac1 | ||
|
95186c5fe6 | ||
|
16999ce19b | ||
|
4ed400910f | ||
|
6bfb1cc861 | ||
|
01e159d5e1 | ||
|
1d31518d38 | ||
|
3822275ac0 | ||
|
cd026e0edb | ||
|
40785d7f26 | ||
|
c35cfc8181 | ||
|
77945c8236 | ||
|
cb1e348665 | ||
|
f3215190c5 | ||
|
644647fa38 | ||
|
95c355d586 | ||
|
9cc0acdaeb | ||
|
54afcbee06 | ||
|
254d29558a | ||
|
2a6ddb4958 | ||
|
0842ecbc79 | ||
|
5bc430f47f | ||
|
6e10b024ef | ||
|
1e420d5448 | ||
|
348199ba8f | ||
|
dc5e64593a | ||
|
1431e362c9 | ||
|
7695393a74 | ||
|
e3db9d24ef | ||
|
fbeb4e4553 | ||
|
2230afc2af | ||
|
9a049302b1 | ||
|
c7d16ecdcf | ||
|
f80b604f39 | ||
|
1dddf901b0 | ||
|
59e62a9bdb | ||
|
6e3ce82dcf | ||
|
24fe461043 | ||
|
9e851e4a57 | ||
|
776164e7d9 | ||
|
8060171753 | ||
|
17df07188e | ||
|
329b7498f8 | ||
|
9da434308e | ||
|
720295b64f | ||
|
f72c5d8673 | ||
|
5084422c85 | ||
|
b58a6b1a1a | ||
|
6f1625a55c | ||
|
e0b9bc1539 | ||
|
b45b703143 | ||
|
0b2dde0bff | ||
|
93e6831403 | ||
|
17d910ea88 | ||
|
759ebe2298 | ||
|
deda6da6f5 | ||
|
d47094ecad | ||
|
012c2ad344 | ||
|
b098ae202d | ||
|
23864deffe | ||
|
d5545e01b0 | ||
|
19005d3199 | ||
|
d8574e0d15 | ||
|
b47dad5659 | ||
|
cb7e4bddf5 | ||
|
2d57ac6466 | ||
|
22d9d716ee | ||
|
9364331a1d | ||
|
0e4440a9e8 | ||
|
7610fe3fa7 | ||
|
d150e45fce | ||
|
21b9b1ab9d | ||
|
a8db378010 | ||
|
a98d4fee4b | ||
|
ea6e80f056 | ||
|
0bc59325fb | ||
|
080b3f9a50 | ||
|
d36b664c6b | ||
|
fe9aa78feb | ||
|
56a2f0da49 | ||
|
43e0502c57 | ||
|
5b886ec767 | ||
|
f2c1770459 | ||
|
7334e6b5b8 | ||
|
73c5429058 | ||
|
c279511ffe | ||
|
546da074d1 | ||
|
5cea3f3cf0 | ||
|
becf688d8d | ||
|
0eef3a5e4a | ||
|
a8a8890117 | ||
|
d59077edca | ||
|
3fc1a723ac | ||
|
44de57fffa | ||
|
f083dcb687 | ||
|
b02580d654 | ||
|
09b61af22f | ||
|
003a8fe4e4 | ||
|
07dc83c9dd | ||
|
e149be864d | ||
|
b5cd09d91a | ||
|
ce443ce708 | ||
|
0136bb7f87 | ||
|
36f19916b5 | ||
|
ddadf4c3d4 | ||
|
53ab9ac7b2 | ||
|
124a9cb6a6 | ||
|
9f8bd5c87a | ||
|
7f2aa2a9f9 | ||
|
c6de89e145 | ||
|
747ce883ce | ||
|
4370e2e406 | ||
|
fb7c78eadb | ||
|
66dfd54632 | ||
|
0f2c4b6897 | ||
|
6385e29cc3 | ||
|
f89105d49d | ||
|
3a925afeea | ||
|
594ee50a1f | ||
|
594dbfbaab | ||
|
3c763c953a | ||
|
4e1165da23 | ||
|
b608ba9444 | ||
|
6000625985 | ||
|
b1197cbefa | ||
|
05ac4a679c | ||
|
a2cee5314e | ||
|
d1e4069451 | ||
|
718cb0ca6a | ||
|
c09c92df2d | ||
|
4a93104c62 | ||
|
3a2160dd4c | ||
|
383acdb052 | ||
|
be7bc3a437 | ||
|
13ca024dc6 | ||
|
75ed8b93e1 | ||
|
f62ad4a3fb | ||
|
6666539554 | ||
|
923d3ca994 | ||
|
8230c35d49 | ||
|
e140d86846 | ||
|
d87be1f4f0 | ||
|
6fa3dcd339 | ||
|
32684bd350 | ||
|
b02456cb6a | ||
|
a739e9d887 | ||
|
34400156b8 | ||
|
35e4760942 | ||
|
7b13cd9aef | ||
|
18b455e45b | ||
|
152d5f47e5 | ||
|
b39fe54faa | ||
|
d6660b2a9a | ||
|
9b64de803d | ||
|
a1f73c951a | ||
|
c6fe85458b | ||
|
5db44c9b6b | ||
|
4fec053546 | ||
|
2c62d5ac87 | ||
|
1fb47cda4c | ||
|
081f8082d1 | ||
|
b91b0fdc6f | ||
|
97285e4f1a | ||
|
9665b15f96 | ||
|
e9ca950067 | ||
|
30f220f73f | ||
|
4568a4f4c2 | ||
|
7ef481dad3 | ||
|
6e3d8157d0 | ||
|
8dcd3043ad | ||
|
ab17b9a829 | ||
|
06ae4a5436 | ||
|
5f39d8fb26 | ||
|
21fb9c9f1c | ||
|
6980bbfea4 | ||
|
605868c13f | ||
|
e45eae178a | ||
|
6fecdb1e23 | ||
|
72f52f7bcf | ||
|
d55565f54d | ||
|
4f4083f9f8 | ||
|
da5be99197 | ||
|
c112a98773 | ||
|
ec77dbcfa0 | ||
|
a6abb22cfc | ||
|
328068155b | ||
|
796f03f8fa | ||
|
e03a09da45 | ||
|
72877423c5 | ||
|
d96923d29a | ||
|
6fdb01308b | ||
|
f618318d8d | ||
|
33c7c5247e | ||
|
ba4752dee6 | ||
|
6ca9cfc144 | ||
|
7f252a5866 | ||
|
22941101af | ||
|
d3d76e9d4e | ||
|
ea42f63cc8 | ||
|
64f9a54c1c | ||
|
9f3a2e1801 | ||
|
8e7bb4f3fc | ||
|
9c60d5c670 | ||
|
bd64e19af4 | ||
|
9ddcd6dff6 | ||
|
0bc931fef0 | ||
|
5bbf288053 | ||
|
fc2a2ddef5 | ||
|
2158017994 | ||
|
f0627cbc31 | ||
|
1ff003f917 | ||
|
aefb2e2dee | ||
|
3d6971b08c | ||
|
7ab055af70 | ||
|
f181ee9158 | ||
|
436baa148f | ||
|
d6ec612393 | ||
|
bccbfb1624 | ||
|
3a3d14fddc | ||
|
4983f2d695 | ||
|
368911b58e | ||
|
f5bdedf789 | ||
|
91ed13d5c2 | ||
|
85f8450a85 | ||
|
b47e46cef5 | ||
|
7a82fed92a | ||
|
771348e6e0 | ||
|
49bacc4ea1 | ||
|
786987c969 | ||
|
e50f3b1967 | ||
|
e528984a3a | ||
|
befc9aa86e | ||
|
cd3c316881 | ||
|
93425108c5 | ||
|
45ce7d52b0 | ||
|
3c294b33a8 | ||
|
1b0f59937d | ||
|
f33a9bf2ae | ||
|
355173fd6f | ||
|
73098498b1 | ||
|
884a69aef9 | ||
|
a71c51a174 | ||
|
b4eb6e9893 | ||
|
019d694d2a | ||
|
117052fb91 | ||
|
35fb06c345 | ||
|
72ed766f26 | ||
|
63356b5994 | ||
|
c30944dede | ||
|
9daf031928 | ||
|
91f2b2bd6d | ||
|
e4467ec4ad | ||
|
969cfc78be | ||
|
9d3cd05e33 | ||
|
e7ce24966b | ||
|
ed4a55b4b6 | ||
|
f5e6e51817 | ||
|
a461674f9d | ||
|
3d50f2d90d | ||
|
4c5cb34b5b | ||
|
f50b2b51b8 | ||
|
7067173a5a | ||
|
3dd0b60606 | ||
|
4b7498c67e | ||
|
edbe6ee21a | ||
|
27b7138c58 | ||
|
f043ec32d1 | ||
|
0fe6d54eb3 | ||
|
74694a423b | ||
|
9804743747 | ||
|
67d791c925 | ||
|
0ee35c3505 | ||
|
53dd1935e0 | ||
|
f9249ebce2 | ||
|
c72c1f4080 | ||
|
b5804d517f | ||
|
83ecdc0142 | ||
|
532949526f | ||
|
3233a7e258 | ||
|
a32b6dd03e | ||
|
b81326b7fb | ||
|
215306e08a | ||
|
a165b654f1 | ||
|
cb06f2cd6c | ||
|
684e77d54b | ||
|
073f8dfebf | ||
|
67fdefbf3c | ||
|
283282f834 | ||
|
f01eba136f | ||
|
7afa2d129b | ||
|
7dad52f2e2 | ||
|
4e218746a8 | ||
|
4669430b05 | ||
|
e09f25a304 | ||
|
1a5679970b | ||
|
6c17906eaf | ||
|
79d804a331 | ||
|
99abdb007c | ||
|
a0c5ddbc2e | ||
|
11ac2dcceb | ||
|
87b37a4153 | ||
|
b012be0541 | ||
|
494c173001 | ||
|
95be4d0436 | ||
|
8cca22abc4 | ||
|
7d8060ab71 | ||
|
d8c307eef4 | ||
|
3917f616a9 | ||
|
be10044186 | ||
|
99b2e9b9e7 | ||
|
e796e4dcb7 | ||
|
ba931ec55b | ||
|
541e9b515f | ||
|
fddc818572 | ||
|
7a83eac12a | ||
|
89f22d785f | ||
|
81fcfd68b4 | ||
|
3df2102f78 | ||
|
9888c01a9e | ||
|
981f94296e | ||
|
425420439b | ||
|
3b606cddb8 | ||
|
d1fb2e2f32 | ||
|
ed2c5aaeca | ||
|
749746b8b5 | ||
|
b55328ec30 | ||
|
4e41fc584a | ||
|
feb5e039ae | ||
|
da85c20112 | ||
|
3ee68bb2ed | ||
|
f5fae3cf95 | ||
|
7a71306ebc | ||
|
9ed953a3ed | ||
|
aa49b221fe | ||
|
97eb25340c | ||
|
e401050ca6 | ||
|
83b07fa348 | ||
|
99ce4942fb | ||
|
10ca260fc5 | ||
|
ce672a4aeb | ||
|
1be3ab1b55 | ||
|
b70ed5a76e | ||
|
880c4fdf6b | ||
|
1968c87a69 | ||
|
26e110fd03 | ||
|
3dcd9d1b2c | ||
|
769d164159 | ||
|
fe05a2563d | ||
|
2134e7b152 | ||
|
a66f19cc5d | ||
|
c57d192abe | ||
|
fbe5a8d0c4 | ||
|
c8735e5837 | ||
|
bda18a2150 | ||
|
2a629759fa | ||
|
bba76c7ab6 | ||
|
5b639f10f8 | ||
|
24ddb6e2bf | ||
|
e8e3d9ef1c | ||
|
b9fba5650d | ||
|
4f6bc0cd7a | ||
|
175d3d49e0 | ||
|
648c4d28e4 | ||
|
9f07e48be4 | ||
|
2d5df1d5f1 | ||
|
9cc0419417 | ||
|
0adbfc60cb | ||
|
7d404433fc | ||
|
b726970448 | ||
|
91e17b37e5 | ||
|
82cac08642 | ||
|
1fa44dea70 | ||
|
3d14b72eee | ||
|
8bfca857fc | ||
|
7f60fe2faa | ||
|
a5b0f26f46 | ||
|
6bcccc91b9 | ||
|
c351587783 | ||
|
50a54e0610 | ||
|
cd08c0db4f | ||
|
e713f28fe6 | ||
|
9e928cf9c5 | ||
|
86b488a07d | ||
|
93bb3b89be | ||
|
41e5daacdf | ||
|
ecc3264ac1 | ||
|
d78fbcd670 | ||
|
6cf7512933 | ||
|
d400347a3e | ||
|
a1df30f4a7 | ||
|
e6b95a43d1 | ||
|
6e5061250c | ||
|
ca6ef9b06b | ||
|
d428d1ddf7 | ||
|
260fa83c3f | ||
|
1daa8c5f5a | ||
|
40758a83d5 | ||
|
6c8ecc6dbb | ||
|
411d7eca6c | ||
|
2ac3755120 | ||
|
193fcde123 | ||
|
26b1ea401a | ||
|
1319985047 | ||
|
bc6cec45fa | ||
|
e717312de7 | ||
|
65249b60f2 | ||
|
7f665daaf3 | ||
|
722bb67fb9 | ||
|
8581ecc106 | ||
|
2e6d80295b | ||
|
f41a27f331 | ||
|
d228805c6d | ||
|
397ce507d6 | ||
|
35e12cf056 | ||
|
5db3123ef0 | ||
|
ef9ebeb80c | ||
|
48dcbe6c3a | ||
|
f051419c2f | ||
|
eeb40b15b0 | ||
|
c58b1f5219 | ||
|
968d2ce051 | ||
|
b564817962 | ||
|
5d75452c8e | ||
|
1e2273e4f0 | ||
|
e9ece4893b | ||
|
4dd7f73456 | ||
|
7d1e8cbfa2 | ||
|
e9601fac72 | ||
|
15d186cc25 | ||
|
19bf587dc9 | ||
|
943643c7f1 | ||
|
e2bab6268c | ||
|
dee8c90bb3 | ||
|
9e2060c1bc | ||
|
23071f2cc5 | ||
|
4beab2b1fd | ||
|
72b4f8e869 | ||
|
2d69e85340 | ||
|
79901e85ee | ||
|
815eec071e | ||
|
bd6f8206a7 | ||
|
79b58012cb | ||
|
ba097c736e | ||
|
c174e18b39 | ||
|
b484629010 | ||
|
3ad5f8977e | ||
|
dddcd4f2be | ||
|
462e76c93d | ||
|
49b0a0778e | ||
|
cd25002bdf | ||
|
68bc0c44ce | ||
|
308f3a6d60 | ||
|
9e3b6ec419 | ||
|
44cd4129bb | ||
|
9e8e2a97d1 | ||
|
a481413153 | ||
|
eb9b9d8f69 | ||
|
7771b27b55 | ||
|
daca9d71e7 | ||
|
305f53cb03 | ||
|
e624671da1 | ||
b579379582 | |||
780971f19c | |||
|
8a36999121 | ||
|
eb9268be49 | ||
|
2a2124a251 | ||
|
c9f0907d59 | ||
|
9984daee1f | ||
|
50176ba5c3 | ||
|
f27cf520d3 | ||
|
3a35ce8668 | ||
|
bfac39f954 | ||
|
f7e07e366f | ||
|
c1571e11e6 | ||
|
1c619a309e | ||
|
626974595b | ||
|
dae0bd54a0 | ||
|
e27390f289 | ||
|
00ed9c2f3e | ||
|
de7375a970 | ||
|
d7bad319ab | ||
|
5398d61561 | ||
8b9b1f7acc | |||
fa372387aa | |||
|
d315c75926 | ||
|
fa0885b8dd | ||
|
d8fdd3aaf9 | ||
|
aeecc4b140 | ||
|
eb540dfaa3 | ||
|
4fd1ebefa6 | ||
|
1d912568ac | ||
|
266055ff48 | ||
|
d32cc1e0d9 | ||
|
2067422f3c | ||
|
2abab8d25f | ||
|
b7d4885ce6 | ||
|
e47c6da569 | ||
|
9c657ac571 | ||
|
f0fbf20f5b | ||
|
a41919774c | ||
|
611750f44c | ||
|
8c1c79ad6c | ||
|
27edb05d5e | ||
|
25bd0a1ad5 | ||
|
cfb2226e6c | ||
|
331269778b | ||
|
a977d7f6d6 | ||
|
af6e71d8e6 | ||
|
2b0d5d0ef5 | ||
|
422d380a6d | ||
|
a4ffddd30b | ||
|
55f6a57c81 | ||
|
935a4bb34f | ||
|
d86830bb1c | ||
|
821fd3cdf0 | ||
|
80e3e354d8 | ||
|
aed259e901 | ||
|
5e97361045 | ||
|
41e99aa82f | ||
|
67ee95f81e | ||
|
52343ea5cf | ||
|
798ee0b6c9 | ||
|
24ab288e45 | ||
|
11c9831159 | ||
|
e597c21aea | ||
|
6662ef35de | ||
|
1e1a145847 | ||
|
4f0ba30076 | ||
|
822a5604e0 | ||
|
1bfff235f3 | ||
|
07d679c4ab | ||
|
f437eb842c | ||
|
3fb0265d2d | ||
|
21325a014d | ||
|
402e3d97ea | ||
|
2a02ae0a03 | ||
|
99e4f612b2 | ||
|
861a7afc95 | ||
|
79252c4a5d | ||
|
e1216befa9 | ||
|
77207342a2 | ||
|
3da846cdf9 | ||
|
51afec1856 | ||
|
52e624bb88 | ||
|
f39d760cef | ||
|
0beea8e6c6 | ||
|
1f58915eb6 | ||
|
22724f2446 | ||
|
c9c1aa1452 | ||
|
a44bb963d2 | ||
|
dd925fd0f3 | ||
|
adc508e734 | ||
|
36ebb1b59d | ||
|
50e9f463ed | ||
|
566d84b957 | ||
|
22550a60e9 | ||
|
541d2f49c8 | ||
|
e81db2e643 | ||
|
3aa67dcc0b | ||
|
7b6ad312c0 | ||
|
bfe833af7f | ||
|
477e8a5778 | ||
|
be7905cebf | ||
|
df364371a1 | ||
|
31d297172c | ||
|
f5a777004c | ||
|
6316fdb4a7 | ||
|
694e6dbe6b | ||
|
260bae4a93 | ||
|
d748e246cd | ||
|
4bebc8a7a2 | ||
|
eacdcee812 | ||
|
4a2fe900cd | ||
|
bebc112148 | ||
|
baa8582df7 | ||
|
ef954569f4 | ||
|
9aaea50f59 | ||
|
147ea64483 | ||
|
2b399ac1df | ||
|
c89ce87541 | ||
|
921f4b577d | ||
|
b3353dd4dc | ||
|
d66787aa66 | ||
|
2d6fa93906 | ||
|
20eea58ac9 | ||
|
0ec4218636 | ||
|
d4dfba48d8 | ||
|
a54b879c2d | ||
|
233cf1026a | ||
|
377b83e02d | ||
|
ed7e6e4d4b | ||
|
ab724b0893 | ||
|
4f769926d7 | ||
|
c6812dd672 | ||
|
0af36e19f8 | ||
|
2dbc92b0cd | ||
|
82af0f10b0 | ||
|
f5a75c0af3 | ||
|
506a6dd4c7 | ||
|
83783ad34b | ||
|
49ad1637f9 | ||
|
6167d8c416 | ||
|
c4d9101302 | ||
|
3fd79f89a8 | ||
|
1d1574b426 | ||
|
83e8136b0e | ||
|
0307cbb201 | ||
|
785bc539d6 | ||
|
1ee0dd5ca8 | ||
|
e64f89142e | ||
|
0348826994 | ||
|
93acdeda20 | ||
|
07a11d792c | ||
|
e8da59f4a5 | ||
|
2154457be3 | ||
|
2b99b48258 | ||
|
07d7f908c9 | ||
|
08f35169d1 | ||
|
d7fd30f8e6 | ||
|
fbf2d480a3 | ||
|
2b99267f2d | ||
|
eaae72d678 | ||
|
60484ef426 | ||
|
6f637afdbd | ||
|
76cefca4d4 | ||
|
ebcf2bc387 | ||
|
535feb29bf | ||
|
07a6f1c584 | ||
|
14fe16dc06 | ||
|
ba7f03610c | ||
|
cc9c2c878c | ||
|
5489c54f10 | ||
|
0cae2bb84d | ||
|
d1f0a9224a | ||
|
702c7fa6d4 | ||
|
5b85d96e06 | ||
|
f35db6540b | ||
|
6f6d617eba | ||
|
84bd1ccfad | ||
|
88067bd217 | ||
|
d7ef8f3280 | ||
|
446c5f00ab | ||
|
2032586352 | ||
|
19794bde23 | ||
|
d291a83cc9 | ||
|
be1664ec85 | ||
|
200adf272c | ||
|
85ceb1de47 | ||
|
bcf17fe30b | ||
|
fea97f3713 | ||
|
d31bdc99f8 | ||
|
425d5f61fc | ||
|
060f644e09 | ||
|
ba0af5d0af | ||
|
9c8b01c2a5 | ||
|
94652bbf44 | ||
|
42d6b8a9ab | ||
|
56bb28373b | ||
|
e5e95b81b4 | ||
|
b547f1f1ee | ||
|
3f9e1c8e19 | ||
|
df2c184bc0 | ||
|
2e869c1ade | ||
|
f3b97e44e5 | ||
|
5fa9eb68b1 | ||
|
819f2af518 | ||
|
346cd6cfac | ||
|
55af776df9 | ||
|
6d599441a9 | ||
|
6a63ece91f | ||
|
63e7807f0d | ||
|
0a37719029 | ||
|
cf7744ab51 | ||
|
3d2fea6bb8 | ||
|
0e98010906 | ||
|
3dc8663c3e | ||
|
4a8284fa6d | ||
|
b1a9c28a97 | ||
|
e6b186026d | ||
|
18cd7c11f1 | ||
|
b4bfa0856f | ||
|
c284c78034 | ||
|
08e782860d | ||
|
fbb167983d | ||
|
d653b038c5 | ||
|
bd2f5f8c1b | ||
|
b940f3c49e | ||
|
f287b9126e | ||
|
6eba531c89 | ||
|
ae24fa17d5 | ||
|
e484728c3d | ||
|
38468e06d9 | ||
|
34ba8bd298 | ||
|
97bddd70ec | ||
|
c571d8336e | ||
|
c68a7e8c0c | ||
|
012dbe6419 | ||
|
4a2805d43e | ||
|
a04ffcd6ce | ||
|
9583804890 | ||
|
38a3ffc19f | ||
|
8250d34597 | ||
|
c15123e5ea | ||
|
afff01d6d2 | ||
|
aaf9c2c931 | ||
|
6822cfabf1 | ||
|
41bddebda2 | ||
|
ccce64d6cb | ||
|
89bbafb44c | ||
|
5c7067b22b | ||
|
800060a926 | ||
|
cf69c8b375 | ||
|
2401abedb5 | ||
|
6d9fe639d6 | ||
|
e4a64e8ad6 | ||
|
16925ebf77 | ||
|
0bc78ad26e | ||
|
184b1cbdd2 | ||
|
cd8a5552bb | ||
|
a54607a57d | ||
|
90665f0294 | ||
|
6a03e49b63 | ||
|
7e15b5fb7a | ||
|
3961a2067b | ||
|
729a6a7113 | ||
|
41eb62bc2b | ||
|
e7662d3c77 | ||
|
ea276fbe73 | ||
|
df21729ba0 | ||
|
4fe5625597 | ||
|
73a7ef988f | ||
|
1bd32b4e36 | ||
|
7bf0a5f5cc | ||
|
81f189b385 | ||
|
c0dfb1bec6 | ||
|
5de0cee025 | ||
|
7c4a76cc89 | ||
|
7a30c92651 | ||
|
f55ca90c35 | ||
|
a46372094c | ||
|
4f739d8672 | ||
|
69e91e89f5 | ||
|
9ac3da618d | ||
|
1cc776622a | ||
|
a2017a3546 | ||
|
53de80dfee | ||
|
9290ab9487 | ||
|
4340cf7569 | ||
|
4de39d5850 | ||
|
4fc044c595 | ||
|
096c3a435a | ||
|
c806beddcd | ||
|
0625e7f3e0 | ||
|
8519364e77 | ||
|
39ea05a04a | ||
|
547e222f5f | ||
|
52e90fb320 | ||
|
5d2d4f0fa6 | ||
|
8c2e4e21ea | ||
|
f6aaf493c0 | ||
|
52ea2ff579 | ||
|
75fbcb985c | ||
|
26644c1677 | ||
|
5e64d0515e | ||
|
475c72597e | ||
|
8cb7df16d0 | ||
|
f4736bd1b9 | ||
|
3dc1707e01 | ||
|
497bac8453 | ||
|
d89c0fc414 | ||
|
54116686c8 | ||
|
20999c4ef4 | ||
|
2b3a0f19b9 | ||
|
f44e466194 | ||
|
b12cea5ead | ||
|
4923c52f3b | ||
|
39f40a86f7 | ||
|
ea4116c207 | ||
|
807b1084b0 | ||
|
23fddaedf1 | ||
|
88b078ebce | ||
|
2a9098975d | ||
|
6cdc68e1a8 | ||
|
d0b125064f | ||
|
3e74982ec4 | ||
|
59edfe26cc | ||
|
bac6628aea | ||
|
d0b45de175 | ||
|
f315685deb | ||
|
f8eda4aac5 | ||
|
244a349b7d | ||
|
358be2aac6 | ||
|
44e8ac7e9a | ||
|
cf9ba47b69 | ||
|
cf2c260e79 | ||
|
c46e0a367a | ||
|
3f5d44e5fb | ||
|
df8e3e913d | ||
|
f4faf9f85c | ||
|
cbb6ba6f52 | ||
|
2131a774fb | ||
|
4a1fa55475 | ||
|
8d855e3108 | ||
|
693545d3c6 | ||
|
89ead286ad | ||
|
ef1e4124de | ||
|
7c27ba4000 | ||
|
3b63c2928e | ||
|
e47f38691d | ||
|
f87fb0f579 | ||
|
acef56ee7c | ||
|
a9105f82da | ||
|
7f2118346c | ||
|
e227a75337 | ||
|
8eddc2cf88 | ||
|
8acca91dd5 | ||
|
70b4fd5f6e | ||
|
df22082f3e | ||
|
3ecc2cbad7 | ||
|
52e69bb2a3 | ||
|
12e0b8a30f | ||
|
9b4a92077f | ||
|
0792bf5445 | ||
|
312e71d3ec | ||
|
3f5f86db99 | ||
|
1df9f84c20 | ||
|
f24ec89408 | ||
|
f31454c692 | ||
|
f9ddeb80fb | ||
|
ea76805a90 | ||
|
bd4fad3c58 | ||
|
aca2ed612e | ||
|
7ecf2e1da0 | ||
|
d2ccc21f91 | ||
|
88e9ae8214 | ||
|
8a1dfb0612 | ||
|
676fab7871 | ||
|
555ae867ea | ||
|
4de78f58e0 | ||
|
44f90c7b0b | ||
|
a9e36aaacb | ||
|
cd89efa1e3 | ||
|
4ca831a5b3 | ||
|
38cb9d3c9f | ||
|
46feb40908 | ||
|
348af2ab7f | ||
|
779851d7af | ||
|
895728e4b0 | ||
|
358dd63b92 | ||
|
6925420e97 | ||
|
76aeac2989 | ||
|
b383d11f51 | ||
|
9621caf661 | ||
|
e280820e9c | ||
|
e606090f17 | ||
|
6e136ff8b5 | ||
|
d4b6fb2b49 | ||
|
62af1e72d6 | ||
|
d58ca5743d | ||
|
eba3c70c9b | ||
|
6113836e29 | ||
|
034b9ad9f0 | ||
|
f958d291a0 | ||
|
463d8bf86e | ||
|
cdfef60414 | ||
|
a8cbb0386c | ||
|
8580281fb4 | ||
|
db55859855 | ||
|
7940d69d5a | ||
|
7885151220 | ||
|
dee7c58449 | ||
|
76bc409f68 | ||
|
ba61880e50 | ||
|
98449b9cfd | ||
|
f4284e1d3a | ||
|
5d8d2e80a5 | ||
|
2f83e5a3d5 | ||
|
f4237dc469 | ||
|
14922c1479 | ||
|
32e9b0a838 | ||
|
a05c3a20ca | ||
|
739dcc2405 | ||
|
9d5bf806df | ||
|
60c77eb355 | ||
|
0c667b13ae | ||
|
5dd24e1c9e | ||
|
4c66162e7e | ||
|
ae098f8cc7 | ||
|
10430d9cb6 | ||
|
6a43812977 | ||
|
b0ed979ed1 | ||
|
e09811bd95 | ||
|
12987068ed | ||
|
8235f4c7b5 | ||
|
2b808d998e | ||
|
98b840190d | ||
|
74145dc520 | ||
|
4a7c6a861e | ||
|
6edeeef9a9 | ||
|
fd980176bc | ||
|
0f8358b96a | ||
|
35b83950d4 | ||
|
5ef50766b1 | ||
|
4d7281e9e0 | ||
|
3fcdba9c1f | ||
|
4ef70e7d0f | ||
|
9c0bc9dcd0 | ||
|
393c92bd03 | ||
|
81c319bdf8 | ||
|
327dfbb4c2 | ||
|
773a29d1f4 | ||
|
fa25ffbc83 | ||
|
9d910db040 | ||
|
c924975d40 | ||
|
c5624ae33d | ||
|
120d79d580 | ||
|
9e4378d4db | ||
|
20ffd2ad0f | ||
|
dc52cd042b | ||
|
7bf745edf7 | ||
|
d8b64e9a19 | ||
|
8226007a94 | ||
|
b5d9b82bdd | ||
|
14fbe3fecb | ||
|
89b3e12ace | ||
|
10bb228ce9 | ||
|
41f086e2c9 | ||
|
cc3106e425 | ||
|
4f2dd048c6 | ||
|
55e7696230 | ||
|
ae97339353 | ||
|
1893d9f55b | ||
|
d235653876 | ||
|
734e4684f7 | ||
|
75e254d8b4 | ||
|
e9fecc4d24 | ||
|
28a2efc6eb | ||
|
acbe0e6c59 | ||
|
e9e12500dc | ||
|
6bb0b6d08a | ||
|
de047c8939 | ||
|
029a4ea194 | ||
|
61f430bad8 | ||
|
b0a6b82829 | ||
|
0c622303d1 | ||
|
01c7222a79 | ||
|
3811c10995 | ||
|
7e8b1a06f9 | ||
|
d5082c1b6c | ||
|
ccf2423c42 | ||
|
09cc5d1bd1 | ||
|
e6dfaf8950 | ||
|
4eceacde97 | ||
|
b8dd858a0e | ||
|
77108e1a97 | ||
|
3c035b613b | ||
|
06c680b541 | ||
|
f4293b028a | ||
|
2fde7e2591 | ||
|
4f4c92e917 | ||
|
39238c4b8a | ||
|
b1ab0dabbe | ||
|
d81c479aa6 | ||
|
68065a611a | ||
|
9c7a4f0079 | ||
|
291a788438 | ||
|
d36f1d4b5e | ||
|
3564b69db8 | ||
|
c198b21587 | ||
|
c2c3014c2b | ||
|
bc1f71e742 | ||
|
8a58f5ba7c | ||
|
9e2e071609 | ||
|
08abaf56be | ||
|
cc197d7638 | ||
|
7c71e9e04f | ||
|
2d1facbf39 | ||
|
f5340e32a7 | ||
|
efb72f4751 | ||
|
796fd4ffbe | ||
|
7958c109ca | ||
faa0e2fd3e | |||
|
bea1015b44 | ||
|
a97ac84b2c | ||
|
a53690fbe5 | ||
|
bcad13dd28 | ||
|
24ff99ffaf | ||
|
f3bb1f8976 | ||
|
3a79806c0f | ||
|
c2b72b45d3 | ||
|
89da83dce4 | ||
|
ab843dff4c | ||
|
1ac2990cda | ||
|
d35041bd4c | ||
|
846a4c376a | ||
|
e2496536e9 | ||
|
5cf962fd8f | ||
|
ba075c1973 | ||
|
d577b07c6e | ||
|
7c9b76765f |
66
.devcontainer/Dockerfile
Normal file
66
.devcontainer/Dockerfile
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# Update the VARIANT arg in docker-compose.yml to pick an Elixir version: 1.9, 1.10, 1.10.4
|
||||||
|
ARG VARIANT="1.12.3"
|
||||||
|
FROM elixir:${VARIANT}
|
||||||
|
|
||||||
|
# This Dockerfile adds a non-root user with sudo access. Update the “remoteUser” property in
|
||||||
|
# devcontainer.json to use it. More info: https://aka.ms/vscode-remote/containers/non-root-user.
|
||||||
|
ARG USERNAME=vscode
|
||||||
|
ARG USER_UID=1000
|
||||||
|
ARG USER_GID=$USER_UID
|
||||||
|
|
||||||
|
# Options for common package install script
|
||||||
|
ARG INSTALL_ZSH="true"
|
||||||
|
ARG UPGRADE_PACKAGES="true"
|
||||||
|
ARG COMMON_SCRIPT_SOURCE="https://raw.githubusercontent.com/microsoft/vscode-dev-containers/v0.209.6/script-library/common-debian.sh"
|
||||||
|
ARG COMMON_SCRIPT_SHA="d35dd1711454156c9a59cc41ebe04fbff681ca0bd304f10fd5b13285d0de13b2"
|
||||||
|
|
||||||
|
# Optional Settings for Phoenix
|
||||||
|
ARG PHOENIX_VERSION="1.6.2"
|
||||||
|
|
||||||
|
# [Optional] Setup nodejs
|
||||||
|
ARG NODE_SCRIPT_SOURCE="https://raw.githubusercontent.com/microsoft/vscode-dev-containers/main/script-library/node-debian.sh"
|
||||||
|
ARG NODE_SCRIPT_SHA="dev-mode"
|
||||||
|
ARG NODE_VERSION="none"
|
||||||
|
ENV NVM_DIR=/usr/local/share/nvm
|
||||||
|
ENV NVM_SYMLINK_CURRENT=true
|
||||||
|
ENV PATH=${NVM_DIR}/current/bin:${PATH}
|
||||||
|
|
||||||
|
# [Optional, Choice] Node.js version: none, lts/*, 16, 14, 12, 10
|
||||||
|
ARG NODE_VERSION="none"
|
||||||
|
|
||||||
|
# Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies.
|
||||||
|
RUN apt-get update \
|
||||||
|
&& export DEBIAN_FRONTEND=noninteractive \
|
||||||
|
&& apt-get -y install --no-install-recommends curl ca-certificates 2>&1 \
|
||||||
|
&& curl -sSL ${COMMON_SCRIPT_SOURCE} -o /tmp/common-setup.sh \
|
||||||
|
&& ([ "${COMMON_SCRIPT_SHA}" = "dev-mode" ] || (echo "${COMMON_SCRIPT_SHA} */tmp/common-setup.sh" | sha256sum -c -)) \
|
||||||
|
&& /bin/bash /tmp/common-setup.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" \
|
||||||
|
#
|
||||||
|
# [Optional] Install Node.js for use with web applications
|
||||||
|
&& if [ "$NODE_VERSION" != "none" ]; then \
|
||||||
|
curl -sSL ${NODE_SCRIPT_SOURCE} -o /tmp/node-setup.sh \
|
||||||
|
&& ([ "${NODE_SCRIPT_SHA}" = "dev-mode" ] || (echo "${NODE_SCRIPT_SHA} */tmp/node-setup.sh" | sha256sum -c -)) \
|
||||||
|
&& /bin/bash /tmp/node-setup.sh "${NVM_DIR}" "${NODE_VERSION}" "${USERNAME}"; \
|
||||||
|
fi \
|
||||||
|
#
|
||||||
|
# Install dependencies
|
||||||
|
&& apt-get install -y build-essential \
|
||||||
|
#
|
||||||
|
# Clean up
|
||||||
|
&& apt-get autoremove -y \
|
||||||
|
&& apt-get clean -y \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/common-setup.sh /tmp/node-setup.sh
|
||||||
|
|
||||||
|
RUN su ${USERNAME} -c "mix local.hex --force \
|
||||||
|
&& mix local.rebar --force \
|
||||||
|
&& mix archive.install --force hex phx_new ${PHOENIX_VERSION}"
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& export DEBIAN_FRONTEND=noninteractive \
|
||||||
|
&& apt-get -y install --no-install-recommends cmake webp bash libncurses6 git python3 inotify-tools \
|
||||||
|
&& apt-get autoremove -y \
|
||||||
|
&& apt-get clean -y \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# [Optional] Uncomment this line to install additional package.
|
||||||
|
# RUN mix ...
|
44
.devcontainer/devcontainer.json
Normal file
44
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||||
|
// https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/elixir-phoenix-postgres
|
||||||
|
{
|
||||||
|
"name": "Elixir, Phoenix, Node.js & PostgresSQL (Community)",
|
||||||
|
"dockerComposeFile": "docker-compose.yml",
|
||||||
|
"service": "elixir",
|
||||||
|
"workspaceFolder": "/workspace",
|
||||||
|
|
||||||
|
// Set *default* container specific settings.json values on container create.
|
||||||
|
"settings": {
|
||||||
|
"sqltools.connections": [{
|
||||||
|
"name": "Container database",
|
||||||
|
"driver": "PostgreSQL",
|
||||||
|
"previewLimit": 50,
|
||||||
|
"server": "localhost",
|
||||||
|
"port": 5432,
|
||||||
|
"database": "postgres",
|
||||||
|
"username": "postgres",
|
||||||
|
"password": "postgres"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
|
"extensions": [
|
||||||
|
"jakebecker.elixir-ls",
|
||||||
|
"mtxr.sqltools",
|
||||||
|
"mtxr.sqltools-driver-pg"
|
||||||
|
],
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
"forwardPorts": [4000, 4001, 5432],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "mix deps.get",
|
||||||
|
// "runArgs": ["--userns=keep-id", "--privileged"],
|
||||||
|
// "containerUser": "vscode",
|
||||||
|
// "containerEnv": {
|
||||||
|
// "HOME": "/home/vscode",
|
||||||
|
// },
|
||||||
|
// "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,Z",
|
||||||
|
|
||||||
|
// Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
|
||||||
|
"remoteUser": "vscode"
|
||||||
|
}
|
46
.devcontainer/docker-compose.yml
Normal file
46
.devcontainer/docker-compose.yml
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
elixir:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
# Elixir Version: 1.9, 1.10, 1.10.4, ...
|
||||||
|
VARIANT: "1.13.1"
|
||||||
|
# Phoenix Version: 1.4.17, 1.5.4, ...
|
||||||
|
PHOENIX_VERSION: "1.6.6"
|
||||||
|
# Node Version: 10, 11, ...
|
||||||
|
NODE_VERSION: "16"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- ..:/workspace:z
|
||||||
|
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
||||||
|
network_mode: service:db
|
||||||
|
|
||||||
|
# Overrides default command so things don't shut down after the process ends.
|
||||||
|
command: sleep infinity
|
||||||
|
environment:
|
||||||
|
MOBILIZON_INSTANCE_NAME: My Mobilizon Instance
|
||||||
|
MOBILIZON_INSTANCE_HOST: localhost
|
||||||
|
MOBILIZON_INSTANCE_HOST_PORT: 4000
|
||||||
|
MOBILIZON_INSTANCE_PORT: 4000
|
||||||
|
MOBILIZON_INSTANCE_EMAIL: noreply@mobilizon.me
|
||||||
|
MOBILIZON_INSTANCE_REGISTRATIONS_OPEN: "true"
|
||||||
|
MOBILIZON_DATABASE_PASSWORD: postgres
|
||||||
|
MOBILIZON_DATABASE_USERNAME: postgres
|
||||||
|
MOBILIZON_DATABASE_DBNAME: mobilizon
|
||||||
|
MOBILIZON_DATABASE_HOST: db
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgis/postgis:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: app
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data: null
|
15
.doctor.exs
Normal file
15
.doctor.exs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
%Doctor.Config{
|
||||||
|
exception_moduledoc_required: true,
|
||||||
|
failed: false,
|
||||||
|
ignore_modules: [Mobilizon.Web, Mobilizon.GraphQL.Schema, Mobilizon.Service.Activity.Renderer, Mobilizon.Service.Workers.Helper],
|
||||||
|
ignore_paths: [],
|
||||||
|
min_module_doc_coverage: 100,
|
||||||
|
min_module_spec_coverage: 50,
|
||||||
|
min_overall_doc_coverage: 100,
|
||||||
|
min_overall_spec_coverage: 90,
|
||||||
|
moduledoc_required: true,
|
||||||
|
raise: false,
|
||||||
|
reporter: Doctor.Reporters.Full,
|
||||||
|
struct_type_spec_required: true,
|
||||||
|
umbrella: false
|
||||||
|
}
|
|
@ -19,7 +19,6 @@ MOBILIZON_REPLY_EMAIL=contact@mobilizon.lan
|
||||||
# Email settings
|
# Email settings
|
||||||
MOBILIZON_SMTP_SERVER=localhost
|
MOBILIZON_SMTP_SERVER=localhost
|
||||||
MOBILIZON_SMTP_PORT=25
|
MOBILIZON_SMTP_PORT=25
|
||||||
MOBILIZON_SMTP_HOSTNAME=localhost
|
|
||||||
MOBILIZON_SMTP_USERNAME=noreply@mobilizon.lan
|
MOBILIZON_SMTP_USERNAME=noreply@mobilizon.lan
|
||||||
MOBILIZON_SMTP_PASSWORD=password
|
MOBILIZON_SMTP_PASSWORD=password
|
||||||
MOBILIZON_SMTP_SSL=false
|
MOBILIZON_SMTP_SSL=false
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
[
|
[
|
||||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test,priv}/**/*.{ex,exs}"]
|
plugins: [Phoenix.LiveView.HTMLFormatter],
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test,priv}/**/*.{ex,exs,heex}"]
|
||||||
]
|
]
|
||||||
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -27,6 +27,7 @@ priv/data/*
|
||||||
priv/errors/*
|
priv/errors/*
|
||||||
!priv/errors/.gitkeep
|
!priv/errors/.gitkeep
|
||||||
priv/cert/
|
priv/cert/
|
||||||
|
priv/python/__pycache__/
|
||||||
.vscode/
|
.vscode/
|
||||||
cover/
|
cover/
|
||||||
site/
|
site/
|
||||||
|
@ -37,6 +38,8 @@ test/uploads/
|
||||||
uploads/*
|
uploads/*
|
||||||
release/
|
release/
|
||||||
!uploads/.gitkeep
|
!uploads/.gitkeep
|
||||||
|
!uploads/exports/.gitkeep
|
||||||
|
!uploads/exports/**/.gitkeep
|
||||||
.idea
|
.idea
|
||||||
*.mo
|
*.mo
|
||||||
*.po~
|
*.po~
|
||||||
|
@ -44,4 +47,5 @@ release/
|
||||||
docker/production/.env
|
docker/production/.env
|
||||||
test-junit-report.xml
|
test-junit-report.xml
|
||||||
js/junit.xml
|
js/junit.xml
|
||||||
.env
|
.env
|
||||||
|
demo/
|
||||||
|
|
176
.gitlab-ci.yml
176
.gitlab-ci.yml
|
@ -28,6 +28,10 @@ variables:
|
||||||
# Release elements
|
# Release elements
|
||||||
PACKAGE_REGISTRY_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${CI_PROJECT_NAME}"
|
PACKAGE_REGISTRY_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${CI_PROJECT_NAME}"
|
||||||
ARCH: "amd64"
|
ARCH: "amd64"
|
||||||
|
EXPORT_FORMATS: "csv,ods,pdf"
|
||||||
|
APP_VERSION: "${CI_COMMIT_REF_NAME}"
|
||||||
|
APP_ASSET: "${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME}_${ARCH}.tar.gz"
|
||||||
|
CYPRESS_INSTALL_BINARY: 0
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
key: "${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}"
|
key: "${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}"
|
||||||
|
@ -55,8 +59,11 @@ lint-elixir:
|
||||||
- mix deps.get
|
- mix deps.get
|
||||||
script:
|
script:
|
||||||
- export EXITVALUE=0
|
- export EXITVALUE=0
|
||||||
|
- git fetch origin ${CI_DEFAULT_BRANCH}
|
||||||
|
- TARGET_SHA1=$(git show-ref -s ${CI_DEFAULT_BRANCH})
|
||||||
|
- echo "$TARGET_SHA1"
|
||||||
- mix format --check-formatted --dry-run || export EXITVALUE=1
|
- mix format --check-formatted --dry-run || export EXITVALUE=1
|
||||||
- mix credo --strict -a || export EXITVALUE=1
|
- mix credo diff --from-git-merge-base $TARGET_SHA1 --strict -a || export EXITVALUE=1
|
||||||
- mix sobelow --config || export EXITVALUE=1
|
- mix sobelow --config || export EXITVALUE=1
|
||||||
- exit $EXITVALUE
|
- exit $EXITVALUE
|
||||||
|
|
||||||
|
@ -100,32 +107,15 @@ deps:
|
||||||
needs:
|
needs:
|
||||||
- install
|
- install
|
||||||
|
|
||||||
exunit-1.11:
|
|
||||||
stage: test
|
|
||||||
image: tcitworld/mobilizon-ci:legacy
|
|
||||||
services:
|
|
||||||
- name: postgis/postgis:11-3.0
|
|
||||||
alias: postgres
|
|
||||||
variables:
|
|
||||||
MIX_ENV: test
|
|
||||||
before_script:
|
|
||||||
- mix deps.clean --all
|
|
||||||
- mix deps.get
|
|
||||||
- mix ecto.create
|
|
||||||
- mix ecto.migrate
|
|
||||||
script:
|
|
||||||
- mix coveralls
|
|
||||||
allow_failure: true
|
|
||||||
|
|
||||||
exunit:
|
exunit:
|
||||||
stage: test
|
stage: test
|
||||||
services:
|
services:
|
||||||
- name: postgis/postgis:13-3.1
|
- name: postgis/postgis:14-3.2
|
||||||
alias: postgres
|
alias: postgres
|
||||||
variables:
|
variables:
|
||||||
MIX_ENV: test
|
MIX_ENV: test
|
||||||
before_script:
|
before_script:
|
||||||
- mix deps.get
|
- mix deps.get && mix tz_world.update
|
||||||
- mix ecto.create
|
- mix ecto.create
|
||||||
- mix ecto.migrate
|
- mix ecto.migrate
|
||||||
script:
|
script:
|
||||||
|
@ -185,7 +175,7 @@ pages:
|
||||||
# #- yarn run --cwd "js" styleguide:build
|
# #- yarn run --cwd "js" styleguide:build
|
||||||
# #- mv js/styleguide public/frontend
|
# #- mv js/styleguide public/frontend
|
||||||
rules:
|
rules:
|
||||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||||
artifacts:
|
artifacts:
|
||||||
expire_in: 1 hour
|
expire_in: 1 hour
|
||||||
paths:
|
paths:
|
||||||
|
@ -193,24 +183,42 @@ pages:
|
||||||
|
|
||||||
.docker: &docker
|
.docker: &docker
|
||||||
stage: docker
|
stage: docker
|
||||||
|
image: docker:20.10.12
|
||||||
|
variables:
|
||||||
|
DOCKER_TLS_CERTDIR: "/certs"
|
||||||
|
DOCKER_HOST: tcp://docker:2376
|
||||||
|
DOCKER_TLS_VERIFY: 1
|
||||||
|
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
|
||||||
|
DOCKER_DRIVER: overlay2
|
||||||
|
services:
|
||||||
|
- docker:20.10.12-dind
|
||||||
cache: {}
|
cache: {}
|
||||||
image:
|
|
||||||
name: gcr.io/kaniko-project/executor:debug
|
|
||||||
entrypoint: [""]
|
|
||||||
before_script:
|
before_script:
|
||||||
- mkdir -p /kaniko/.docker
|
# Install buildx
|
||||||
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$CI_REGISTRY_AUTH\",\"email\":\"$CI_REGISTRY_EMAIL\"}}}" > /kaniko/.docker/config.json
|
- wget https://github.com/docker/buildx/releases/download/v0.8.1/buildx-v0.8.1.linux-amd64
|
||||||
script:
|
- mkdir -p ~/.docker/cli-plugins/
|
||||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/docker/production/Dockerfile --destination $DOCKER_IMAGE_NAME --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP
|
- mv buildx-v0.8.1.linux-amd64 ~/.docker/cli-plugins/docker-buildx
|
||||||
|
- chmod a+x ~/.docker/cli-plugins/docker-buildx
|
||||||
|
# Create env
|
||||||
|
- docker context create tls-environment
|
||||||
|
- docker buildx create --use tls-environment
|
||||||
|
# Install qemu/binfmt
|
||||||
|
- docker pull tonistiigi/binfmt:latest
|
||||||
|
- docker run --rm --privileged tonistiigi/binfmt:latest --install all
|
||||||
|
# Login to DockerHub
|
||||||
|
- mkdir -p ~/.docker
|
||||||
|
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$CI_REGISTRY_AUTH\",\"email\":\"$CI_REGISTRY_EMAIL\"}}}" > ~/.docker/config.json
|
||||||
|
tags:
|
||||||
|
- "privileged"
|
||||||
|
|
||||||
build-docker-master:
|
build-docker-main:
|
||||||
<<: *docker
|
<<: *docker
|
||||||
rules:
|
rules:
|
||||||
- if: '$CI_PROJECT_NAMESPACE != "framasoft"'
|
- if: '$CI_PROJECT_NAMESPACE != "framasoft"'
|
||||||
when: never
|
when: never
|
||||||
- if: '$CI_PIPELINE_SOURCE == "schedule"'
|
- if: '$CI_PIPELINE_SOURCE == "schedule"'
|
||||||
variables:
|
script:
|
||||||
DOCKER_IMAGE_NAME: framasoft/mobilizon:master
|
- docker buildx build --push --platform linux/amd64 -t framasoft/mobilizon:main -f docker/production/Dockerfile .
|
||||||
|
|
||||||
build-docker-tag:
|
build-docker-tag:
|
||||||
<<: *docker
|
<<: *docker
|
||||||
|
@ -218,26 +226,43 @@ build-docker-tag:
|
||||||
- if: '$CI_PROJECT_NAMESPACE != "framasoft"'
|
- if: '$CI_PROJECT_NAMESPACE != "framasoft"'
|
||||||
when: never
|
when: never
|
||||||
- if: $CI_COMMIT_TAG
|
- if: $CI_COMMIT_TAG
|
||||||
variables:
|
timeout: 3 hours
|
||||||
DOCKER_IMAGE_NAME: framasoft/mobilizon:$CI_COMMIT_TAG
|
script:
|
||||||
|
- >
|
||||||
|
docker buildx build
|
||||||
|
--push
|
||||||
|
--platform linux/amd64,linux/arm64,linux/arm
|
||||||
|
-t framasoft/mobilizon:$CI_COMMIT_TAG
|
||||||
|
-t framasoft/mobilizon:latest
|
||||||
|
-f docker/production/Dockerfile .
|
||||||
|
|
||||||
|
# Packaging app for amd64
|
||||||
package-app:
|
package-app:
|
||||||
|
image: mobilizon/buildpack:1.13.4-erlang-24.3.3-debian-buster
|
||||||
stage: package
|
stage: package
|
||||||
variables: &release-variables
|
variables: &release-variables
|
||||||
MIX_ENV: "prod"
|
MIX_ENV: "prod"
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
TZ: Etc/UTC
|
||||||
|
APP_ASSET: "${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME}_${ARCH}.tar.gz"
|
||||||
script: &release-script
|
script: &release-script
|
||||||
- mix local.hex --force
|
- mix local.hex --force
|
||||||
- mix local.rebar --force
|
- mix local.rebar --force
|
||||||
- mix deps.get
|
- mix deps.get --only-prod
|
||||||
- mix phx.digest
|
- mix compile
|
||||||
|
- mix phx.digest.clean --all && \
|
||||||
- mix release --path release/mobilizon
|
- mix release --path release/mobilizon
|
||||||
- cd release/mobilizon && ln -s lib/mobilizon-*/priv priv
|
- cd release/mobilizon && ln -s lib/mobilizon-*/priv priv && cd ../../
|
||||||
|
- du -sh release/
|
||||||
|
- 'echo "Artifact: ${APP_ASSET}"'
|
||||||
|
- tar czf ${APP_ASSET} -C release mobilizon
|
||||||
|
- du -sh ${APP_ASSET}
|
||||||
only:
|
only:
|
||||||
- tags@framasoft/mobilizon
|
- tags@framasoft/mobilizon
|
||||||
artifacts:
|
artifacts:
|
||||||
expire_in: 30 days
|
expire_in: 2 days
|
||||||
paths:
|
paths:
|
||||||
- release
|
- ${APP_ASSET}
|
||||||
|
|
||||||
package-app-dev:
|
package-app-dev:
|
||||||
stage: package
|
stage: package
|
||||||
|
@ -248,20 +273,64 @@ package-app-dev:
|
||||||
artifacts:
|
artifacts:
|
||||||
expire_in: 2 days
|
expire_in: 2 days
|
||||||
paths:
|
paths:
|
||||||
- release
|
- ${APP_ASSET}
|
||||||
|
|
||||||
|
# Packaging app for multi-arch
|
||||||
|
multi-arch-release:
|
||||||
|
stage: package
|
||||||
|
image: docker:20.10.12
|
||||||
|
variables:
|
||||||
|
DOCKER_TLS_CERTDIR: "/certs"
|
||||||
|
DOCKER_HOST: tcp://docker:2376
|
||||||
|
DOCKER_TLS_VERIFY: 1
|
||||||
|
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
|
||||||
|
DOCKER_DRIVER: overlay2
|
||||||
|
APP_ASSET: "${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME}_${ARCH}.tar.gz"
|
||||||
|
OS: debian-buster
|
||||||
|
services:
|
||||||
|
- docker:20.10.12-dind
|
||||||
|
cache: {}
|
||||||
|
before_script:
|
||||||
|
# Install buildx
|
||||||
|
- wget https://github.com/docker/buildx/releases/download/v0.8.1/buildx-v0.8.1.linux-amd64
|
||||||
|
- mkdir -p ~/.docker/cli-plugins/
|
||||||
|
- mv buildx-v0.8.1.linux-amd64 ~/.docker/cli-plugins/docker-buildx
|
||||||
|
- chmod a+x ~/.docker/cli-plugins/docker-buildx
|
||||||
|
# Create env
|
||||||
|
- docker context create tls-environment
|
||||||
|
- docker buildx create --use tls-environment
|
||||||
|
# Install qemu/binfmt
|
||||||
|
- docker pull tonistiigi/binfmt:latest
|
||||||
|
- docker run --rm --privileged tonistiigi/binfmt:latest --install all
|
||||||
|
script:
|
||||||
|
- docker buildx build --platform linux/${ARCH} --output type=local,dest=releases --build-arg APP_ASSET=${APP_ASSET} -f docker/multiarch/Dockerfile .
|
||||||
|
- ls -alh releases/mobilizon/
|
||||||
|
- du -sh releases/mobilizon/${APP_ASSET}
|
||||||
|
- mv releases/mobilizon/${APP_ASSET} .
|
||||||
|
tags:
|
||||||
|
- "privileged"
|
||||||
|
artifacts:
|
||||||
|
expire_in: 2 days
|
||||||
|
paths:
|
||||||
|
- ${APP_ASSET}
|
||||||
|
parallel:
|
||||||
|
matrix:
|
||||||
|
- ARCH: ["arm", "arm64"]
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PROJECT_NAMESPACE != "framasoft"'
|
||||||
|
when: never
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "schedule"'
|
||||||
|
- if: $CI_COMMIT_TAG
|
||||||
|
timeout: 3h
|
||||||
|
|
||||||
|
# Release
|
||||||
release-upload:
|
release-upload:
|
||||||
stage: upload
|
stage: upload
|
||||||
image: framasoft/yakforms-assets-deploy:latest
|
image: framasoft/upload-packages:latest
|
||||||
|
variables:
|
||||||
|
APP_ASSET: "${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME}_${ARCH}.tar.gz"
|
||||||
rules: *tag-rules
|
rules: *tag-rules
|
||||||
script:
|
script:
|
||||||
- APP_VERSION="${CI_COMMIT_TAG}"
|
|
||||||
- APP_ASSET="${CI_PROJECT_NAME}_${APP_VERSION}_${ARCH}.tar.gz"
|
|
||||||
|
|
||||||
- 'echo "Artifact: ${APP_ASSET}"'
|
|
||||||
- tar czf ${APP_ASSET} -C release mobilizon
|
|
||||||
- ls -al ${APP_ASSET}
|
|
||||||
|
|
||||||
- eval `ssh-agent -s`
|
- eval `ssh-agent -s`
|
||||||
- ssh-add <(echo "${DEPLOYEMENT_KEY}" | base64 --decode -i)
|
- ssh-add <(echo "${DEPLOYEMENT_KEY}" | base64 --decode -i)
|
||||||
- echo "put -r ${APP_ASSET}" | sftp -o "VerifyHostKeyDNS yes" ${DEPLOYEMENT_USER}@${DEPLOYEMENT_HOST}:public/
|
- echo "put -r ${APP_ASSET}" | sftp -o "VerifyHostKeyDNS yes" ${DEPLOYEMENT_USER}@${DEPLOYEMENT_HOST}:public/
|
||||||
|
@ -270,20 +339,27 @@ release-upload:
|
||||||
when: on_success
|
when: on_success
|
||||||
paths:
|
paths:
|
||||||
- mobilizon_*.tar.gz
|
- mobilizon_*.tar.gz
|
||||||
|
parallel:
|
||||||
|
matrix:
|
||||||
|
- ARCH: ["amd64", "arm", "arm64"]
|
||||||
|
|
||||||
release-create:
|
release-create:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: registry.gitlab.com/gitlab-org/release-cli:latest
|
image: registry.gitlab.com/gitlab-org/release-cli:latest
|
||||||
rules: *tag-rules
|
rules: *tag-rules
|
||||||
|
variables:
|
||||||
|
APP_ASSET_AMD64: "${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME}_amd64.tar.gz"
|
||||||
|
APP_ASSET_ARM: "${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME}_arm.tar.gz"
|
||||||
|
APP_ASSET_ARM64: "${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME}_arm64.tar.gz"
|
||||||
before_script:
|
before_script:
|
||||||
- apk --no-cache add gawk sed grep
|
- apk --no-cache add gawk sed grep
|
||||||
script: |
|
script: |
|
||||||
APP_VERSION="${CI_COMMIT_TAG}"
|
|
||||||
APP_ASSET="${CI_PROJECT_NAME}_${APP_VERSION}_${ARCH}.tar.gz"
|
|
||||||
CHANGELOG=$(awk -v version="$APP_VERSION" '/^## / { printit = $2 == version }; printit' CHANGELOG.md | grep -v "## $APP_VERSION" | sed '1{/^$/d}')
|
CHANGELOG=$(awk -v version="$APP_VERSION" '/^## / { printit = $2 == version }; printit' CHANGELOG.md | grep -v "## $APP_VERSION" | sed '1{/^$/d}')
|
||||||
ENDPOINT="https://packages.joinmobilizon.org"
|
ENDPOINT="https://packages.joinmobilizon.org"
|
||||||
|
|
||||||
release-cli create --name "$CI_COMMIT_TAG" \
|
release-cli create --name "$CI_COMMIT_TAG" \
|
||||||
--description "$CHANGELOG" \
|
--description "$CHANGELOG" \
|
||||||
--tag-name "$CI_COMMIT_TAG" \
|
--tag-name "$CI_COMMIT_TAG" \
|
||||||
--assets-link "{\"name\":\"${APP_ASSET}\",\"url\":\"${ENDPOINT}/${APP_ASSET}\"}"
|
--assets-link "{\"name\":\"${APP_ASSET_AMD64}\",\"url\":\"${ENDPOINT}/${APP_ASSET_AMD64}\"}" \
|
||||||
|
--assets-link "{\"name\":\"${APP_ASSET_ARM}\",\"url\":\"${ENDPOINT}/${APP_ASSET_ARM}\"}" \
|
||||||
|
--assets-link "{\"name\":\"${APP_ASSET_ARM64}\",\"url\":\"${ENDPOINT}/${APP_ASSET_ARM64}\"}"
|
||||||
|
|
|
@ -8,5 +8,5 @@
|
||||||
out: "",
|
out: "",
|
||||||
threshold: "medium",
|
threshold: "medium",
|
||||||
ignore: ["Config.HTTPS", "Config.CSP"],
|
ignore: ["Config.HTTPS", "Config.CSP"],
|
||||||
ignore_files: ["config/dev.1.secret.exs", "config/dev.2.secret.exs", "config/dev.3.secret.exs", "config/dev.secret.exs", "config/e2e.secret.exs", "config/prod.secret.exs", "config/test.secret.exs", "config/runtime.1.secret.exs", "config/runtime.2.secret.exs", "config/runtime.3.secret.exs", "config/runtime.exs"]
|
ignore_files: ["config/runtime.exs"]
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
|
|
||||||
5048AE33D6269B15E21CF28C6F545AB6
|
02CE4963DFD1B0D6D5C567357CAFFE97
|
||||||
|
|
||||||
752C0E897CA81ACD81F4BB215FA5F8E4
|
|
||||||
23412CF16549E4E88366DC9DECF39071
|
|
||||||
81C1F600C5809C7029EE32DE4818CD7D
|
|
||||||
155A1FB53DE39EC8EFCFD7FB94EA823D
|
155A1FB53DE39EC8EFCFD7FB94EA823D
|
||||||
73B351E4CB3AF715AD450A085F5E6304
|
2262742E5C8944D5BF6698EC61F5DE50
|
||||||
BBACD7F0BACD4A6D3010C26604671692
|
25BEE162A99754480967216281E9EF33
|
||||||
6D4D4A4821B93BCFAC9CDBB367B34C4B
|
2A6F71CD6F1246F0B152C2376E2E398A
|
||||||
5674F0D127852889ED0132DC2F442AAB
|
30552A09D485A6AA73401C1D54F63C21
|
||||||
|
52900CE4EE3598F6F178A651FB256770
|
||||||
|
6151F44368FC19F2394274F513C29151
|
||||||
|
765526195D4C6D770EAF4DC944A8CBF4
|
||||||
|
B2FF1A12F13B873507C85091688C1D6D
|
||||||
|
B9AF8A342CD7FF39E10CC10A408C28E1
|
||||||
|
C042E87389F7BDCFF4E076E95731AE69
|
||||||
|
C42BFAEF7100F57BED75998B217C857A
|
||||||
|
D11958E86F1B6D37EF656B63405CA8A4
|
||||||
|
F16F054F2628609A726B9FF2F089D484
|
2
.tool-versions
Normal file
2
.tool-versions
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
elixir 1.13.4-otp-24
|
||||||
|
erlang 24.3.3
|
705
CHANGELOG.md
705
CHANGELOG.md
|
@ -5,6 +5,682 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## 2.1.0 - 2022-05-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an event category field. Administrators can extend the pre-configured list of categories through configuration.
|
||||||
|
- Added possibility for administrators to have analytics (Matomo, Plausible supported) and error handling (Sentry supported) on front-end.
|
||||||
|
- Redesigned federation admin section with dedicated instance pages
|
||||||
|
- Allow to filter moderation reports by domain
|
||||||
|
- Added a button to go to past events of a group if it has no upcoming events
|
||||||
|
- Add Überauth CAS Strategy
|
||||||
|
- Add a CLI command to delete actors
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed mailer library from Bamboo to Swoosh, should fix emails being considered spam. **Some configuration changes are required, see below.**
|
||||||
|
- Expose some fields to ActivityStreams event representation: `isOnline`, `remainingAttendeeCapacity` and `participantCount`
|
||||||
|
- Expose a new field to ActivityStreams group representation: `memberCount`
|
||||||
|
- Improve group creation errors feedback
|
||||||
|
- Only display locality in event card
|
||||||
|
- Stale groups are now excluded from group search
|
||||||
|
- Event default visibility is now set according to group privacy setting
|
||||||
|
- Remove Koena Connect button
|
||||||
|
- Hide the whole metadata block if group has no description
|
||||||
|
- Increase task timeout in Refresher to 60 seconds
|
||||||
|
- Allow webfinger to be fetched over http (not https) in dev mode
|
||||||
|
- Improve reactions when approving/rejecting an instance follow
|
||||||
|
- Improve instance admin view for mobile
|
||||||
|
- Allow to reject instance following
|
||||||
|
- Allow instance to have non-standard ports
|
||||||
|
- Add pagination to the instances list
|
||||||
|
- Eventually fetch actors in mentions
|
||||||
|
- Improve IdentityPicker, JoinGroupWithAccount and ActorInline components
|
||||||
|
- Various group and posts improvements
|
||||||
|
- Update schema.graphql file
|
||||||
|
- Add "Accept-Language" header to sentry request metadata
|
||||||
|
- Hide address blocks when address has no real data
|
||||||
|
- Remove obsolete attribute type="text/css" from <style> tags
|
||||||
|
- Improve actor cards integration
|
||||||
|
- Use upstream dependencies for Ueberauth providers
|
||||||
|
- Include ongoing events in search
|
||||||
|
- Send push notification into own task
|
||||||
|
- Add appropriate timeouts for Repo.transactions
|
||||||
|
- Add a proper error message when adding an instance follow that doesn't respond
|
||||||
|
- Allow the instance to be followed from Mastodon (through relays)
|
||||||
|
- Remove unused fragment from FETCH_PERSON GraphQL query
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed actor refreshment being impossible
|
||||||
|
- Fixed ical export for undefined datetimes
|
||||||
|
- Fixed parsing links with hashtag characters
|
||||||
|
- Fixed fetching link details from Twitter
|
||||||
|
- Fixed Thunderbird accessing ICS feed endpoint with special `Accept` HTTP header
|
||||||
|
- Make sure every ICS/Feed caches are emptied when modifying entities
|
||||||
|
- Fixed time issues with DST changes
|
||||||
|
- Fixed group preview card not truncating description
|
||||||
|
- Fixed redirection after login
|
||||||
|
- Fixed user admin section showing button to confirm user when the user is already confirmed
|
||||||
|
- Fixed creating event from group view not always setting the group as organizer
|
||||||
|
- Fixed invalid addresses blocking event metadata preview rendering
|
||||||
|
- Fixed group deletion with comments that caused foreign key issues
|
||||||
|
- Fixed incoming Accept activities from participations we don't already have
|
||||||
|
- Fixed resources that didn't have metadata size limits
|
||||||
|
- Properly fallback to UTC when sending notifications and the user doesn't have a timezone setting set
|
||||||
|
- Fix posts creation
|
||||||
|
- Fix rejecting instance follow
|
||||||
|
- Fix pagination of group events
|
||||||
|
- Add proper fallback for when a TZ isn't registered
|
||||||
|
- Hide side of report modal on low width screens
|
||||||
|
- Fix Telegram Logo being replaced with Mastodon logo in ShareGroupModal
|
||||||
|
- Change URL for Mastodon Share Manager
|
||||||
|
- Fix receiving Flag activities on federated events
|
||||||
|
- Fix activity notifications by preloading user.activity_settings
|
||||||
|
- Fix text overflow on group card description
|
||||||
|
- Exclude tags with more than 40 characters from being extracted
|
||||||
|
- Avoid duplicate tags with different casing
|
||||||
|
- Fix invalid HTML (<div> inside <label>)
|
||||||
|
- Fix latest group not refreshing in admin section
|
||||||
|
- Add missing "relay@" part of federated address to follow
|
||||||
|
- Fix Ueberauth use of CSRF with session
|
||||||
|
- Fix being an administrator when using 3rd-party auth provider
|
||||||
|
- Make sure activity recipient can't be nil
|
||||||
|
- Make sure users can't create profiles or groups with non-valid patterns
|
||||||
|
- Add description field to address representation
|
||||||
|
- Make sure prompt show the correct message and not just "Continue?" in mix mode
|
||||||
|
- Make sure activity notification recaps can't be sent multiple times
|
||||||
|
- Fix group notification of new event being sent multiple times
|
||||||
|
- Fix links to group page in group membership emails and participation
|
||||||
|
- Fix clicking on map crashing the app
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
- Arabic
|
||||||
|
- Basque
|
||||||
|
- Belarusian
|
||||||
|
- Bengali
|
||||||
|
- Catalan
|
||||||
|
- Chinese (Traditional)
|
||||||
|
- Croatian
|
||||||
|
- Czech
|
||||||
|
- Danish
|
||||||
|
- Dutch
|
||||||
|
- Esperanto
|
||||||
|
- Finnish
|
||||||
|
- French
|
||||||
|
- Gaelic
|
||||||
|
- Galician
|
||||||
|
- German
|
||||||
|
- Hebrew
|
||||||
|
- Hungarian
|
||||||
|
- Indonesian
|
||||||
|
- Italian
|
||||||
|
- Japanese
|
||||||
|
- Kabyle
|
||||||
|
- Kannada
|
||||||
|
- Norwegian Nynorsk
|
||||||
|
- Occitan
|
||||||
|
- Persian
|
||||||
|
- Polish
|
||||||
|
- Portuguese
|
||||||
|
- Portuguese (Brazil)
|
||||||
|
- Russian
|
||||||
|
- Slovenian
|
||||||
|
- Spanish
|
||||||
|
- Swedish
|
||||||
|
- Welsh
|
||||||
|
|
||||||
|
## 2.1.0-rc.6 - 2022-05-11
|
||||||
|
|
||||||
|
Changes since rc.5:
|
||||||
|
|
||||||
|
- Allow the instance to be followed from Mastodon (through relays)
|
||||||
|
- Make sure activity recipient can't be nil
|
||||||
|
- Make sure users can't create profiles or groups with non-valid patterns
|
||||||
|
- Add description field to address representation
|
||||||
|
- Make sure prompt show the correct message and not just "Continue?" in mix mode
|
||||||
|
- Add a CLI command to delete actors
|
||||||
|
- Make sure activity notification recaps can't be sent multiple times
|
||||||
|
- Fix group notification of new event being sent multiple times
|
||||||
|
- Fix links to group page in group membership emails and participation
|
||||||
|
- Fix clicking on map crashing the app
|
||||||
|
- Remove unused fragment from FETCH_PERSON GraphQL query
|
||||||
|
|
||||||
|
## 2.1.0-rc.5 - 2022-05-06
|
||||||
|
|
||||||
|
Changes since rc.4:
|
||||||
|
|
||||||
|
- Add appropriate timeouts for Repo.transactions
|
||||||
|
- Remove OS-specific packages
|
||||||
|
- Remove refresh instance triggers
|
||||||
|
- Add a proper error message when adding an instance follow that doesn't respond
|
||||||
|
|
||||||
|
## 2.1.0-rc.4 - 2022-05-03
|
||||||
|
|
||||||
|
Changes since rc.3:
|
||||||
|
|
||||||
|
- Use upstream dependencies for Ueberauth providers
|
||||||
|
- Fix Ueberauth use of CSRF with session
|
||||||
|
- Fix being an administrator when using 3rd-party auth provider
|
||||||
|
- Include ongoing events in search
|
||||||
|
- Send push notification into own task
|
||||||
|
- Add Überauth CAS Strategy
|
||||||
|
|
||||||
|
## 2.1.0-rc.3 - 2022-04-24
|
||||||
|
|
||||||
|
Changes since rc.2:
|
||||||
|
|
||||||
|
- Fix activity notifications by preloading user.activity_settings
|
||||||
|
- Add "Accept-Language" header to sentry request metadata
|
||||||
|
- Hide address blocks when address has no real data
|
||||||
|
- Fix text overflow on group card description
|
||||||
|
- Exclude tags with more than 40 characters from being extracted
|
||||||
|
- Avoid duplicate tags with different casing
|
||||||
|
- Fix invalid HTML (<div> inside <label>)
|
||||||
|
- Remove attribute type="text/css" from <style> tags
|
||||||
|
- Improve actor cards integration
|
||||||
|
- Fix latest group not refreshing in admin section
|
||||||
|
- Add missing "relay@" part of federated address to follow
|
||||||
|
|
||||||
|
## 2.1.0-rc.2 - 2022-04-20
|
||||||
|
|
||||||
|
Changes since rc.1:
|
||||||
|
|
||||||
|
- Hide the whole metadata block if group has no description
|
||||||
|
- Increase task timeout in Refresher to 60 seconds
|
||||||
|
- Allow webfinger to be fetched over http (not https) in dev mode
|
||||||
|
- Fix rejecting instance follow
|
||||||
|
- Allow instance to have non-standard ports
|
||||||
|
- Improve reactions when approving/rejecting an instance follow
|
||||||
|
- Improve instance admin view for mobile
|
||||||
|
- Allow to reject instance following
|
||||||
|
- Fix pagination of group events
|
||||||
|
- Add pagination to the instances list
|
||||||
|
- Upgrade deps
|
||||||
|
- Eventually fetch actors in mentions
|
||||||
|
- Add proper fallback for when a TZ isn't registered
|
||||||
|
- Improve IdentityPicker
|
||||||
|
- Hide side of report modal on low width screens
|
||||||
|
- Improve JoinGroupWithAccount component
|
||||||
|
- Various group and posts improvements
|
||||||
|
- Fix Telegram Logo being replaced with Mastodon logo in ShareGroupModal
|
||||||
|
- Change URL to Mastodon Share Manager
|
||||||
|
- Improve ActorInline component
|
||||||
|
- Avoid assuming we're on Debian-based in release build
|
||||||
|
- Fix receiving Flag activities on federated events
|
||||||
|
- Update schema.graphql file
|
||||||
|
|
||||||
|
## 2.1.0-rc.1 - 2022-04-18
|
||||||
|
|
||||||
|
Changes since beta.3:
|
||||||
|
|
||||||
|
- Fix posts creation
|
||||||
|
- Fix some typespecs
|
||||||
|
- Remove Koena Connect button
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
## 2.1.0-beta.3 - 2022-04-09
|
||||||
|
|
||||||
|
Changes since beta.2:
|
||||||
|
|
||||||
|
- Add Fedora and Alpine builds
|
||||||
|
|
||||||
|
## 2.1.0-beta.2 - 2022-04-08
|
||||||
|
|
||||||
|
Changes since beta.1 :
|
||||||
|
|
||||||
|
- Build release packages for several distributions (Debian Bullseye, Debian Buster, Ubuntu Focal, Ubuntu Bionic) because of libc version changes
|
||||||
|
|
||||||
|
## 2.1.0-beta.1 - 2022-04-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an event category field. Administrators can extend the pre-configured list of categories through configuration.
|
||||||
|
- Added possibility for administrators to have analytics (Matomo, Plausible supported) and error handling (Sentry supported) on front-end.
|
||||||
|
- Redesigned federation admin section with dedicated instance pages
|
||||||
|
- Allow to filter moderation reports by domain
|
||||||
|
- Added a button to go to past events of a group if it has no upcoming events
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed mailer library from Bamboo to Swoosh, should fix emails being considered spam. **Some configuration changes are required, see below.**
|
||||||
|
- Expose some fields to ActivityStreams event representation: `isOnline`, `remainingAttendeeCapacity` and `participantCount`
|
||||||
|
- Expose a new field to ActivityStreams group representation: `memberCount`
|
||||||
|
- Improve group creation errors feedback
|
||||||
|
- Only display locality in event card
|
||||||
|
- Stale groups are now excluded from group search
|
||||||
|
- Event default visibility is now set according to group privacy setting
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed actor refreshment being impossible
|
||||||
|
- Fixed ical export for undefined datetimes
|
||||||
|
- Fixed parsing links with hashtag characters
|
||||||
|
- Fixed fetching link details from Twitter
|
||||||
|
- Fixed Thunderbird accessing ICS feed endpoint with special `Accept` HTTP header
|
||||||
|
- Make sure every ICS/Feed caches are emptied when modifying entities
|
||||||
|
- Fixed time issues with DST changes
|
||||||
|
- Fixed group preview card not truncating description
|
||||||
|
- Fixed redirection after login
|
||||||
|
- Fixed user admin section showing button to confirm user when the user is already confirmed
|
||||||
|
- Fixed creating event from group view not always setting the group as organizer
|
||||||
|
- Fixed invalid addresses blocking event metadata preview rendering
|
||||||
|
- Fixed group deletion with comments that caused foreign key issues
|
||||||
|
- Fixed incoming Accept activities from participations we don't already have
|
||||||
|
- Fixed resources that didn't have metadata size limits
|
||||||
|
- Properly fallback to UTC when sending notifications and the user doesn't have a timezone setting set
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
- Arabic
|
||||||
|
- Basque
|
||||||
|
- Belarusian
|
||||||
|
- Bengali
|
||||||
|
- Catalan
|
||||||
|
- Chinese (Traditional)
|
||||||
|
- Croatian
|
||||||
|
- Czech
|
||||||
|
- Danish
|
||||||
|
- Dutch
|
||||||
|
- Esperanto
|
||||||
|
- Finnish
|
||||||
|
- French
|
||||||
|
- Gaelic
|
||||||
|
- Galician
|
||||||
|
- German
|
||||||
|
- Hebrew
|
||||||
|
- Hungarian
|
||||||
|
- Indonesian
|
||||||
|
- Italian
|
||||||
|
- Japanese
|
||||||
|
- Kabyle
|
||||||
|
- Kannada
|
||||||
|
- Norwegian Nynorsk
|
||||||
|
- Occitan
|
||||||
|
- Persian
|
||||||
|
- Polish
|
||||||
|
- Portuguese
|
||||||
|
- Portuguese (Brazil)
|
||||||
|
- Russian
|
||||||
|
- Slovenian
|
||||||
|
- Spanish
|
||||||
|
- Swedish
|
||||||
|
- Welsh
|
||||||
|
|
||||||
|
## 2.0.2 - 2021-12-22
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved handling of media file deletion
|
||||||
|
- Releases and Docker image are now using Elixir 1.13
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed position of tentative tag on event cards
|
||||||
|
- Fixed text overflow when a link is too long in event mobile view
|
||||||
|
- Fixed filtering user own memberships and group members in event organizer & contacts picker
|
||||||
|
- Fixed first day of week not depending on locale in the datetime picker
|
||||||
|
- Fixed the admin page when a group/profile/user was not found
|
||||||
|
- Fixed group members pagination on admin group profile view
|
||||||
|
- Fixed admin edition of the instance's language
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
- Croatian
|
||||||
|
- Czech
|
||||||
|
- Esperanto
|
||||||
|
- German
|
||||||
|
- Hebrew
|
||||||
|
- Occitan
|
||||||
|
- Persian
|
||||||
|
- Russian
|
||||||
|
- Spanish
|
||||||
|
|
||||||
|
## 2.0.1 - 2021-11-26
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Remove litepub context
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Make sure my group upcoming events are ordered by their start date
|
||||||
|
- Fix event participants pagination
|
||||||
|
- Always focus the search field after results have been fetched
|
||||||
|
- Don't sign fetches to instance actor when refreshing their keys
|
||||||
|
- Fix reject of already following instances
|
||||||
|
- Added missing timezone data to the Docker image
|
||||||
|
- Replace @tiptap/starter-kit with indidual extensions, removing unneeded extensions that caused issues on old Firefox versions
|
||||||
|
- Better handling of Friendica Update activities without actor information
|
||||||
|
- Always show pending/cancelled status on event cards
|
||||||
|
- Fixed nightly docker build
|
||||||
|
- Refresh loggeduser information before the final step of onboarding, avoiding loop when finishing onboarding
|
||||||
|
- Handle tz_world data being absent
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
- Croatian (New !)
|
||||||
|
- Czech
|
||||||
|
- Gaelic
|
||||||
|
- Hungarian
|
||||||
|
- Indonesian
|
||||||
|
- Welsh (New !)
|
||||||
|
|
||||||
|
## 2.0.0 - 2021-11-23
|
||||||
|
|
||||||
|
Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/main/UPGRADE.md#upgrading-from-13-to-20) file as well.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added possibility to follow groups and be notified from new upcoming events
|
||||||
|
- Export list of participants to CSV, `PDF` and `ODS`
|
||||||
|
- Allow to set timezone for an event. The timezone is automatically defined from the address if one is defined. If the event timezone is different than the user's current one, a toggle is shown to switch between the two.
|
||||||
|
- Added initial support for Right To Left languages (such as arabic) and [BiDi](https://en.wikipedia.org/wiki/Bidirectional_text)
|
||||||
|
- Group followers and members get an notification email by default when a group publishes a new event (subject to activity notification settings)
|
||||||
|
- Group admins can now approve or deny new memberships
|
||||||
|
- Build releases in `arm` and `arm64` format in addition to `amd64`
|
||||||
|
- Build Docker images in `arm` and `arm64` format in addition to `amd64`
|
||||||
|
- Added possibility to indicate the event is fully online
|
||||||
|
- Added possibility to search only for online events
|
||||||
|
- Added possibility to search only in past events
|
||||||
|
- Detect event, comments and posts languages automatically. Allows setting language
|
||||||
|
- Allow to change an user's password through the users.modify mix task
|
||||||
|
- Add instance setting so that only the admin can create groups
|
||||||
|
- Add instance setting so that only groups can create events
|
||||||
|
- Added JSON-LD metadata about the event in emails
|
||||||
|
- Added a quick link to email notification settings at the bottom of emails
|
||||||
|
- Allow to access Mobilizon with a specific language directly by using `https://instance.tld/lang` where `lang` is a language supported by Mobilizon
|
||||||
|
- Added organizer actor name (profile or group) in the icalendar export
|
||||||
|
- Add initial support for federation with Gancio
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Multiple UI improvements, including post, event and participation cards, discussions and emails. The « My Events » page was also redesigned to allow showing events from your groups.
|
||||||
|
- Various accessibility improvements
|
||||||
|
- Event update notification is send to participants ~30 minutes after the event update, so that successive edits are throttled.
|
||||||
|
- Event, post and comments titles and content now have expose their detected language in HTML, for improved screen reader experience
|
||||||
|
- Delete current actor ID as well from local storage when unlogging
|
||||||
|
- Show a default text for instance contact in default terms text when no instance contact is set
|
||||||
|
- Only show locatecontrol button in leaflet map when we can do geolocation
|
||||||
|
- Disable push column in notification settings when push is not available
|
||||||
|
- Show actual language instead of language code in Users admin view
|
||||||
|
- Empty old & new passwords fields when successful password change
|
||||||
|
- Don't link to the group page from admin when actor is suspended
|
||||||
|
- Warn participants when the event organizer is suspended (and therefore the event cancelled)
|
||||||
|
- Improve metadata on public page
|
||||||
|
- Make sure some event action pages (participate remotely or without an account) don't get indexed by search engines
|
||||||
|
- Only send `Tombstone` element in `Delete` activities, not the whole previous deleted element.
|
||||||
|
- Make sure `Delete` activity are send correctly to everyone
|
||||||
|
- Only add address and tags to event icalendar export if they exist
|
||||||
|
- `master` branch has been renamed to `main`
|
||||||
|
- Mention following groups on the registration page
|
||||||
|
- Add missing group name to activity notifications
|
||||||
|
- Warn while registering and logging when the email contains uppercase characters
|
||||||
|
- Improve json-ld metadata on event live streams
|
||||||
|
- Add "eventAttendanceMode" to JSON-ld schema.org event representation
|
||||||
|
- Improve sending pending participation notifications
|
||||||
|
- Add "formerType" and "delete" attributes on Tombstones ActivityPub objects representation
|
||||||
|
- Improve MyEvents page description text
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Support for Elixir < 1.12 and OTP < 22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix tags autocomplete
|
||||||
|
- Fix config onboarding after LDAP initial connexion
|
||||||
|
- Fix events pagination on tags page
|
||||||
|
- Fixed deduplicated files from orphan media being deleted as well
|
||||||
|
- Fix deleting own account
|
||||||
|
- Fix search returning user profiles instead of only groups
|
||||||
|
- Fix federating geo coordinates
|
||||||
|
- Fix an issue with group activity items when moving resources
|
||||||
|
- Fix an issue with Identity Picker
|
||||||
|
- Fix an issue with TagInput
|
||||||
|
- Fix an issue when leaving a group
|
||||||
|
- Fix admin settings edition
|
||||||
|
- Fix an issue when showing public page of suspended group
|
||||||
|
- Removed non existing page (`/about/mobilizon`) from sitemap
|
||||||
|
- Fix action logs containing group suspension events
|
||||||
|
- Fixed group physical address not exposed to ActivityPub
|
||||||
|
- Release front-end files are no longer in duplicate
|
||||||
|
- Only show datetime timezone toggle on event if the timezone offset is different from our own
|
||||||
|
- Fix error when determining audience for Discussion when deleting a comment
|
||||||
|
- Fix a couple of accessibility issues
|
||||||
|
- Limit to acceptable tags when pasting raw HTML into comment fields on front-end
|
||||||
|
- Fixed group map display
|
||||||
|
- Fixed updating group physical address
|
||||||
|
- Allow group members to access group drafts
|
||||||
|
- Improve group refreshment workflow
|
||||||
|
- Fixed date signature generation for federation
|
||||||
|
- Fixed an issue when duplicating a group event from another profile
|
||||||
|
- Fixed event metadata not saved on eventcreation
|
||||||
|
- Use a different pagination parameter for searched events and featured events on search page
|
||||||
|
- Fixed creating group activities when creating events with some fields
|
||||||
|
- Move release package at correct path for CI upload
|
||||||
|
- Fixed event contacts that were not exposed and fetched over federation
|
||||||
|
- Don't sign fetch when fetching actor for a given signature
|
||||||
|
- Some various HTTP signatures issues
|
||||||
|
- Fixed actor AP representation of avatar
|
||||||
|
- Handle errors when fetching actor pictures
|
||||||
|
- Fixed sending group events to followers on Mastodon
|
||||||
|
- Fixed actors avatars and banners being deleted if the same file was also an orphan media
|
||||||
|
- Fix spacing in organizer picker
|
||||||
|
- Increase number of close events and follow group events
|
||||||
|
- Fix accessing user profile in admin section
|
||||||
|
- Set initial values for some EventMetadata elements, fixing submitting them right away with no value
|
||||||
|
- Avoid giving an error page if the apollo futureParticipations query is undefined
|
||||||
|
- Fixed path to exports in production
|
||||||
|
- Fixed padding below truncated title of event cards
|
||||||
|
- Fixed exports that weren't enabled in Docker
|
||||||
|
- Fixed error page when event end date is null
|
||||||
|
- Fixed event language not being allowed to be null
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fixed private messages sent as event replies from Mastodon that were shown publically as public comments. They are now discarded.
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
- Czech
|
||||||
|
- Gaelic
|
||||||
|
- German
|
||||||
|
- Hungarian
|
||||||
|
- Indonesian
|
||||||
|
- Norwegian Nynorsk
|
||||||
|
- Occitan
|
||||||
|
- Persian
|
||||||
|
- Portuguese (Brazil)
|
||||||
|
- Russian
|
||||||
|
- Slovenian
|
||||||
|
- Spanish
|
||||||
|
|
||||||
|
## 2.0.0-rc.3 - 2021-11-22
|
||||||
|
|
||||||
|
This lists changes since 2.0.0-rc.3. Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/main/UPGRADE.md#upgrading-from-13-to-20) file as well.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed path to exports in production
|
||||||
|
- Fixed padding below truncated title of event cards
|
||||||
|
- Fixed exports that weren't enabled in Docker
|
||||||
|
- Fixed error page when event end date is null
|
||||||
|
|
||||||
|
## 2.0.0-rc.2 - 2021-11-22
|
||||||
|
|
||||||
|
This lists changes since 2.0.0-rc.1. Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/main/UPGRADE.md#upgrading-from-13-to-20) file as well.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improve MyEvents page description text
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix spacing in organizer picker
|
||||||
|
- Increase number of close events and follow group events
|
||||||
|
- Fix accessing user profile in admin section
|
||||||
|
- Set initial values for some EventMetadata elements, fixing submitting them right away with no value
|
||||||
|
- Avoid giving an error page if the apollo futureParticipations query is undefined
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
- German
|
||||||
|
- Hungarian
|
||||||
|
|
||||||
|
## 2.0.0-rc.1 - 2021-11-20
|
||||||
|
|
||||||
|
This lists changes since 2.0.0-beta.2. Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/main/UPGRADE.md#upgrading-from-13-to-20) file as well.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Mention following groups on the registration page
|
||||||
|
- Add missing group name to activity notifications
|
||||||
|
- Warn while registering and logging when the email contains uppercase characters
|
||||||
|
- Improve json-ld metadata on event live streams
|
||||||
|
- Add "eventAttendanceMode" to JSON-ld schema.org event representation
|
||||||
|
- Improve sending pending participation notifications
|
||||||
|
- Add "formerType" and "delete" attributes on Tombstones ActivityPub objects representation
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed creating group activities when creating events with some fields
|
||||||
|
- Move release package at correct path for CI upload
|
||||||
|
- Fixed event contacts that were not exposed and fetched over federation
|
||||||
|
- Don't sign fetch when fetching actor for a given signature
|
||||||
|
- Some various HTTP signatures issues
|
||||||
|
- Fixed actor AP representation of avatar
|
||||||
|
- Handle errors when fetching actor pictures
|
||||||
|
- Fixed sending group events to followers on Mastodon
|
||||||
|
- Fixed actors avatars and banners being deleted if the same file was also an orphan media
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
- Gaelic
|
||||||
|
- Spanish
|
||||||
|
|
||||||
|
## 2.0.0-beta.2 - 2021-11-15
|
||||||
|
|
||||||
|
This lists changes since 2.0.0-beta.1. Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/main/UPGRADE.md#upgrading-from-13-to-20) file as well.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Group followers and members get an notification email by default when a group publishes a new event (subject to activity notification settings)
|
||||||
|
- Group admins can now approve or deny new memberships
|
||||||
|
- Added organizer actor name (profile or group) in the icalendar export
|
||||||
|
- Add initial support for federation with Gancio
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Event update notification is send to participants ~30 minutes after the event update, so that successive edits are throttled.
|
||||||
|
- Event, post and comments titles and content now have expose their detected language in HTML, for improved screen reader experience
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Release front-end files are no longer in duplicate
|
||||||
|
- Only show datetime timezone toggle on event if the timezone offset is different from our own
|
||||||
|
- Fix error when determining audience for Discussion when deleting a comment
|
||||||
|
- Fix a couple of accessibility issues
|
||||||
|
- Limit to acceptable tags when pasting raw HTML into comment fields on front-end
|
||||||
|
- Fixed group map display
|
||||||
|
- Fixed updating group physical address
|
||||||
|
- Allow group members to access group drafts
|
||||||
|
- Improve group refreshment workflow
|
||||||
|
- Fixed date signature generation for federation
|
||||||
|
- Fixed an issue when duplicating a group event from another profile
|
||||||
|
- Fixed event metadata not saved on eventcreation
|
||||||
|
- Use a different pagination parameter for searched events and featured events on search page
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
- Gaelic
|
||||||
|
- Spanish
|
||||||
|
|
||||||
|
## 2.0.0-beta.1 - 2021-11-09
|
||||||
|
|
||||||
|
Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/main/UPGRADE.md#upgrading-from-13-to-20) file as well.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added possibility to follow groups and be notified from new upcoming events
|
||||||
|
- Export list of participants to CSV, `PDF` and `ODS`
|
||||||
|
- Allow to set timezone for an event. The timezone is automatically defined from the address if one is defined. If the event timezone is different than the user's current one, a toggle is shown to switch between the two.
|
||||||
|
- Added initial support for Right To Left languages (such as arabic) and [BiDi](https://en.wikipedia.org/wiki/Bidirectional_text)
|
||||||
|
- Build releases in `arm` and `arm64` format in addition to `amd64`
|
||||||
|
- Build Docker images in `arm` and `arm64` format in addition to `amd64`
|
||||||
|
- Added possibility to indicate the event is fully online
|
||||||
|
- Added possibility to search only for online events
|
||||||
|
- Added possibility to search only in past events
|
||||||
|
- Detect event, comments and posts languages automatically. Allows setting language
|
||||||
|
- Allow to change an user's password through the users.modify mix task
|
||||||
|
- Add instance setting so that only the admin can create groups
|
||||||
|
- Add instance setting so that only groups can create events
|
||||||
|
- Added JSON-LD metadata about the event in emails
|
||||||
|
- Added a quick link to email notification settings at the bottom of emails
|
||||||
|
- Allow to access Mobilizon with a specific language directly by using `https://instance.tld/lang` where `lang` is a language supported by Mobilizon
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Multiple UI improvements, including post, event and participation cards, discussions and emails. The « My Events » page was also redesigned to allow showing events from your groups.
|
||||||
|
- Various accessibility improvements
|
||||||
|
- Delete current actor ID as well from local storage when unlogging
|
||||||
|
- Show a default text for instance contact in default terms text when no instance contact is set
|
||||||
|
- Only show locatecontrol button in leaflet map when we can do geolocation
|
||||||
|
- Disable push column in notification settings when push is not available
|
||||||
|
- Show actual language instead of language code in Users admin view
|
||||||
|
- Empty old & new passwords fields when successful password change
|
||||||
|
- Don't link to the group page from admin when actor is suspended
|
||||||
|
- Warn participants when the event organizer is suspended (and therefore the event cancelled)
|
||||||
|
- Improve metadata on public page
|
||||||
|
- Make sure some event action pages (participate remotely or without an account) don't get indexed by search engines
|
||||||
|
- Only send `Tombstone` element in `Delete` activities, not the whole previous deleted element.
|
||||||
|
- Make sure `Delete` activity are send correctly to everyone
|
||||||
|
- Only add address and tags to event icalendar export if they exist
|
||||||
|
- `master` branch has been renamed to `main`
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Support for Elixir < 1.12 and OTP < 22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix tags autocomplete
|
||||||
|
- Fix config onboarding after LDAP initial connexion
|
||||||
|
- Fix events pagination on tags page
|
||||||
|
- Fixed deduplicated files from orphan media being deleted as well
|
||||||
|
- Fix deleting own account
|
||||||
|
- Fix search returning user profiles instead of only groups
|
||||||
|
- Fix federating geo coordinates
|
||||||
|
- Fix an issue with group activity items when moving resources
|
||||||
|
- Fix an issue with Identity Picker
|
||||||
|
- Fix an issue with TagInput
|
||||||
|
- Fix an issue when leaving a group
|
||||||
|
- Fix admin settings edition
|
||||||
|
- Fix an issue when showing public page of suspended group
|
||||||
|
- Removed non existing page (`/about/mobilizon`) from sitemap
|
||||||
|
- Fix action logs containing group suspension events
|
||||||
|
- Fixed group physical address not exposed to ActivityPub
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fixed private messages sent as event replies from Mastodon that were shown publically as public comments. They are now discarded.
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
- Czech
|
||||||
|
- Gaelic
|
||||||
|
- German
|
||||||
|
- Indonesian
|
||||||
|
- Norwegian Nynorsk
|
||||||
|
- Occitan
|
||||||
|
- Persian
|
||||||
|
- Portuguese (Brazil)
|
||||||
|
- Russian
|
||||||
|
- Slovenian
|
||||||
|
- Spanish
|
||||||
|
|
||||||
## 1.3.2 - 2021-08-23
|
## 1.3.2 - 2021-08-23
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@ -58,7 +734,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed links contained in event & post description that didn't open in new tabs
|
- Fixed links contained in event & post description that didn't open in new tabs
|
||||||
- Add back missing RSS/ical links on public group pages
|
- Add back missing RSS/ical links on public group pages
|
||||||
- Fixed links to Framacolibri forum
|
- Fixed links to Framacolibri forum
|
||||||
- Fixed drafts and restricted visibility events & posts listed on group page
|
- Fixed drafts and restricted visibility events & posts listed on group page
|
||||||
- Fixed notification page on Safari
|
- Fixed notification page on Safari
|
||||||
|
@ -80,7 +756,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Fixed token refreshment issues
|
- Fixed token refreshment issues
|
||||||
- Fixed search from 404 page
|
- Fixed search from 404 page
|
||||||
|
|
||||||
|
|
||||||
### Translations
|
### Translations
|
||||||
|
|
||||||
- Catalan
|
- Catalan
|
||||||
|
@ -106,9 +781,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Fixed group discussions with deleted comments
|
- Fixed group discussions with deleted comments
|
||||||
|
|
||||||
## 1.2.2 - 2021-07-01
|
## 1.2.2 - 2021-07-01
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Improved UI for participations when message is too long
|
- Improved UI for participations when message is too long
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
@ -116,7 +792,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Fixed crash when trying to notify activities not from groups
|
- Fixed crash when trying to notify activities not from groups
|
||||||
- Fixed imagemagick missing from Dockerfile
|
- Fixed imagemagick missing from Dockerfile
|
||||||
- Fixed push notifications for group, members & post activities
|
- Fixed push notifications for group, members & post activities
|
||||||
- Fixed ellipsis in DiscussionListView
|
- Fixed ellipsis in DiscussionListView
|
||||||
- Fixed submission button for posts not visible on mobile
|
- Fixed submission button for posts not visible on mobile
|
||||||
- Fixed remote profile suspension
|
- Fixed remote profile suspension
|
||||||
|
|
||||||
|
@ -132,6 +808,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Fixed compatibility check in Notification section for service workers
|
- Fixed compatibility check in Notification section for service workers
|
||||||
|
|
||||||
## 1.2.0 - 2021-06-29
|
## 1.2.0 - 2021-06-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **Notifications for various group and event activity, both by email and browser push notifications. Daily and weekly digests are also available.**
|
- **Notifications for various group and event activity, both by email and browser push notifications. Daily and weekly digests are also available.**
|
||||||
|
@ -146,7 +823,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- **Various improvements to mobile views**
|
- **Various improvements to mobile views**
|
||||||
- Make JWT access tokens short-lived
|
- Make JWT access tokens short-lived
|
||||||
- Disabled Cldr warning that the `Cldr.Plug.AcceptLanguage` plug didn't many any known locale
|
- Disabled Cldr warning that the `Cldr.Plug.AcceptLanguage` plug didn't many any known locale
|
||||||
- Replaced GraphiQL web interface with graphql-playground
|
- Replaced GraphiQL web interface with graphql-playground
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
@ -306,7 +983,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Added an unique index on the addresses url
|
- Added an unique index on the addresses url
|
||||||
- Added org.opencontainers.image.source annotation to the Docker image
|
- Added org.opencontainers.image.source annotation to the Docker image
|
||||||
- Improved the moderation action logs interface
|
- Improved the moderation action logs interface
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
@ -427,7 +1104,7 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
|
||||||
- Fixed getting metadata from tweets when creating a resource
|
- Fixed getting metadata from tweets when creating a resource
|
||||||
- Fixed bad handling of duplicate usernames
|
- Fixed bad handling of duplicate usernames
|
||||||
- Fixed handling of bad URIs to proxify
|
- Fixed handling of bad URIs to proxify
|
||||||
- Fixed creating discussion with title containing only spaces
|
- Fixed creating discussion with title containing only spaces
|
||||||
- Fixed registering new user account with same email as unconfirmed
|
- Fixed registering new user account with same email as unconfirmed
|
||||||
- Fixed handling changing default actor unlogged
|
- Fixed handling changing default actor unlogged
|
||||||
- Fixed handling getting organized events from an actor when not authorized
|
- Fixed handling getting organized events from an actor when not authorized
|
||||||
|
@ -499,6 +1176,7 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
|
||||||
- Hungarian
|
- Hungarian
|
||||||
- Russian
|
- Russian
|
||||||
- Spanish
|
- Spanish
|
||||||
|
|
||||||
## 1.1.0-rc.1 - 2021-03-29
|
## 1.1.0-rc.1 - 2021-03-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -524,7 +1202,7 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
|
||||||
- Fixed getting metadata from tweets when creating a resource
|
- Fixed getting metadata from tweets when creating a resource
|
||||||
- Fixed bad handling of duplicate usernames
|
- Fixed bad handling of duplicate usernames
|
||||||
- Fixed handling of bad URIs to proxify
|
- Fixed handling of bad URIs to proxify
|
||||||
- Fixed creating discussion with title containing only spaces
|
- Fixed creating discussion with title containing only spaces
|
||||||
- Fixed registering new user account with same email as unconfirmed
|
- Fixed registering new user account with same email as unconfirmed
|
||||||
- Fixed handling changing default actor unlogged
|
- Fixed handling changing default actor unlogged
|
||||||
- Fixed handling getting organized events from an actor when not authorized
|
- Fixed handling getting organized events from an actor when not authorized
|
||||||
|
@ -546,11 +1224,13 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
|
||||||
## 1.1.0-beta.6 - 2021-03-17
|
## 1.1.0-beta.6 - 2021-03-17
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed a typo in range/radius showing the wrong radius for close events on homepage
|
- Fixed a typo in range/radius showing the wrong radius for close events on homepage
|
||||||
|
|
||||||
## 1.1.0-beta.5 - 2021-03-17
|
## 1.1.0-beta.5 - 2021-03-17
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed a typo in range/radius preventing close events from showing up
|
- Fixed a typo in range/radius preventing close events from showing up
|
||||||
|
|
||||||
## 1.1.0-beta.4 - 2021-03-17
|
## 1.1.0-beta.4 - 2021-03-17
|
||||||
|
@ -564,15 +1244,18 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
|
||||||
## 1.1.0-beta.3 - 2021-03-16
|
## 1.1.0-beta.3 - 2021-03-16
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Handle ActivityPub Fetcher returning text that's not JSON
|
- Handle ActivityPub Fetcher returning text that's not JSON
|
||||||
- Fix accessing a group profile when not a member
|
- Fix accessing a group profile when not a member
|
||||||
|
|
||||||
## 1.1.0-beta.2 - 2021-03-16
|
## 1.1.0-beta.2 - 2021-03-16
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed geospatial configuration only being evaluated at compile-time, not at runtime
|
- Fixed geospatial configuration only being evaluated at compile-time, not at runtime
|
||||||
|
|
||||||
### Translations
|
### Translations
|
||||||
|
|
||||||
- Slovenian
|
- Slovenian
|
||||||
|
|
||||||
## 1.1.0-beta.1 - 2021-03-10
|
## 1.1.0-beta.1 - 2021-03-10
|
||||||
|
@ -961,7 +1644,7 @@ Updated translations:
|
||||||
MIX_ENV=prod mix deps.clean mime --build
|
MIX_ENV=prod mix deps.clean mime --build
|
||||||
```
|
```
|
||||||
|
|
||||||
- The [nginx configuration](https://framagit.org/framasoft/mobilizon/-/blob/master/support/nginx/mobilizon.conf) has been changed with improvements and support for custom error pages.
|
- The [nginx configuration](https://framagit.org/framasoft/mobilizon/-/blob/main/support/nginx/mobilizon.conf) has been changed with improvements and support for custom error pages.
|
||||||
|
|
||||||
- The cmake dependency has been added (see [our documentation](https://docs.joinmobilizon.org/administration/dependencies/#basic-tools))
|
- The cmake dependency has been added (see [our documentation](https://docs.joinmobilizon.org/administration/dependencies/#basic-tools))
|
||||||
|
|
||||||
|
@ -1003,9 +1686,9 @@ Config has moved from `.env` files to a more traditional way to handle things in
|
||||||
|
|
||||||
To migrate existing configuration, you can simply run `mix mobilizon.instance gen` and fill in the adequate values previously in `.env` files (you don't need to perform the operations to create the database).
|
To migrate existing configuration, you can simply run `mix mobilizon.instance gen` and fill in the adequate values previously in `.env` files (you don't need to perform the operations to create the database).
|
||||||
|
|
||||||
A minimal file template [is available](https://framagit.org/framasoft/mobilizon/blob/master/priv/templates/config.template.eex) to check for missing configuration.
|
A minimal file template [is available](https://framagit.org/framasoft/mobilizon/blob/main/priv/templates/config.template.eex) to check for missing configuration.
|
||||||
|
|
||||||
Also make sure to remove the `EnvironmentFile=` line from the systemd service and set `Environment=MIX_ENV=prod` instead. See [the updated file](https://framagit.org/framasoft/mobilizon/blob/master/support/systemd/mobilizon.service).
|
Also make sure to remove the `EnvironmentFile=` line from the systemd service and set `Environment=MIX_ENV=prod` instead. See [the updated file](https://framagit.org/framasoft/mobilizon/blob/main/support/systemd/mobilizon.service).
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
FROM elixir:alpine
|
FROM elixir:alpine
|
||||||
|
|
||||||
RUN apk add --no-cache inotify-tools postgresql-client yarn file make gcc libc-dev argon2 imagemagick cmake build-base libwebp-tools bash ncurses git
|
RUN apk add --no-cache inotify-tools postgresql-client yarn file make gcc libc-dev argon2 imagemagick cmake build-base libwebp-tools bash ncurses git python3
|
||||||
|
|
||||||
RUN mix local.hex --force && mix local.rebar --force
|
RUN mix local.hex --force && mix local.rebar --force
|
||||||
|
|
||||||
|
|
18
Makefile
18
Makefile
|
@ -1,5 +1,5 @@
|
||||||
init:
|
init:
|
||||||
@bash docker/message.sh "start"
|
@bash docker/message.sh "Start"
|
||||||
make start
|
make start
|
||||||
|
|
||||||
setup: stop
|
setup: stop
|
||||||
|
@ -10,16 +10,18 @@ migrate:
|
||||||
logs:
|
logs:
|
||||||
docker-compose logs -f
|
docker-compose logs -f
|
||||||
start: stop
|
start: stop
|
||||||
@bash docker/message.sh "starting Mobilizon with docker"
|
@bash docker/message.sh "Starting Mobilizon with Docker"
|
||||||
docker-compose up -d api
|
docker-compose up -d api
|
||||||
@bash docker/message.sh "Docker server started."
|
@bash docker/message.sh "Docker server started"
|
||||||
stop:
|
stop:
|
||||||
@bash docker/message.sh "stopping Mobilizon"
|
@bash docker/message.sh "Stopping Mobilizon"
|
||||||
docker-compose down
|
docker-compose down
|
||||||
@bash docker/message.sh "stopped"
|
@bash docker/message.sh "Mobilizon is stopped"
|
||||||
test: stop
|
test: stop
|
||||||
@bash docker/message.sh "Running tests"
|
@bash docker/message.sh "Running tests"
|
||||||
docker-compose -f docker-compose.yml -f docker-compose.test.yml run api mix test
|
docker-compose -f docker-compose.yml -f docker-compose.test.yml run api mix test $(only)
|
||||||
@bash docker/message.sh "Tests runned"
|
@bash docker/message.sh "Done running tests"
|
||||||
|
format:
|
||||||
|
docker-compose run --rm api bash -c "mix format && mix credo --strict"
|
||||||
|
@bash docker/message.sh "Code is now ready to commit :)"
|
||||||
target: init
|
target: init
|
||||||
|
|
179
UPGRADE.md
179
UPGRADE.md
|
@ -1,37 +1,182 @@
|
||||||
|
# Upgrading from 2.0 to 2.1
|
||||||
|
|
||||||
|
## Mailer library change
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
The change is already applied. You may remove the `MOBILIZON_SMTP_HOSTNAME` environment key which is not used anymore.
|
||||||
|
|
||||||
|
### Release and source mode
|
||||||
|
|
||||||
|
In your configuration file under `config :mobilizon, Mobilizon.Web.Email.Mailer`,
|
||||||
|
|
||||||
|
- Change `Bamboo.SMTPAdapter` to `Swoosh.Adapters.SMTP`,
|
||||||
|
- rename the `server` key to `relay`
|
||||||
|
- remove the `hostname` key,
|
||||||
|
- the default value of the username and password fields is an empty string and no longer `nil`.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
config :mobilizon, Mobilizon.Web.Email.Mailer,
|
||||||
|
- adapter: Bamboo.SMTPAdapter,
|
||||||
|
+ adapter: Swoosh.Adapters.SMTP,
|
||||||
|
- server: "localhost",
|
||||||
|
+ relay: "localhost",
|
||||||
|
- hostname: "localhost",
|
||||||
|
# usually 25, 465 or 587
|
||||||
|
port: 25,
|
||||||
|
- username: nil,
|
||||||
|
+ username: "",
|
||||||
|
- password: nil,
|
||||||
|
+ password: "",
|
||||||
|
# can be `:always` or `:never`
|
||||||
|
tls: :if_available,
|
||||||
|
allowed_tls_versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
|
||||||
|
retries: 1,
|
||||||
|
# can be `true`
|
||||||
|
no_mx_lookups: false,
|
||||||
|
# can be `:always`. If your smtp relay requires authentication set it to `:always`.
|
||||||
|
auth: :if_available
|
||||||
|
```
|
||||||
|
|
||||||
|
# Upgrading from 1.3 to 2.0
|
||||||
|
|
||||||
|
Requirements dependencies depend on the way Mobilizon is installed.
|
||||||
|
|
||||||
|
## New Elixir version requirement
|
||||||
|
|
||||||
|
### Docker and Release install
|
||||||
|
|
||||||
|
You are already using latest Elixir version in the release tarball and Docker images.
|
||||||
|
|
||||||
|
### Source install
|
||||||
|
|
||||||
|
**Elixir 1.12 and Erlang OTP 22 are now required**. If your distribution or the repositories from Erlang Solutions don't provide these versions, you need to uninstall the current versions and install [Elixir](https://github.com/asdf-vm/asdf-elixir) through the [ASDF tool](https://asdf-vm.com/).
|
||||||
|
|
||||||
|
## Geographic timezone data
|
||||||
|
|
||||||
|
Mobilizon 2.0 uses data based on [timezone-boundary-builder](https://github.com/evansiroky/timezone-boundary-builder) (which is based itself on OpenStreetMap data) to determine the timezone of an event automatically, based on it's geocoordinates. However, this needs ~700Mio of disk, so we don't redistribute data directly, depending on the case. It's possible to skip this part, but users will need to manually pick the timezone for every event they created when it has a different timezone from their own.
|
||||||
|
|
||||||
|
### Docker install
|
||||||
|
|
||||||
|
The geographic timezone data is already bundled into the image, you have nothing to do.
|
||||||
|
|
||||||
|
### Release install
|
||||||
|
|
||||||
|
In order to keep the release tarballs light, the geographic timezone data is not bundled directly. You need to download the data :
|
||||||
|
|
||||||
|
- either raw from Github, but **requires an extra ~1Gio of memory** to process the data
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo -u mobilizon mkdir /var/lib/mobilizon/timezones
|
||||||
|
sudo -u mobilizon ./bin/mobilizon_ctl tz_world.update
|
||||||
|
```
|
||||||
|
|
||||||
|
- either already processed from our own distribution server
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo -u mobilizon mkdir /var/lib/mobilizon/timezones
|
||||||
|
sudo -u mobilizon curl -L 'https://packages.joinmobilizon.org/tz_world/timezones-geodata.dets' -o /var/lib/mobilizon/timezones/timezones-geodata.dets
|
||||||
|
```
|
||||||
|
|
||||||
|
In both cases, ~700Mio of disk will be used. You may use the following configuration to specify where the data is expected if you decide to change it from the default location (`/var/lib/mobilizon/timezones`) :
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
config :tz_world, data_dir: "/some/place"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source install
|
||||||
|
|
||||||
|
You need to download the data :
|
||||||
|
|
||||||
|
- either raw from Github, but **requires an extra ~1Gio of memory** to process the data
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo -u mobilizon mkdir /var/lib/mobilizon/timezones
|
||||||
|
sudo -u mobilizon mix mobilizon.tz_world.update
|
||||||
|
```
|
||||||
|
|
||||||
|
- either already processed from our own distribution server
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo -u mobilizon mkdir /var/lib/mobilizon/timezones
|
||||||
|
sudo -u mobilizon curl -L 'https://packages.joinmobilizon.org/tz_world/timezones-geodata.dets' -o /var/lib/mobilizon/timezones/timezones-geodata.dets
|
||||||
|
```
|
||||||
|
|
||||||
|
In both cases, ~700Mio of disk will be used. You may use the following configuration to specify where the data is expected:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
config :tz_world, data_dir: "/some/place"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exports folder
|
||||||
|
|
||||||
|
Create the folder for default CSV export:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo -u mobilizon mkdir -p /var/lib/mobilizon/uploads/exports/csv
|
||||||
|
```
|
||||||
|
|
||||||
|
This path can be configured, see [the dedicated docs page about this](https://docs.joinmobilizon.org/administration/configure/exports/).
|
||||||
|
Files in this folder are temporary and are cleaned once an hour.
|
||||||
|
|
||||||
|
## New optional dependencies
|
||||||
|
|
||||||
|
These are optional, installing them will allow Mobilizon to export to PDF and ODS as well. Mobilizon 2.0 allows to export the participant list, but more is planned.
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
Everything is included in our Docker image.
|
||||||
|
|
||||||
|
### Release and source install
|
||||||
|
|
||||||
|
New optional Python dependencies:
|
||||||
|
|
||||||
|
- `Python` >= 3.6
|
||||||
|
- `weasyprint` for PDF export (with [a few extra dependencies](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html))
|
||||||
|
- `pyexcel-ods3` for ODS export (no extra dependencies)
|
||||||
|
|
||||||
|
Both can be installed through pip. You need to enable and configure exports for PDF and ODS in the configuration afterwards. Read [the dedicated docs page about this](https://docs.joinmobilizon.org/administration/configure/exports/).
|
||||||
|
|
||||||
# Upgrading from 1.0 to 1.1
|
# Upgrading from 1.0 to 1.1
|
||||||
|
|
||||||
The 1.1 version of Mobilizon brings Elixir releases support. An Elixir release is a self-contained directory that contains all of Mobilizon's code (front-end and backend), it's dependencies, as well as the Erlang Virtual Machine and runtime (only the parts you need). As long as the release has been assembled on the same OS and architecture, it can be deploy and run straight away. [Read more about releases](https://elixir-lang.org/getting-started/mix-otp/config-and-releases.html#releases).
|
The 1.1 version of Mobilizon brings Elixir releases support. An Elixir release is a self-contained directory that contains all of Mobilizon's code (front-end and backend), it's dependencies, as well as the Erlang Virtual Machine and runtime (only the parts you need). As long as the release has been assembled on the same OS and architecture, it can be deploy and run straight away. [Read more about releases](https://elixir-lang.org/getting-started/mix-otp/config-and-releases.html#releases).
|
||||||
|
|
||||||
## Comparison
|
## Comparison
|
||||||
|
|
||||||
Migrating to releases means:
|
Migrating to releases means:
|
||||||
* You only get a precompiled binary, so you avoid compilation times when updating
|
|
||||||
* No need to have Elixir/NodeJS installed on the system
|
- You only get a precompiled binary, so you avoid compilation times when updating
|
||||||
* Code/data/config location is more common (/opt, /var/lib, /etc)
|
- No need to have Elixir/NodeJS installed on the system
|
||||||
* More efficient, as only what you need from the Elixir/Erlang standard libraries is included and all of the code is directly preloaded
|
- Code/data/config location is more common (/opt, /var/lib, /etc)
|
||||||
* You can't hardcode modifications in Mobilizon's code
|
- More efficient, as only what you need from the Elixir/Erlang standard libraries is included and all of the code is directly preloaded
|
||||||
|
- You can't hardcode modifications in Mobilizon's code
|
||||||
|
|
||||||
Staying on source releases means:
|
Staying on source releases means:
|
||||||
* You need to recompile everything with each update
|
|
||||||
* Compiling frontend and backend has higher system requirements than just running Mobilizon
|
- You need to recompile everything with each update
|
||||||
* You can change things in Mobilizon's code and recompile right away to test changes
|
- Compiling frontend and backend has higher system requirements than just running Mobilizon
|
||||||
|
- You can change things in Mobilizon's code and recompile right away to test changes
|
||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
If you want to migrate to releases, [we provide a full guide](https://docs.joinmobilizon.org/administration/upgrading/source_to_release/). You may do this at any time.
|
If you want to migrate to releases, [we provide a full guide](https://docs.joinmobilizon.org/administration/upgrading/source_to_release/). You may do this at any time.
|
||||||
|
|
||||||
## Source install
|
## Source install
|
||||||
|
|
||||||
To stay on a source release, you just need to check the following things:
|
To stay on a source release, you just need to check the following things:
|
||||||
* Rename your configuration file `config/prod.secret.exs` to `config/runtime.exs`.
|
|
||||||
* If your config file includes `server: true` under `Mobilizon.Web.Endpoint`, remove it.
|
- Rename your configuration file `config/prod.secret.exs` to `config/runtime.exs`.
|
||||||
```diff
|
- If your config file includes `server: true` under `Mobilizon.Web.Endpoint`, remove it.
|
||||||
config :mobilizon, Mobilizon.Web.Endpoint,
|
```diff
|
||||||
- server: true,
|
config :mobilizon, Mobilizon.Web.Endpoint,
|
||||||
```
|
- server: true,
|
||||||
* The uploads default directory is now `/var/lib/mobilizon/uploads`. To keep it in the previous `uploads/` directory, just add the following line to `config/runtime.exs`:
|
```
|
||||||
|
- The uploads default directory is now `/var/lib/mobilizon/uploads`. To keep it in the previous `uploads/` directory, just add the following line to `config/runtime.exs`:
|
||||||
```elixir
|
```elixir
|
||||||
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, uploads: "uploads"
|
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, uploads: "uploads"
|
||||||
```
|
```
|
||||||
Or you may use any other directory where the `mobilizon` user has write permissions.
|
Or you may use any other directory where the `mobilizon` user has write permissions.
|
||||||
* The GeoIP database default directory is now `/var/lib/mobilizon/geo/GeoLite2-City.mmdb`. To keep it in the previous `priv/data/GeoLite2-City.mmdb` directory, just add the following line to `config/runtime.exs`:
|
- The GeoIP database default directory is now `/var/lib/mobilizon/geo/GeoLite2-City.mmdb`. To keep it in the previous `priv/data/GeoLite2-City.mmdb` directory, just add the following line to `config/runtime.exs`:
|
||||||
```elixir
|
```elixir
|
||||||
config :geolix, databases: [
|
config :geolix, databases: [
|
||||||
%{
|
%{
|
||||||
|
@ -41,4 +186,4 @@ To stay on a source release, you just need to check the following things:
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
Or you may use any other directory where the `mobilizon` user has read permissions.
|
Or you may use any other directory where the `mobilizon` user has read permissions.
|
||||||
|
|
|
@ -13,8 +13,8 @@ config :mobilizon,
|
||||||
config :mobilizon, Mobilizon.Storage.Repo, types: Mobilizon.Storage.PostgresTypes
|
config :mobilizon, Mobilizon.Storage.Repo, types: Mobilizon.Storage.PostgresTypes
|
||||||
|
|
||||||
config :mobilizon, :instance,
|
config :mobilizon, :instance,
|
||||||
name: "mobilizon du chapril",
|
name: "Mobilizon du Chapril",
|
||||||
description: "instance du chapril",
|
description: "Instance du Chapril",
|
||||||
hostname: "localhost",
|
hostname: "localhost",
|
||||||
registrations_open: true,
|
registrations_open: true,
|
||||||
registration_email_allowlist: [],
|
registration_email_allowlist: [],
|
||||||
|
@ -35,14 +35,16 @@ config :mobilizon, :instance,
|
||||||
unconfirmed_user_grace_period_hours: 48,
|
unconfirmed_user_grace_period_hours: 48,
|
||||||
activity_expire_days: 365,
|
activity_expire_days: 365,
|
||||||
activity_keep_number: 100,
|
activity_keep_number: 100,
|
||||||
enable_instance_feeds: false,
|
enable_instance_feeds: true,
|
||||||
email_from: "noreply@localhost",
|
email_from: "noreply@mobilizon.chapril.org",
|
||||||
email_reply_to: "noreply@localhost"
|
email_reply_to: "noreply@mobilizon.chapril.org"
|
||||||
|
|
||||||
config :mobilizon, :groups, enabled: true
|
config :mobilizon, :groups, enabled: true
|
||||||
|
|
||||||
config :mobilizon, :events, creation: true
|
config :mobilizon, :events, creation: true
|
||||||
|
|
||||||
|
config :mobilizon, :restrictions, only_admin_can_create_groups: false
|
||||||
|
config :mobilizon, :restrictions, only_groups_can_create_events: false
|
||||||
|
|
||||||
# Configures the endpoint
|
# Configures the endpoint
|
||||||
config :mobilizon, Mobilizon.Web.Endpoint,
|
config :mobilizon, Mobilizon.Web.Endpoint,
|
||||||
url: [
|
url: [
|
||||||
|
@ -86,6 +88,10 @@ config :mobilizon, Mobilizon.Web.Upload,
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, uploads: "/var/lib/mobilizon/uploads"
|
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, uploads: "/var/lib/mobilizon/uploads"
|
||||||
|
|
||||||
|
config :tz_world, data_dir: "/var/lib/mobilizon/timezones"
|
||||||
|
|
||||||
|
config :mobilizon, Timex.Gettext, default_locale: "fr"
|
||||||
|
|
||||||
config :mobilizon, :media_proxy,
|
config :mobilizon, :media_proxy,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
proxy_opts: [
|
proxy_opts: [
|
||||||
|
@ -100,13 +106,16 @@ config :mobilizon, :media_proxy,
|
||||||
]
|
]
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Web.Email.Mailer,
|
config :mobilizon, Mobilizon.Web.Email.Mailer,
|
||||||
adapter: Bamboo.SMTPAdapter,
|
adapter: Swoosh.Adapters.SMTP,
|
||||||
server: "localhost",
|
relay: "localhost",
|
||||||
hostname: "localhost",
|
|
||||||
# usually 25, 465 or 587
|
# usually 25, 465 or 587
|
||||||
port: 25,
|
port: 25,
|
||||||
username: nil,
|
username: "",
|
||||||
password: nil,
|
password: "",
|
||||||
|
# can be `:always` or `:never`
|
||||||
|
auth: :if_available,
|
||||||
|
# can be `true`
|
||||||
|
ssl: false,
|
||||||
# can be `:always` or `:never`
|
# can be `:always` or `:never`
|
||||||
tls: :if_available,
|
tls: :if_available,
|
||||||
allowed_tls_versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
|
allowed_tls_versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
|
||||||
|
@ -179,6 +188,14 @@ config :phoenix, :filter_parameters, ["password", "token"]
|
||||||
config :absinthe, schema: Mobilizon.GraphQL.Schema
|
config :absinthe, schema: Mobilizon.GraphQL.Schema
|
||||||
config :absinthe, Absinthe.Logger, filter_variables: ["token", "password", "secret"]
|
config :absinthe, Absinthe.Logger, filter_variables: ["token", "password", "secret"]
|
||||||
|
|
||||||
|
config :codepagex, :encodings, [
|
||||||
|
:ascii,
|
||||||
|
~r[iso8859]i,
|
||||||
|
:"VENDORS/MICSFT/WINDOWS/CP1252"
|
||||||
|
]
|
||||||
|
|
||||||
|
config :mobilizon, Mobilizon.Web.Gettext, split_module_by: [:locale, :domain]
|
||||||
|
|
||||||
config :ex_cldr,
|
config :ex_cldr,
|
||||||
default_locale: "en",
|
default_locale: "en",
|
||||||
default_backend: Mobilizon.Cldr
|
default_backend: Mobilizon.Cldr
|
||||||
|
@ -189,14 +206,17 @@ config :http_signatures,
|
||||||
config :mobilizon, :cldr,
|
config :mobilizon, :cldr,
|
||||||
locales: [
|
locales: [
|
||||||
"fr",
|
"fr",
|
||||||
"en"
|
"en",
|
||||||
|
"ru",
|
||||||
|
"ar"
|
||||||
]
|
]
|
||||||
|
|
||||||
config :mobilizon, :activitypub,
|
config :mobilizon, :activitypub,
|
||||||
# One day
|
# One day
|
||||||
actor_stale_period: 3_600 * 48,
|
actor_stale_period: 3_600 * 48,
|
||||||
actor_key_rotation_delay: 3_600 * 48,
|
actor_key_rotation_delay: 3_600 * 48,
|
||||||
sign_object_fetches: true
|
sign_object_fetches: true,
|
||||||
|
stale_actor_search_exclusion_after: 3_600 * 24 * 7
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim
|
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim
|
||||||
|
|
||||||
|
@ -280,8 +300,10 @@ config :mobilizon, Oban,
|
||||||
crontab: [
|
crontab: [
|
||||||
{"@hourly", Mobilizon.Service.Workers.BuildSiteMap, queue: :background},
|
{"@hourly", Mobilizon.Service.Workers.BuildSiteMap, queue: :background},
|
||||||
{"17 4 * * *", Mobilizon.Service.Workers.RefreshGroups, queue: :background},
|
{"17 4 * * *", Mobilizon.Service.Workers.RefreshGroups, queue: :background},
|
||||||
|
{"36 * * * *", Mobilizon.Service.Workers.RefreshInstances, queue: :background},
|
||||||
{"@hourly", Mobilizon.Service.Workers.CleanOrphanMediaWorker, queue: :background},
|
{"@hourly", Mobilizon.Service.Workers.CleanOrphanMediaWorker, queue: :background},
|
||||||
{"@hourly", Mobilizon.Service.Workers.CleanUnconfirmedUsersWorker, queue: :background},
|
{"@hourly", Mobilizon.Service.Workers.CleanUnconfirmedUsersWorker, queue: :background},
|
||||||
|
{"@hourly", Mobilizon.Service.Workers.ExportCleanerWorker, queue: :background},
|
||||||
{"@hourly", Mobilizon.Service.Workers.SendActivityRecapWorker, queue: :notifications},
|
{"@hourly", Mobilizon.Service.Workers.SendActivityRecapWorker, queue: :notifications},
|
||||||
{"@daily", Mobilizon.Service.Workers.CleanOldActivityWorker, queue: :background}
|
{"@daily", Mobilizon.Service.Workers.CleanOldActivityWorker, queue: :background}
|
||||||
]},
|
]},
|
||||||
|
@ -317,6 +339,16 @@ config :mobilizon, Mobilizon.Service.Notifier.Email, enabled: true
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Service.Notifier.Push, enabled: true
|
config :mobilizon, Mobilizon.Service.Notifier.Push, enabled: true
|
||||||
|
|
||||||
|
config :mobilizon, :exports,
|
||||||
|
path: "/var/lib/mobilizon/uploads/exports",
|
||||||
|
formats: [
|
||||||
|
Mobilizon.Service.Export.Participants.CSV,
|
||||||
|
Mobilizon.Service.Export.Participants.PDF,
|
||||||
|
Mobilizon.Service.Export.Participants.ODS
|
||||||
|
]
|
||||||
|
|
||||||
|
config :mobilizon, :analytics, providers: []
|
||||||
|
|
||||||
# Import environment specific config. This must remain at the bottom
|
# Import environment specific config. This must remain at the bottom
|
||||||
# of this file so it overrides the configuration defined above.
|
# of this file so it overrides the configuration defined above.
|
||||||
import_config "#{config_env()}.exs"
|
import_config "#{config_env()}.exs"
|
||||||
|
|
|
@ -58,6 +58,8 @@ config :logger, :console, format: "[$level] $message\n", level: :debug
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim
|
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim
|
||||||
|
|
||||||
|
config :mobilizon, Mobilizon.Web.Gettext, allowed_locales: ["fr", "en", "ru", "ar"]
|
||||||
|
|
||||||
# Set a higher stacktrace during development. Avoid configuring such
|
# Set a higher stacktrace during development. Avoid configuring such
|
||||||
# in production as building large stacktraces may be expensive.
|
# in production as building large stacktraces may be expensive.
|
||||||
config :phoenix, :stacktrace_depth, 20
|
config :phoenix, :stacktrace_depth, 20
|
||||||
|
@ -65,7 +67,7 @@ config :phoenix, :stacktrace_depth, 20
|
||||||
# Initialize plugs at runtime for faster development compilation
|
# Initialize plugs at runtime for faster development compilation
|
||||||
config :phoenix, :plug_init_mode, :runtime
|
config :phoenix, :plug_init_mode, :runtime
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Web.Email.Mailer, adapter: Bamboo.LocalAdapter
|
config :mobilizon, Mobilizon.Web.Email.Mailer, adapter: Swoosh.Adapters.Local
|
||||||
|
|
||||||
# Configure your database
|
# Configure your database
|
||||||
config :mobilizon, Mobilizon.Storage.Repo,
|
config :mobilizon, Mobilizon.Storage.Repo,
|
||||||
|
@ -92,6 +94,10 @@ config :mobilizon, Mobilizon.Web.Auth.Guardian,
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, uploads: "uploads"
|
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, uploads: "uploads"
|
||||||
|
|
||||||
|
config :mobilizon, :exports, path: "uploads/exports"
|
||||||
|
|
||||||
|
config :tz_world, data_dir: "_build/dev/lib/tz_world/priv"
|
||||||
|
|
||||||
config :mobilizon, :anonymous,
|
config :mobilizon, :anonymous,
|
||||||
reports: [
|
reports: [
|
||||||
allowed: true
|
allowed: true
|
||||||
|
|
|
@ -33,9 +33,6 @@ config :mobilizon, :instance,
|
||||||
email_from: System.get_env("MOBILIZON_INSTANCE_EMAIL", "noreply@mobilizon.lan"),
|
email_from: System.get_env("MOBILIZON_INSTANCE_EMAIL", "noreply@mobilizon.lan"),
|
||||||
email_reply_to: System.get_env("MOBILIZON_REPLY_EMAIL", "noreply@mobilizon.lan")
|
email_reply_to: System.get_env("MOBILIZON_REPLY_EMAIL", "noreply@mobilizon.lan")
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local,
|
|
||||||
uploads: System.get_env("MOBILIZON_UPLOADS", "/app/uploads")
|
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Storage.Repo,
|
config :mobilizon, Mobilizon.Storage.Repo,
|
||||||
adapter: Ecto.Adapters.Postgres,
|
adapter: Ecto.Adapters.Postgres,
|
||||||
username: System.get_env("MOBILIZON_DATABASE_USERNAME", "username"),
|
username: System.get_env("MOBILIZON_DATABASE_USERNAME", "username"),
|
||||||
|
@ -46,9 +43,8 @@ config :mobilizon, Mobilizon.Storage.Repo,
|
||||||
pool_size: 10
|
pool_size: 10
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Web.Email.Mailer,
|
config :mobilizon, Mobilizon.Web.Email.Mailer,
|
||||||
adapter: Bamboo.SMTPAdapter,
|
adapter: Swoosh.Adapters.SMTP,
|
||||||
server: System.get_env("MOBILIZON_SMTP_SERVER", "localhost"),
|
relay: System.get_env("MOBILIZON_SMTP_SERVER", "localhost"),
|
||||||
hostname: System.get_env("MOBILIZON_SMTP_HOSTNAME", "localhost"),
|
|
||||||
port: System.get_env("MOBILIZON_SMTP_PORT", "25"),
|
port: System.get_env("MOBILIZON_SMTP_PORT", "25"),
|
||||||
username: System.get_env("MOBILIZON_SMTP_USERNAME", nil),
|
username: System.get_env("MOBILIZON_SMTP_USERNAME", nil),
|
||||||
password: System.get_env("MOBILIZON_SMTP_PASSWORD", nil),
|
password: System.get_env("MOBILIZON_SMTP_PASSWORD", nil),
|
||||||
|
@ -68,4 +64,16 @@ config :geolix,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, uploads: "/var/lib/mobilizon/uploads"
|
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local,
|
||||||
|
uploads: System.get_env("MOBILIZON_UPLOADS", "/var/lib/mobilizon/uploads")
|
||||||
|
|
||||||
|
config :mobilizon, :exports,
|
||||||
|
path: System.get_env("MOBILIZON_UPLOADS_EXPORTS", "/var/lib/mobilizon/uploads/exports"),
|
||||||
|
formats: [
|
||||||
|
Mobilizon.Service.Export.Participants.CSV,
|
||||||
|
Mobilizon.Service.Export.Participants.PDF,
|
||||||
|
Mobilizon.Service.Export.Participants.ODS
|
||||||
|
]
|
||||||
|
|
||||||
|
config :tz_world,
|
||||||
|
data_dir: System.get_env("MOBILIZON_TIMEZONES_DIR", "/var/lib/mobilizon/timezones")
|
||||||
|
|
|
@ -16,33 +16,29 @@ config :logger, level: :info
|
||||||
# Load all locales in production
|
# Load all locales in production
|
||||||
config :mobilizon, :cldr,
|
config :mobilizon, :cldr,
|
||||||
locales: [
|
locales: [
|
||||||
"ar",
|
# "ar",
|
||||||
"be",
|
# "be",
|
||||||
"ca",
|
# "bn",
|
||||||
"cs",
|
# "ca",
|
||||||
"de",
|
# "cs",
|
||||||
|
# "cy",
|
||||||
|
# "de",
|
||||||
"en",
|
"en",
|
||||||
"es",
|
# "es",
|
||||||
"fi",
|
# "fa",
|
||||||
|
# "fi",
|
||||||
"fr",
|
"fr",
|
||||||
"gl",
|
# "gd",
|
||||||
"hu",
|
# "gl",
|
||||||
"it",
|
# "hu",
|
||||||
"ja",
|
# "id",
|
||||||
"nl",
|
# "it",
|
||||||
"nn",
|
# "ja",
|
||||||
"oc",
|
# "nl",
|
||||||
"pl",
|
# "nn",
|
||||||
"pt",
|
# "pl",
|
||||||
"ru",
|
# "pt",
|
||||||
"sv"
|
# "ru",
|
||||||
|
# "sv",
|
||||||
|
# "zh_Hant"
|
||||||
]
|
]
|
||||||
|
|
||||||
cond do
|
|
||||||
System.get_env("INSTANCE_CONFIG") &&
|
|
||||||
File.exists?("./config/#{System.get_env("INSTANCE_CONFIG")}") ->
|
|
||||||
import_config System.get_env("INSTANCE_CONFIG")
|
|
||||||
|
|
||||||
true ->
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
|
@ -54,21 +54,28 @@ config :mobilizon, :ldap,
|
||||||
bind_uid: System.get_env("LDAP_BIND_UID"),
|
bind_uid: System.get_env("LDAP_BIND_UID"),
|
||||||
bind_password: System.get_env("LDAP_BIND_PASSWORD")
|
bind_password: System.get_env("LDAP_BIND_PASSWORD")
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Web.Email.Mailer, adapter: Bamboo.TestAdapter
|
config :mobilizon, Mobilizon.Web.Email.Mailer, adapter: Swoosh.Adapters.Test
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Web.Upload, filters: [], link_name: false
|
config :mobilizon, Mobilizon.Web.Upload, filters: [], link_name: false
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, uploads: "test/uploads"
|
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, uploads: "test/uploads"
|
||||||
|
|
||||||
config :exvcr,
|
config :mobilizon, :exports, path: "test/uploads/exports"
|
||||||
vcr_cassette_library_dir: "test/fixtures/vcr_cassettes"
|
|
||||||
|
config :tz_world, data_dir: "_build/test/lib/tz_world/priv"
|
||||||
|
|
||||||
config :tesla, Mobilizon.Service.HTTP.ActivityPub,
|
config :tesla, Mobilizon.Service.HTTP.ActivityPub,
|
||||||
adapter: Mobilizon.Service.HTTP.ActivityPub.Mock
|
adapter: Mobilizon.Service.HTTP.ActivityPub.Mock
|
||||||
|
|
||||||
|
config :tesla, Mobilizon.Service.HTTP.WebfingerClient,
|
||||||
|
adapter: Mobilizon.Service.HTTP.WebfingerClient.Mock
|
||||||
|
|
||||||
config :tesla, Mobilizon.Service.HTTP.GeospatialClient,
|
config :tesla, Mobilizon.Service.HTTP.GeospatialClient,
|
||||||
adapter: Mobilizon.Service.HTTP.GeospatialClient.Mock
|
adapter: Mobilizon.Service.HTTP.GeospatialClient.Mock
|
||||||
|
|
||||||
|
config :tesla, Mobilizon.Service.HTTP.HostMetaClient,
|
||||||
|
adapter: Mobilizon.Service.HTTP.HostMetaClient.Mock
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Mock
|
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Mock
|
||||||
|
|
||||||
config :mobilizon, Oban, queues: false, plugins: false
|
config :mobilizon, Oban, queues: false, plugins: false
|
||||||
|
@ -77,6 +84,8 @@ config :mobilizon, Mobilizon.Web.Auth.Guardian, secret_key: "some secret"
|
||||||
|
|
||||||
config :mobilizon, :activitypub, sign_object_fetches: false
|
config :mobilizon, :activitypub, sign_object_fetches: false
|
||||||
|
|
||||||
|
config :mobilizon, Mobilizon.Web.Gettext, allowed_locales: ["fr", "en", "es", "ru"]
|
||||||
|
|
||||||
config :junit_formatter, report_dir: "."
|
config :junit_formatter, report_dir: "."
|
||||||
|
|
||||||
if System.get_env("DOCKER", "false") == "false" && File.exists?("./config/test.secret.exs") do
|
if System.get_env("DOCKER", "false") == "false" && File.exists?("./config/test.secret.exs") do
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
version: "3"
|
version: "3.2"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
|
44
docker/multiarch/Dockerfile
Normal file
44
docker/multiarch/Dockerfile
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
FROM elixir as build
|
||||||
|
SHELL ["/bin/bash", "-c"]
|
||||||
|
ENV MIX_ENV prod
|
||||||
|
# ENV LANG en_US.UTF-8
|
||||||
|
ARG APP_ASSET
|
||||||
|
|
||||||
|
# Set the right versions
|
||||||
|
ENV ELIXIR_VERSION latest
|
||||||
|
ENV ERLANG_VERSION latest
|
||||||
|
ENV NODE_VERSION 16
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update -yq && apt-get install -yq build-essential cmake postgresql-client git curl gnupg unzip exiftool webp imagemagick gifsicle
|
||||||
|
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
# # Install Node & yarn
|
||||||
|
# RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install nodejs -yq
|
||||||
|
# RUN npm install -g yarn
|
||||||
|
|
||||||
|
# Install build tools
|
||||||
|
RUN source /root/.bashrc && \
|
||||||
|
mix local.rebar --force && \
|
||||||
|
mix local.hex -if-missing --force
|
||||||
|
|
||||||
|
RUN mkdir /mobilizon
|
||||||
|
COPY ./ /mobilizon
|
||||||
|
WORKDIR /mobilizon
|
||||||
|
|
||||||
|
# # Build front-end
|
||||||
|
# RUN yarn --cwd "js" install --frozen-lockfile
|
||||||
|
# RUN yarn --cwd "js" run build
|
||||||
|
|
||||||
|
# Elixir release
|
||||||
|
RUN source /root/.bashrc && \
|
||||||
|
mix deps.get --only prod && \
|
||||||
|
mix compile && \
|
||||||
|
mix phx.digest.clean --all && \
|
||||||
|
mix release --path release/mobilizon && \
|
||||||
|
cd release/mobilizon && \
|
||||||
|
ln -s lib/mobilizon-*/priv priv && \
|
||||||
|
cd ../../
|
||||||
|
|
||||||
|
# Make a release archive
|
||||||
|
RUN tar -zcf /mobilizon/${APP_ASSET} -C release mobilizon
|
1
docker/multiarch/README.md
Normal file
1
docker/multiarch/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Contains the Dockerfile used to generate multi-arch Elixir releases
|
|
@ -4,11 +4,15 @@ FROM node:16-alpine as assets
|
||||||
RUN apk add --no-cache python3 build-base libwebp-tools bash imagemagick ncurses
|
RUN apk add --no-cache python3 build-base libwebp-tools bash imagemagick ncurses
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY js .
|
COPY js .
|
||||||
RUN yarn install \
|
|
||||||
|
ENV CYPRESS_INSTALL_BINARY 0
|
||||||
|
|
||||||
|
# Network timeout because it's slow when cross-compiling
|
||||||
|
RUN yarn install --network-timeout 100000 \
|
||||||
&& yarn run build
|
&& yarn run build
|
||||||
|
|
||||||
# Then, build the application binary
|
# Then, build the application binary
|
||||||
FROM elixir:1.12-alpine AS builder
|
FROM elixir:1.13-alpine AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache build-base git cmake
|
RUN apk add --no-cache build-base git cmake
|
||||||
|
|
||||||
|
@ -26,7 +30,7 @@ COPY rel ./rel
|
||||||
COPY support ./support
|
COPY support ./support
|
||||||
COPY --from=assets ./priv/static ./priv/static
|
COPY --from=assets ./priv/static ./priv/static
|
||||||
|
|
||||||
RUN mix phx.digest \
|
RUN mix phx.digest.clean --all \
|
||||||
&& mix release
|
&& mix release
|
||||||
|
|
||||||
# Finally setup the app
|
# Finally setup the app
|
||||||
|
@ -45,9 +49,14 @@ LABEL org.opencontainers.image.title="mobilizon" \
|
||||||
org.opencontainers.image.revision=$VCS_REF \
|
org.opencontainers.image.revision=$VCS_REF \
|
||||||
org.opencontainers.image.created=$BUILD_DATE
|
org.opencontainers.image.created=$BUILD_DATE
|
||||||
|
|
||||||
RUN apk add --no-cache openssl ca-certificates ncurses-libs file postgresql-client libgcc libstdc++ imagemagick
|
RUN apk add --no-cache curl openssl ca-certificates ncurses-libs file postgresql-client libgcc libstdc++ imagemagick python3 py3-pip py3-pillow py3-cffi py3-brotli gcc g++ musl-dev python3-dev pango libxslt-dev ttf-cantarell
|
||||||
|
RUN pip install weasyprint pyexcel-ods3
|
||||||
|
|
||||||
RUN mkdir -p /app/uploads && chown nobody:nobody /app/uploads
|
RUN mkdir -p /var/lib/mobilizon/uploads && chown nobody:nobody /var/lib/mobilizon/uploads
|
||||||
|
RUN mkdir -p /var/lib/mobilizon/uploads/exports/{csv,pdf,ods} && chown -R nobody:nobody /var/lib/mobilizon/uploads/exports
|
||||||
|
RUN mkdir -p /var/lib/mobilizon/timezones
|
||||||
|
RUN curl -L 'https://packages.joinmobilizon.org/tz_world/timezones-geodata.dets' -o /var/lib/mobilizon/timezones/timezones-geodata.dets
|
||||||
|
RUN chown nobody:nobody /var/lib/mobilizon/timezones
|
||||||
RUN mkdir -p /etc/mobilizon && chown nobody:nobody /etc/mobilizon
|
RUN mkdir -p /etc/mobilizon && chown nobody:nobody /etc/mobilizon
|
||||||
|
|
||||||
USER nobody
|
USER nobody
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
FROM elixir:latest
|
FROM elixir:latest
|
||||||
LABEL maintainer="Thomas Citharel <tcit@tcit.fr>"
|
LABEL maintainer="Thomas Citharel <tcit@tcit.fr>"
|
||||||
|
|
||||||
ENV REFRESHED_AT=2021-06-07
|
ENV REFRESHED_AT=2022-04-06
|
||||||
RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 cmake exiftool
|
RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 cmake exiftool python3-pip python3-setuptools
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install nodejs -yq
|
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install nodejs -yq
|
||||||
RUN npm install -g yarn wait-on
|
RUN npm install -g yarn wait-on
|
||||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
RUN mix local.hex --force && mix local.rebar --force
|
RUN mix local.hex --force && mix local.rebar --force
|
||||||
|
RUN pip3 install -Iv weasyprint pyexcel_ods3
|
||||||
RUN curl https://dbip.mirror.framasoft.org/files/dbip-city-lite-latest.mmdb --output GeoLite2-City.mmdb -s && mkdir -p /usr/share/GeoIP && mv GeoLite2-City.mmdb /usr/share/GeoIP/
|
RUN curl https://dbip.mirror.framasoft.org/files/dbip-city-lite-latest.mmdb --output GeoLite2-City.mmdb -s && mkdir -p /usr/share/GeoIP && mv GeoLite2-City.mmdb /usr/share/GeoIP/
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
# We build Elixir manually to have the oldest acceptable version of OTP
|
|
||||||
FROM erlang:21
|
|
||||||
LABEL maintainer="Thomas Citharel <tcit@tcit.fr>"
|
|
||||||
|
|
||||||
# elixir expects utf8.
|
|
||||||
ENV ELIXIR_VERSION="v1.11.4" \
|
|
||||||
LANG=C.UTF-8
|
|
||||||
|
|
||||||
RUN set -xe \
|
|
||||||
&& ELIXIR_DOWNLOAD_URL="https://github.com/elixir-lang/elixir/archive/${ELIXIR_VERSION}.tar.gz" \
|
|
||||||
&& ELIXIR_DOWNLOAD_SHA256="85c7118a0db6007507313db5bddf370216d9394ed7911fe80f21e2fbf7f54d29" \
|
|
||||||
&& curl -fSL -o elixir-src.tar.gz $ELIXIR_DOWNLOAD_URL \
|
|
||||||
&& echo "$ELIXIR_DOWNLOAD_SHA256 elixir-src.tar.gz" | sha256sum -c - \
|
|
||||||
&& mkdir -p /usr/local/src/elixir \
|
|
||||||
&& tar -xzC /usr/local/src/elixir --strip-components=1 -f elixir-src.tar.gz \
|
|
||||||
&& rm elixir-src.tar.gz \
|
|
||||||
&& cd /usr/local/src/elixir \
|
|
||||||
&& make install clean
|
|
||||||
|
|
||||||
CMD ["iex"]
|
|
||||||
|
|
||||||
ENV REFRESHED_AT=2021-06-07
|
|
||||||
RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 cmake exiftool
|
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash && apt-get install nodejs -yq
|
|
||||||
RUN npm install -g yarn wait-on
|
|
||||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
||||||
RUN mix local.hex --force && mix local.rebar --force
|
|
||||||
RUN curl https://dbip.mirror.framasoft.org/files/dbip-city-lite-latest.mmdb --output GeoLite2-City.mmdb -s && mkdir -p /usr/share/GeoIP && mv GeoLite2-City.mmdb /usr/share/GeoIP/
|
|
1
docker/tests/README.md
Normal file
1
docker/tests/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Contains the Dockerfile for the image used to run the tests
|
|
@ -9,8 +9,7 @@ module.exports = {
|
||||||
"plugin:vue/essential",
|
"plugin:vue/essential",
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"@vue/typescript/recommended",
|
"@vue/typescript/recommended",
|
||||||
"@vue/prettier",
|
"plugin:prettier/recommended",
|
||||||
"@vue/prettier/@typescript-eslint",
|
|
||||||
],
|
],
|
||||||
|
|
||||||
plugins: ["prettier"],
|
plugins: ["prettier"],
|
||||||
|
@ -30,7 +29,6 @@ module.exports = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"cypress/no-unnecessary-waiting": "off",
|
|
||||||
"vue/max-len": [
|
"vue/max-len": [
|
||||||
"off",
|
"off",
|
||||||
{
|
{
|
||||||
|
|
1
js/.gitignore
vendored
1
js/.gitignore
vendored
|
@ -23,3 +23,4 @@ yarn-error.log*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
.yarn
|
|
@ -1 +1,6 @@
|
||||||
{}
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"bracketSpacing": true
|
||||||
|
}
|
||||||
|
|
3
js/.yarnrc.yml
Normal file
3
js/.yarnrc.yml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
nodeLinker: node-modules
|
||||||
|
|
||||||
|
#yarnPath: .yarn/releases/yarn-3.1.1.cjs
|
|
@ -5,79 +5,41 @@
|
||||||
"kind": "INTERFACE",
|
"kind": "INTERFACE",
|
||||||
"name": "ActionLogObject",
|
"name": "ActionLogObject",
|
||||||
"possibleTypes": [
|
"possibleTypes": [
|
||||||
{
|
{ "name": "Comment" },
|
||||||
"name": "Comment"
|
{ "name": "Event" },
|
||||||
},
|
{ "name": "Group" },
|
||||||
{
|
{ "name": "Person" },
|
||||||
"name": "Event"
|
{ "name": "Report" },
|
||||||
},
|
{ "name": "ReportNote" },
|
||||||
{
|
{ "name": "User" }
|
||||||
"name": "Person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Report"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ReportNote"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "User"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "INTERFACE",
|
"kind": "INTERFACE",
|
||||||
"name": "ActivityObject",
|
"name": "ActivityObject",
|
||||||
"possibleTypes": [
|
"possibleTypes": [
|
||||||
{
|
{ "name": "Comment" },
|
||||||
"name": "Comment"
|
{ "name": "Discussion" },
|
||||||
},
|
{ "name": "Event" },
|
||||||
{
|
{ "name": "Group" },
|
||||||
"name": "Discussion"
|
{ "name": "Member" },
|
||||||
},
|
{ "name": "Post" },
|
||||||
{
|
{ "name": "Resource" }
|
||||||
"name": "Event"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Group"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Member"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Post"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Resource"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "INTERFACE",
|
"kind": "INTERFACE",
|
||||||
"name": "Actor",
|
"name": "Actor",
|
||||||
"possibleTypes": [
|
"possibleTypes": [
|
||||||
{
|
{ "name": "Application" },
|
||||||
"name": "Person"
|
{ "name": "Group" },
|
||||||
},
|
{ "name": "Person" }
|
||||||
{
|
|
||||||
"name": "Group"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Application"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "INTERFACE",
|
"kind": "INTERFACE",
|
||||||
"name": "Interactable",
|
"name": "Interactable",
|
||||||
"possibleTypes": [
|
"possibleTypes": [{ "name": "Event" }, { "name": "Group" }]
|
||||||
{
|
|
||||||
"name": "Event"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Group"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
103
js/package.json
103
js/package.json
|
@ -1,59 +1,79 @@
|
||||||
{
|
{
|
||||||
"name": "mobilizon",
|
"name": "mobilizon",
|
||||||
"version": "1.3.2",
|
"version": "2.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "yarn run build:assets && yarn run build:pictures",
|
"build": "yarn run build:assets && yarn run build:pictures",
|
||||||
"test:unit": "LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 vue-cli-service test:unit",
|
"test:unit": "LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 TZ=UTC vue-cli-service test:unit",
|
||||||
"test:e2e": "vue-cli-service test:e2e",
|
"test:e2e": "vue-cli-service test:e2e",
|
||||||
"lint": "vue-cli-service lint",
|
"lint": "vue-cli-service lint",
|
||||||
"build:assets": "vue-cli-service build",
|
"build:assets": "vue-cli-service build --report",
|
||||||
"build:pictures": "bash ./scripts/build/pictures.sh"
|
"build:pictures": "bash ./scripts/build/pictures.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@absinthe/socket": "^0.2.1",
|
"@absinthe/socket": "^0.2.1",
|
||||||
"@absinthe/socket-apollo-link": "^0.2.1",
|
"@absinthe/socket-apollo-link": "^0.2.1",
|
||||||
"@apollo/client": "^3.3.16",
|
"@apollo/client": "^3.3.16",
|
||||||
"@mdi/font": "^5.0.45",
|
"@mdi/font": "^6.1.95",
|
||||||
|
"@sentry/tracing": "^6.16.1",
|
||||||
|
"@sentry/vue": "^6.16.1",
|
||||||
|
"@tailwindcss/line-clamp": "^0.4.0",
|
||||||
"@tiptap/core": "^2.0.0-beta.41",
|
"@tiptap/core": "^2.0.0-beta.41",
|
||||||
"@tiptap/extension-blockquote": "^2.0.0-beta.6",
|
"@tiptap/extension-blockquote": "^2.0.0-beta.25",
|
||||||
|
"@tiptap/extension-bold": "^2.0.0-beta.24",
|
||||||
"@tiptap/extension-bubble-menu": "^2.0.0-beta.9",
|
"@tiptap/extension-bubble-menu": "^2.0.0-beta.9",
|
||||||
"@tiptap/extension-character-count": "^2.0.0-beta.5",
|
"@tiptap/extension-bullet-list": "^2.0.0-beta.23",
|
||||||
"@tiptap/extension-history": "^2.0.0-beta.5",
|
"@tiptap/extension-document": "^2.0.0-beta.15",
|
||||||
|
"@tiptap/extension-dropcursor": "^2.0.0-beta.25",
|
||||||
|
"@tiptap/extension-gapcursor": "^2.0.0-beta.33",
|
||||||
|
"@tiptap/extension-heading": "^2.0.0-beta.23",
|
||||||
|
"@tiptap/extension-history": "^2.0.0-beta.21",
|
||||||
"@tiptap/extension-image": "^2.0.0-beta.6",
|
"@tiptap/extension-image": "^2.0.0-beta.6",
|
||||||
|
"@tiptap/extension-italic": "^2.0.0-beta.24",
|
||||||
"@tiptap/extension-link": "^2.0.0-beta.8",
|
"@tiptap/extension-link": "^2.0.0-beta.8",
|
||||||
"@tiptap/extension-list-item": "^2.0.0-beta.6",
|
"@tiptap/extension-list-item": "^2.0.0-beta.19",
|
||||||
"@tiptap/extension-mention": "^2.0.0-beta.42",
|
"@tiptap/extension-mention": "^2.0.0-beta.42",
|
||||||
"@tiptap/extension-ordered-list": "^2.0.0-beta.6",
|
"@tiptap/extension-ordered-list": "^2.0.0-beta.24",
|
||||||
|
"@tiptap/extension-paragraph": "^2.0.0-beta.22",
|
||||||
|
"@tiptap/extension-strike": "^2.0.0-beta.26",
|
||||||
|
"@tiptap/extension-text": "^2.0.0-beta.15",
|
||||||
"@tiptap/extension-underline": "^2.0.0-beta.7",
|
"@tiptap/extension-underline": "^2.0.0-beta.7",
|
||||||
"@tiptap/starter-kit": "^2.0.0-beta.37",
|
|
||||||
"@tiptap/vue-2": "^2.0.0-beta.21",
|
"@tiptap/vue-2": "^2.0.0-beta.21",
|
||||||
|
"@vue-a11y/announcer": "^2.1.0",
|
||||||
|
"@vue-a11y/skip-to": "^2.1.2",
|
||||||
"@vue/apollo-option": "4.0.0-alpha.11",
|
"@vue/apollo-option": "4.0.0-alpha.11",
|
||||||
"apollo-absinthe-upload-link": "^1.5.0",
|
"apollo-absinthe-upload-link": "^1.5.0",
|
||||||
|
"autoprefixer": "^10",
|
||||||
"blurhash": "^1.1.3",
|
"blurhash": "^1.1.3",
|
||||||
"buefy": "^0.9.0",
|
"buefy": "^0.9.0",
|
||||||
"bulma-divider": "^0.2.0",
|
"bulma-divider": "^0.2.0",
|
||||||
"core-js": "^3.6.4",
|
"core-js": "^3.6.4",
|
||||||
"date-fns": "^2.16.0",
|
"date-fns": "^2.16.0",
|
||||||
"graphql": "^15.0.0",
|
"date-fns-tz": "^1.1.6",
|
||||||
|
"graphql": "^16.0.0",
|
||||||
"graphql-tag": "^2.10.3",
|
"graphql-tag": "^2.10.3",
|
||||||
"intersection-observer": "^0.12.0",
|
"intersection-observer": "^0.12.0",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"leaflet": "^1.4.0",
|
"leaflet": "^1.4.0",
|
||||||
"leaflet.locatecontrol": "^0.74.0",
|
"leaflet.locatecontrol": "^0.76.0",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.11",
|
||||||
"ngeohash": "^0.6.3",
|
"ngeohash": "^0.6.3",
|
||||||
"p-debounce": "^4.0.0",
|
"p-debounce": "^4.0.0",
|
||||||
"phoenix": "^1.4.11",
|
"phoenix": "^1.6",
|
||||||
|
"postcss": "^8",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
|
"sanitize-html": "^2.5.3",
|
||||||
|
"tailwindcss": "^3",
|
||||||
"tippy.js": "^6.2.3",
|
"tippy.js": "^6.2.3",
|
||||||
"unfetch": "^4.2.0",
|
"unfetch": "^4.2.0",
|
||||||
"v-tooltip": "^2.1.3",
|
"v-tooltip": "^2.1.3",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-class-component": "^7.2.3",
|
"vue-class-component": "^7.2.3",
|
||||||
"vue-i18n": "^8.14.0",
|
"vue-i18n": "^8.14.0",
|
||||||
|
"vue-matomo": "^4.1.0",
|
||||||
"vue-meta": "^2.3.1",
|
"vue-meta": "^2.3.1",
|
||||||
|
"vue-plausible": "^1.3.1",
|
||||||
"vue-property-decorator": "^9.0.0",
|
"vue-property-decorator": "^9.0.0",
|
||||||
"vue-router": "^3.1.6",
|
"vue-router": "^3.1.6",
|
||||||
"vue-scrollto": "^2.17.1",
|
"vue-scrollto": "^2.17.1",
|
||||||
|
@ -61,49 +81,50 @@
|
||||||
"vuedraggable": "^2.24.3"
|
"vuedraggable": "^2.24.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^26.0.18",
|
"@rushstack/eslint-patch": "^1.1.0",
|
||||||
|
"@types/jest": "^27.0.2",
|
||||||
"@types/leaflet": "^1.5.2",
|
"@types/leaflet": "^1.5.2",
|
||||||
"@types/leaflet.locatecontrol": "^0.60.7",
|
"@types/leaflet.locatecontrol": "^0.74",
|
||||||
"@types/lodash": "^4.14.141",
|
"@types/lodash": "^4.14.141",
|
||||||
"@types/ngeohash": "^0.6.2",
|
"@types/ngeohash": "^0.6.2",
|
||||||
|
"@types/phoenix": "^1.5.2",
|
||||||
"@types/prosemirror-inputrules": "^1.0.2",
|
"@types/prosemirror-inputrules": "^1.0.2",
|
||||||
"@types/prosemirror-model": "^1.7.2",
|
"@types/prosemirror-model": "^1.7.2",
|
||||||
"@types/prosemirror-state": "^1.2.4",
|
"@types/prosemirror-state": "^1.2.4",
|
||||||
"@types/prosemirror-view": "^1.11.4",
|
"@types/prosemirror-view": "^1.11.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
"@types/sanitize-html": "^2.5.0",
|
||||||
"@typescript-eslint/parser": "^4.18.0",
|
"@typescript-eslint/eslint-plugin": "^5.3.0",
|
||||||
"@vue/cli-plugin-babel": "~5.0.0-beta.3",
|
"@typescript-eslint/parser": "^5.3.0",
|
||||||
"@vue/cli-plugin-e2e-cypress": "~5.0.0-beta.3",
|
"@vue/cli-plugin-babel": "~5.0.4",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.0-beta.3",
|
"@vue/cli-plugin-eslint": "~5.0.4",
|
||||||
"@vue/cli-plugin-pwa": "~5.0.0-beta.3",
|
"@vue/cli-plugin-pwa": "~5.0.4",
|
||||||
"@vue/cli-plugin-router": "~5.0.0-beta.3",
|
"@vue/cli-plugin-router": "~5.0.4",
|
||||||
"@vue/cli-plugin-typescript": "~5.0.0-beta.3",
|
"@vue/cli-plugin-typescript": "~5.0.4",
|
||||||
"@vue/cli-plugin-unit-jest": "~5.0.0-beta.3",
|
"@vue/cli-plugin-unit-jest": "~5.0.4",
|
||||||
"@vue/cli-service": "~5.0.0-beta.3",
|
"@vue/cli-service": "~5.0.4",
|
||||||
"@vue/eslint-config-prettier": "^6.0.0",
|
"@vue/eslint-config-typescript": "^10.0.0",
|
||||||
"@vue/eslint-config-typescript": "^7.0.0",
|
|
||||||
"@vue/test-utils": "^1.1.0",
|
"@vue/test-utils": "^1.1.0",
|
||||||
"eslint": "^7.20.0",
|
"@vue/vue2-jest": "^27.0.0-alpha.3",
|
||||||
"eslint-plugin-cypress": "^2.10.3",
|
"@vue/vue3-jest": "^27.0.0-alpha.1",
|
||||||
|
"eslint": "^8.2.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-import": "^2.20.2",
|
"eslint-plugin-import": "^2.20.2",
|
||||||
"eslint-plugin-prettier": "^3.3.1",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-vue": "^7.6.0",
|
"eslint-plugin-vue": "^8.0.3",
|
||||||
"flush-promises": "^1.0.2",
|
"flush-promises": "^1.0.2",
|
||||||
"jest": "^26.6.3",
|
"jest": "^27.1.0",
|
||||||
"jest-junit": "^12.0.0",
|
"jest-junit": "^13.0.0",
|
||||||
"mock-apollo-client": "^1.1.0",
|
"mock-apollo-client": "^1.1.0",
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"prettier-eslint": "^13.0.0",
|
"prettier-eslint": "^14.0.0",
|
||||||
"sass": "^1.34.1",
|
"sass": "^1.34.1",
|
||||||
"sass-loader": "^12.0.0",
|
"sass-loader": "^12.0.0",
|
||||||
"ts-jest": "^26.5.3",
|
"ts-jest": "27",
|
||||||
"typescript": "~4.1.5",
|
"typescript": "~4.5.5",
|
||||||
"vue-i18n-extract": "^1.0.2",
|
"vue-cli-plugin-tailwind": "~3.0.0",
|
||||||
"vue-jest": "^4.0.1",
|
"vue-i18n-extract": "^2.0.4",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"vue-template-compiler": "^2.6.11",
|
||||||
"webpack-cli": "^4.7.0"
|
"webpack-cli": "^4.7.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"packageManager": "yarn@3.1.1"
|
||||||
"webpack": "5.44.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
6
js/postcss.config.js
Normal file
6
js/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" dir="auto">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="mobilizon">
|
<div id="mobilizon">
|
||||||
|
<VueAnnouncer />
|
||||||
|
<VueSkipTo to="#main" :label="$t('Skip to main content')" />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div v-if="config && config.demoMode">
|
<div v-if="config && config.demoMode">
|
||||||
<b-message
|
<b-message
|
||||||
|
@ -7,7 +9,7 @@
|
||||||
type="is-danger"
|
type="is-danger"
|
||||||
:title="$t('Warning').toLocaleUpperCase()"
|
:title="$t('Warning').toLocaleUpperCase()"
|
||||||
closable
|
closable
|
||||||
aria-close-label="Close"
|
:aria-close-label="$t('Close')"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
{{ $t("This is a demonstration site to test Mobilizon.") }}
|
{{ $t("This is a demonstration site to test Mobilizon.") }}
|
||||||
|
@ -22,9 +24,9 @@
|
||||||
</div>
|
</div>
|
||||||
<error v-if="error" :error="error" />
|
<error v-if="error" :error="error" />
|
||||||
|
|
||||||
<main v-else>
|
<main id="main" v-else>
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<router-view />
|
<router-view ref="routerView" />
|
||||||
</transition>
|
</transition>
|
||||||
</main>
|
</main>
|
||||||
<mobilizon-footer />
|
<mobilizon-footer />
|
||||||
|
@ -32,7 +34,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-property-decorator";
|
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
|
||||||
import NavBar from "./components/NavBar.vue";
|
import NavBar from "./components/NavBar.vue";
|
||||||
import {
|
import {
|
||||||
AUTH_ACCESS_TOKEN,
|
AUTH_ACCESS_TOKEN,
|
||||||
|
@ -52,6 +54,7 @@ import { IConfig } from "./types/config.model";
|
||||||
import { ICurrentUser } from "./types/current-user.model";
|
import { ICurrentUser } from "./types/current-user.model";
|
||||||
import jwt_decode, { JwtPayload } from "jwt-decode";
|
import jwt_decode, { JwtPayload } from "jwt-decode";
|
||||||
import { refreshAccessToken } from "./apollo/utils";
|
import { refreshAccessToken } from "./apollo/utils";
|
||||||
|
import { Route } from "vue-router";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
apollo: {
|
apollo: {
|
||||||
|
@ -82,6 +85,8 @@ export default class App extends Vue {
|
||||||
|
|
||||||
interval: number | undefined = undefined;
|
interval: number | undefined = undefined;
|
||||||
|
|
||||||
|
@Ref("routerView") routerView!: Vue;
|
||||||
|
|
||||||
async created(): Promise<void> {
|
async created(): Promise<void> {
|
||||||
if (await this.initializeCurrentUser()) {
|
if (await this.initializeCurrentUser()) {
|
||||||
await initializeCurrentActor(this.$apollo.provider.defaultClient);
|
await initializeCurrentActor(this.$apollo.provider.defaultClient);
|
||||||
|
@ -197,6 +202,55 @@ export default class App extends Vue {
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
this.interval = undefined;
|
this.interval = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Watch("config")
|
||||||
|
async initializeStatistics(config: IConfig) {
|
||||||
|
if (config) {
|
||||||
|
const { statistics } = (await import("./services/statistics")) as {
|
||||||
|
statistics: (config: IConfig, environment: Record<string, any>) => void;
|
||||||
|
};
|
||||||
|
statistics(config, { router: this.$router, version: config.version });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch("$route", { immediate: true })
|
||||||
|
updateAnnouncement(route: Route): void {
|
||||||
|
const pageTitle = this.extractPageTitleFromRoute(route);
|
||||||
|
if (pageTitle) {
|
||||||
|
this.$announcer.polite(
|
||||||
|
this.$t("Navigated to {pageTitle}", {
|
||||||
|
pageTitle,
|
||||||
|
}) as string
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Set the focus to the router view
|
||||||
|
// https://marcus.io/blog/accessible-routing-vuejs
|
||||||
|
setTimeout(() => {
|
||||||
|
const focusTarget = (
|
||||||
|
this.routerView?.$refs?.componentFocusTarget !== undefined
|
||||||
|
? this.routerView?.$refs?.componentFocusTarget
|
||||||
|
: this.routerView?.$el
|
||||||
|
) as HTMLElement;
|
||||||
|
if (focusTarget && focusTarget instanceof Element) {
|
||||||
|
// Make focustarget programmatically focussable
|
||||||
|
focusTarget.setAttribute("tabindex", "-1");
|
||||||
|
|
||||||
|
// Focus element
|
||||||
|
focusTarget.focus();
|
||||||
|
|
||||||
|
// Remove tabindex from focustarget.
|
||||||
|
// Reason: https://axesslab.com/skip-links/#update-3-a-comment-from-gov-uk
|
||||||
|
focusTarget.removeAttribute("tabindex");
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
extractPageTitleFromRoute(route: Route): string {
|
||||||
|
if (route.meta?.announcer?.message) {
|
||||||
|
return route.meta?.announcer?.message();
|
||||||
|
}
|
||||||
|
return document.title;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -206,7 +260,6 @@ export default class App extends Vue {
|
||||||
/* Icons */
|
/* Icons */
|
||||||
$mdi-font-path: "~@mdi/font/fonts";
|
$mdi-font-path: "~@mdi/font/fonts";
|
||||||
@import "~@mdi/font/scss/materialdesignicons";
|
@import "~@mdi/font/scss/materialdesignicons";
|
||||||
|
|
||||||
@import "common";
|
@import "common";
|
||||||
|
|
||||||
#mobilizon {
|
#mobilizon {
|
||||||
|
@ -218,4 +271,8 @@ $mdi-font-path: "~@mdi/font/fonts";
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vue-skip-to {
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -70,6 +70,9 @@ export const typePolicies: TypePolicies = {
|
||||||
participantStats: { merge: replaceMergePolicy },
|
participantStats: { merge: replaceMergePolicy },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Instance: {
|
||||||
|
keyFields: ["domain"],
|
||||||
|
},
|
||||||
RootQueryType: {
|
RootQueryType: {
|
||||||
fields: {
|
fields: {
|
||||||
relayFollowers: paginatedLimitPagination<IFollower>(),
|
relayFollowers: paginatedLimitPagination<IFollower>(),
|
||||||
|
|
9
js/src/assets/logo.svg
Normal file
9
js/src/assets/logo.svg
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60">
|
||||||
|
<path style="opacity:0;fill:#fea72b;fill-opacity:1;stroke:none;stroke-opacity:0" d="M-5.801-6.164h72.69v72.871h-72.69z" />
|
||||||
|
<g data-name="Calque 2">
|
||||||
|
<g data-name="header">
|
||||||
|
<path d="M26.58 27.06q0 8-4.26 12.3a12.21 12.21 0 0 1-9 3.42 12.21 12.21 0 0 1-9-3.42Q0 35.1 0 27.06q0-8.04 4.26-12.3a12.21 12.21 0 0 1 9-3.42 12.21 12.21 0 0 1 9 3.42q4.32 4.24 4.32 12.3zM13.29 17q-5.67 0-5.67 10.06t5.67 10.08q5.71 0 5.71-10.08T13.29 17z" style="fill:#3a384c;fill-opacity:1" transform="translate(14.627 5.256) scale(1.15671)" />
|
||||||
|
<path d="M9 6.78a7.37 7.37 0 0 1-.6-3 7.37 7.37 0 0 1 .6-3A8.09 8.09 0 0 1 12.83 0a7.05 7.05 0 0 1 3.69.84 7.37 7.37 0 0 1 .6 3 7.37 7.37 0 0 1-.6 3 7.46 7.46 0 0 1-3.87.84A6.49 6.49 0 0 1 9 6.78z" style="fill:#fff" transform="translate(14.627 5.256) scale(1.15671)" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 920 B |
5
js/src/assets/tailwind.css
Normal file
5
js/src/assets/tailwind.css
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@tailwind base;
|
||||||
|
|
||||||
|
@tailwind components;
|
||||||
|
|
||||||
|
@tailwind utilities;
|
|
@ -1,75 +1,24 @@
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
@import "variables.scss";
|
@import "variables.scss";
|
||||||
|
|
||||||
@import "~bulma";
|
@import "~bulma";
|
||||||
@import "~bulma-divider";
|
@import "~bulma-divider";
|
||||||
@import "~buefy/src/scss/buefy";
|
@import "~buefy/src/scss/buefy";
|
||||||
|
@import "styles/vue-announcer.scss";
|
||||||
// a {
|
@import "styles/vue-skip-to.scss";
|
||||||
// color: $violet-2;
|
|
||||||
// }
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin: 30px auto 45px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
background: $secondary;
|
|
||||||
display: inline;
|
|
||||||
padding: 3px 8px;
|
|
||||||
margin: 15px auto 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.out,
|
a.out,
|
||||||
.content a,
|
.content a,
|
||||||
.ProseMirror a {
|
.ProseMirror a {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-color: $secondary;
|
text-decoration-color: #ed8d07;
|
||||||
text-decoration-thickness: 2px;
|
text-decoration-thickness: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-content {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
|
|
||||||
> section > .columns {
|
|
||||||
min-height: 50vh;
|
|
||||||
}
|
|
||||||
> section {
|
|
||||||
&.container {
|
|
||||||
min-height: 80vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .container {
|
|
||||||
background: $whitest;
|
|
||||||
min-height: 70vh;
|
|
||||||
}
|
|
||||||
> #homepage {
|
|
||||||
background: $whitest;
|
|
||||||
#featured_events {
|
|
||||||
background: $whitest;
|
|
||||||
}
|
|
||||||
#picture {
|
|
||||||
.container,
|
|
||||||
.section {
|
|
||||||
background: $whitest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .container {
|
|
||||||
min-height: 25vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
padding: 1rem 1% 4rem;
|
padding: 1rem 1% 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
figure img.is-rounded {
|
|
||||||
border: 1px solid $chapril_blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$color-black: #000;
|
$color-black: #000;
|
||||||
|
|
||||||
.mention {
|
.mention {
|
||||||
|
@ -79,7 +28,7 @@ $color-black: #000;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-right: 0.2rem;
|
@include margin-right(0.2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mention-suggestion {
|
.mention-suggestion {
|
||||||
|
@ -88,7 +37,7 @@ $color-black: #000;
|
||||||
|
|
||||||
.mention .mention {
|
.mention .mention {
|
||||||
background: initial;
|
background: initial;
|
||||||
margin-right: 0;
|
@include margin-right(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select select {
|
.select select {
|
||||||
|
@ -112,16 +61,11 @@ body {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#mobilizon {
|
#mobilizon > .container > .message {
|
||||||
> main {
|
margin: 1rem auto auto;
|
||||||
background: $body-background-color;
|
.message-header {
|
||||||
}
|
button.delete {
|
||||||
> .container > .message {
|
background: #4a4a4a;
|
||||||
margin: 1rem auto auto;
|
|
||||||
.message-header {
|
|
||||||
button.delete {
|
|
||||||
background: $chapril_grey;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,7 +82,7 @@ $list-radius: $radius !default;
|
||||||
|
|
||||||
$list-item-border: 1px solid $border !default;
|
$list-item-border: 1px solid $border !default;
|
||||||
$list-item-color: $text !default;
|
$list-item-color: $text !default;
|
||||||
$list-item-active-background-color: $purple-1 !default;
|
$list-item-active-background-color: $link !default;
|
||||||
$list-item-active-color: $link-invert !default;
|
$list-item-active-color: $link-invert !default;
|
||||||
$list-item-hover-background-color: $background !default;
|
$list-item-hover-background-color: $background !default;
|
||||||
|
|
||||||
|
@ -173,70 +117,72 @@ a.list-item {
|
||||||
.setting-title {
|
.setting-title {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
background: $secondary;
|
|
||||||
color: $white;
|
|
||||||
span {
|
|
||||||
background: $secondary !important;
|
|
||||||
color: $white !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h2 {
|
h2 {
|
||||||
display: inline;
|
display: inline;
|
||||||
|
background: $secondary;
|
||||||
padding: 2px 7.5px;
|
padding: 2px 7.5px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-body {
|
@mixin focus() {
|
||||||
background-color: $chapril_blue_light;
|
&:focus {
|
||||||
}
|
border: 2px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
.columns {
|
|
||||||
background: $whitest;
|
|
||||||
}
|
|
||||||
.setting-menu-item {
|
|
||||||
background-color: $yellow-4;
|
|
||||||
|
|
||||||
.router-link-active,
|
|
||||||
.router-link-active {
|
|
||||||
background-color: $info;
|
|
||||||
color: $white;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $white;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-component-container {
|
ul.menu-list > li,
|
||||||
.datetime-container {
|
p {
|
||||||
margin-right: 1em;
|
@include focus;
|
||||||
}
|
}
|
||||||
|
.navbar-item {
|
||||||
|
@include focus;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time.datetime-container {
|
.navbar-dropdown span.navbar-item:hover {
|
||||||
color: $white;
|
background-color: whitesmoke;
|
||||||
background: $chapril_blue_light;
|
color: #0a0a0a;
|
||||||
span.month {
|
|
||||||
color: $whitest;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
footer
|
* Bulma/Buefy fixes
|
||||||
|
|
||||||
*/
|
*/
|
||||||
footer.footer[data-v-40ab164b] span.select select {
|
.icon {
|
||||||
background: $chapril_blue_light;
|
vertical-align: middle;
|
||||||
color: $footer-text-color;
|
}
|
||||||
|
|
||||||
|
.tags .tag:not(:last-child) {
|
||||||
|
margin-right: unset;
|
||||||
|
@include margin-right(0.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button .icon {
|
||||||
|
&:first-child:not(:last-child) {
|
||||||
|
@include margin-left(calc(-0.5em - 1px));
|
||||||
|
@include margin-right(0.25em);
|
||||||
|
}
|
||||||
|
&:last-child:not(:first-child) {
|
||||||
|
@include margin-right(calc(-0.5em - 1px));
|
||||||
|
@include margin-left(0.25em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons .button:not(:last-child):not(.is-fullwidth) {
|
||||||
|
margin-right: unset;
|
||||||
|
@include margin-right(0.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb li:first-child a {
|
||||||
|
padding-left: unset;
|
||||||
|
@include padding-left(0);
|
||||||
|
@include padding-right(0.75em);
|
||||||
|
}
|
||||||
|
.media-left {
|
||||||
|
@include margin-left(1rem);
|
||||||
|
}
|
||||||
|
a.dropdown-item {
|
||||||
|
@include padding-right(3rem);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<p>
|
<p>
|
||||||
<a :title="contact" v-if="configLink" :href="configLink.uri">{{
|
<a dir="auto" :title="contact" v-if="configLink" :href="configLink.uri">{{
|
||||||
configLink.text
|
configLink.text
|
||||||
}}</a>
|
}}</a>
|
||||||
<span v-else-if="contact">{{ contact }}</span>
|
<span dir="auto" v-else-if="contact">{{ contact }}</span>
|
||||||
<span v-else>{{ $t("contact uninformed") }}</span>
|
<span v-else>{{ $t("contact uninformed") }}</span>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
<template>
|
|
||||||
<b-autocomplete
|
|
||||||
:data="baseData"
|
|
||||||
:placeholder="$t('Actor')"
|
|
||||||
v-model="name"
|
|
||||||
field="preferredUsername"
|
|
||||||
:loading="$apollo.loading"
|
|
||||||
check-infinite-scroll
|
|
||||||
@typing="getAsyncData"
|
|
||||||
@select="handleSelect"
|
|
||||||
@infinite-scroll="getAsyncData"
|
|
||||||
>
|
|
||||||
<template #default="props">
|
|
||||||
<div class="media">
|
|
||||||
<div class="media-left">
|
|
||||||
<img
|
|
||||||
width="32"
|
|
||||||
:src="props.option.avatar.url"
|
|
||||||
v-if="props.option.avatar"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<b-icon v-else icon="account-circle" />
|
|
||||||
</div>
|
|
||||||
<div class="media-content">
|
|
||||||
<span v-if="props.option.name">
|
|
||||||
{{ props.option.name }}
|
|
||||||
<br />
|
|
||||||
<small>{{ `@${props.option.preferredUsername}` }}</small>
|
|
||||||
<small v-if="props.option.domain">{{
|
|
||||||
`@${props.option.domain}`
|
|
||||||
}}</small>
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
{{ `@${props.option.preferredUsername}` }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template slot="footer">
|
|
||||||
<span class="has-text-grey" v-show="page > totalPages">
|
|
||||||
Thats it! No more movies found.
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</b-autocomplete>
|
|
||||||
</template>
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Model, Vue, Watch } from "vue-property-decorator";
|
|
||||||
import debounce from "lodash/debounce";
|
|
||||||
import { IPerson } from "@/types/actor";
|
|
||||||
import { SEARCH_PERSONS } from "@/graphql/search";
|
|
||||||
import { Paginate } from "@/types/paginate";
|
|
||||||
|
|
||||||
const SEARCH_PERSON_LIMIT = 10;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class ActorAutoComplete extends Vue {
|
|
||||||
@Model("change", { type: Object }) readonly defaultSelected!: IPerson | null;
|
|
||||||
|
|
||||||
baseData: IPerson[] = [];
|
|
||||||
|
|
||||||
selected: IPerson | null = this.defaultSelected;
|
|
||||||
|
|
||||||
name: string = this.defaultSelected
|
|
||||||
? this.defaultSelected.preferredUsername
|
|
||||||
: "";
|
|
||||||
|
|
||||||
page = 1;
|
|
||||||
|
|
||||||
totalPages = 1;
|
|
||||||
|
|
||||||
mounted(): void {
|
|
||||||
this.selected = this.defaultSelected;
|
|
||||||
}
|
|
||||||
|
|
||||||
data(): Record<string, unknown> {
|
|
||||||
return {
|
|
||||||
getAsyncData: debounce(this.doGetAsyncData, 500),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("defaultSelected")
|
|
||||||
updateDefaultSelected(defaultSelected: IPerson): void {
|
|
||||||
console.log("update defaultSelected", defaultSelected);
|
|
||||||
this.selected = defaultSelected;
|
|
||||||
this.name = defaultSelected.preferredUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSelect(selected: IPerson): void {
|
|
||||||
this.selected = selected;
|
|
||||||
this.$emit("change", selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
async doGetAsyncData(name: string): Promise<void> {
|
|
||||||
this.baseData = [];
|
|
||||||
if (this.name !== name) {
|
|
||||||
this.name = name;
|
|
||||||
this.page = 1;
|
|
||||||
}
|
|
||||||
if (!name.length) {
|
|
||||||
this.page = 1;
|
|
||||||
this.totalPages = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
data: { searchPersons },
|
|
||||||
} = await this.$apollo.query<{ searchPersons: Paginate<IPerson> }>({
|
|
||||||
query: SEARCH_PERSONS,
|
|
||||||
variables: {
|
|
||||||
searchText: this.name,
|
|
||||||
page: this.page,
|
|
||||||
limit: SEARCH_PERSON_LIMIT,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.totalPages = Math.ceil(searchPersons.total / SEARCH_PERSON_LIMIT);
|
|
||||||
this.baseData.push(...searchPersons.elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,33 +1,86 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div
|
||||||
<div class="media" style="align-items: top">
|
class="bg-white rounded-lg flex space-x-4 items-center"
|
||||||
<div class="media-left">
|
:class="{ 'flex-col p-4 shadow-md sm:p-8 pb-10 w-80': !inline }"
|
||||||
<figure class="image is-32x32" v-if="actor.avatar">
|
>
|
||||||
<img class="is-rounded" :src="actor.avatar.url" alt="" />
|
<div>
|
||||||
</figure>
|
<figure class="w-12 h-12" v-if="actor.avatar">
|
||||||
<b-icon v-else size="is-medium" icon="account-circle" />
|
<img
|
||||||
</div>
|
class="rounded-lg"
|
||||||
|
:src="actor.avatar.url"
|
||||||
<div class="media-content">
|
alt=""
|
||||||
<p>
|
width="48"
|
||||||
{{ actor.name || `@${usernameWithDomain(actor)}` }}
|
height="48"
|
||||||
</p>
|
|
||||||
<p class="has-text-grey-dark" v-if="actor.name">
|
|
||||||
@{{ usernameWithDomain(actor) }}
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
v-if="full"
|
|
||||||
class="summary"
|
|
||||||
:class="{ limit: limit }"
|
|
||||||
v-html="actor.summary"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</figure>
|
||||||
|
<b-icon
|
||||||
|
v-else
|
||||||
|
:size="inline ? 'is-medium' : 'is-large'"
|
||||||
|
icon="account-circle"
|
||||||
|
class="ltr:-mr-0.5 rtl:-ml-0.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div :class="{ 'text-center': !inline }" class="overflow-hidden w-full">
|
||||||
|
<h5
|
||||||
|
class="text-xl font-medium violet-title tracking-tight text-gray-900 whitespace-pre-line line-clamp-2"
|
||||||
|
>
|
||||||
|
{{ displayName(actor) }}
|
||||||
|
</h5>
|
||||||
|
<p class="text-gray-500 truncate" v-if="actor.name">
|
||||||
|
<span dir="ltr">@{{ usernameWithDomain(actor) }}</span>
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="full"
|
||||||
|
class="only-first-child"
|
||||||
|
:class="{
|
||||||
|
'line-clamp-3': limit,
|
||||||
|
'line-clamp-10': !limit,
|
||||||
|
}"
|
||||||
|
v-html="actor.summary"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- <div
|
||||||
|
class="p-4 bg-white rounded-lg shadow-md sm:p-8 flex items-center space-x-4"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<figure class="w-12 h-12" v-if="actor.avatar">
|
||||||
|
<img
|
||||||
|
class="rounded-lg"
|
||||||
|
:src="actor.avatar.url"
|
||||||
|
alt=""
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
<b-icon
|
||||||
|
v-else
|
||||||
|
size="is-large"
|
||||||
|
icon="account-circle"
|
||||||
|
class="ltr:-mr-0.5 rtl:-ml-0.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h5 class="text-xl font-medium violet-title tracking-tight text-gray-900">
|
||||||
|
{{ displayName(actor) }}
|
||||||
|
</h5>
|
||||||
|
<p class="text-gray-500 truncate" v-if="actor.name">
|
||||||
|
<span dir="ltr">@{{ usernameWithDomain(actor) }}</span>
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="full"
|
||||||
|
class="line-clamp-3"
|
||||||
|
:class="{ limit: limit }"
|
||||||
|
v-html="actor.summary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||||
import { IActor, usernameWithDomain } from "../../types/actor";
|
import { displayName, IActor, usernameWithDomain } from "../../types/actor";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class ActorCard extends Vue {
|
export default class ActorCard extends Vue {
|
||||||
|
@ -35,131 +88,19 @@ export default class ActorCard extends Vue {
|
||||||
|
|
||||||
@Prop({ required: false, type: Boolean, default: false }) full!: boolean;
|
@Prop({ required: false, type: Boolean, default: false }) full!: boolean;
|
||||||
|
|
||||||
|
@Prop({ required: false, type: Boolean, default: false }) inline!: boolean;
|
||||||
|
|
||||||
@Prop({ required: false, type: Boolean, default: false }) popover!: boolean;
|
@Prop({ required: false, type: Boolean, default: false }) popover!: boolean;
|
||||||
|
|
||||||
@Prop({ required: false, type: Boolean, default: true }) limit!: boolean;
|
@Prop({ required: false, type: Boolean, default: true }) limit!: boolean;
|
||||||
|
|
||||||
usernameWithDomain = usernameWithDomain;
|
usernameWithDomain = usernameWithDomain;
|
||||||
|
|
||||||
|
displayName = displayName;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style scoped>
|
||||||
.summary.limit {
|
.only-first-child ::v-deep :not(:first-child) {
|
||||||
max-width: 25rem;
|
display: none;
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.tooltip {
|
|
||||||
display: block !important;
|
|
||||||
z-index: 10000;
|
|
||||||
|
|
||||||
.tooltip-inner {
|
|
||||||
background: black;
|
|
||||||
color: white;
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 5px 10px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip-arrow {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-style: solid;
|
|
||||||
position: absolute;
|
|
||||||
margin: 5px;
|
|
||||||
border-color: black;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^="top"] {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
|
|
||||||
.tooltip-arrow {
|
|
||||||
border-width: 5px 5px 0 5px;
|
|
||||||
border-left-color: transparent !important;
|
|
||||||
border-right-color: transparent !important;
|
|
||||||
border-bottom-color: transparent !important;
|
|
||||||
bottom: -5px;
|
|
||||||
left: calc(50% - 5px);
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^="bottom"] {
|
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
.tooltip-arrow {
|
|
||||||
border-width: 0 5px 5px 5px;
|
|
||||||
border-left-color: transparent !important;
|
|
||||||
border-right-color: transparent !important;
|
|
||||||
border-top-color: transparent !important;
|
|
||||||
top: -5px;
|
|
||||||
left: calc(50% - 5px);
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^="right"] {
|
|
||||||
margin-left: 5px;
|
|
||||||
|
|
||||||
.tooltip-arrow {
|
|
||||||
border-width: 5px 5px 5px 0;
|
|
||||||
border-left-color: transparent !important;
|
|
||||||
border-top-color: transparent !important;
|
|
||||||
border-bottom-color: transparent !important;
|
|
||||||
left: -5px;
|
|
||||||
top: calc(50% - 5px);
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[x-placement^="left"] {
|
|
||||||
margin-right: 5px;
|
|
||||||
|
|
||||||
.tooltip-arrow {
|
|
||||||
border-width: 5px 0 5px 5px;
|
|
||||||
border-top-color: transparent !important;
|
|
||||||
border-right-color: transparent !important;
|
|
||||||
border-bottom-color: transparent !important;
|
|
||||||
right: -5px;
|
|
||||||
top: calc(50% - 5px);
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.popover {
|
|
||||||
$color: #f9f9f9;
|
|
||||||
|
|
||||||
.popover-inner {
|
|
||||||
background: lighten($background-color, 65%);
|
|
||||||
color: black;
|
|
||||||
padding: 24px;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow: 0 5px 30px rgba(black, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover-arrow {
|
|
||||||
border-color: $color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-hidden="true"] {
|
|
||||||
visibility: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.15s, visibility 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-hidden="false"] {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,31 +1,37 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="actor-inline">
|
<div class="inline-flex items-start">
|
||||||
<div class="actor-avatar">
|
<div class="flex-none mr-2">
|
||||||
<figure class="image is-24x24" v-if="actor.avatar">
|
<figure class="image is-48x48" v-if="actor.avatar">
|
||||||
<img class="is-rounded" :src="actor.avatar.url" alt="" />
|
<img class="is-rounded" :src="actor.avatar.url" alt="" />
|
||||||
</figure>
|
</figure>
|
||||||
<b-icon v-else size="is-medium" icon="account-circle" />
|
<b-icon v-else size="is-large" icon="account-circle" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actor-name">
|
<div class="flex-auto">
|
||||||
<p>
|
<p class="text-base line-clamp-3 md:line-clamp-2 max-w-xl">
|
||||||
{{ actor.name || `@${usernameWithDomain(actor)}` }}
|
{{ displayName(actor) }}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500 truncate">
|
||||||
|
@{{ usernameWithDomain(actor) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||||
import { IActor, usernameWithDomain } from "../../types/actor";
|
import { displayName, IActor, usernameWithDomain } from "../../types/actor";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class ActorInline extends Vue {
|
export default class ActorInline extends Vue {
|
||||||
@Prop({ required: true, type: Object }) actor!: IActor;
|
@Prop({ required: true, type: Object }) actor!: IActor;
|
||||||
|
|
||||||
usernameWithDomain = usernameWithDomain;
|
usernameWithDomain = usernameWithDomain;
|
||||||
|
|
||||||
|
displayName = displayName;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
div.actor-inline {
|
div.actor-inline {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
@ -36,7 +42,7 @@ div.actor-inline {
|
||||||
flex-basis: auto;
|
flex-basis: auto;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-right: 0.5rem;
|
@include margin-right(0.5rem);
|
||||||
}
|
}
|
||||||
div.actor-name {
|
div.actor-name {
|
||||||
flex-basis: auto;
|
flex-basis: auto;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
:class="{ inline, clickable: actor && actor.type === ActorType.GROUP }"
|
:class="{ inline, clickable: actor && actor.type === ActorType.GROUP }"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<template slot="popover" class="popover">
|
<template slot="popover">
|
||||||
<actor-card :full="true" :actor="actor" :popover="true" />
|
<actor-card :full="true" :actor="actor" :popover="true" />
|
||||||
</template>
|
</template>
|
||||||
</v-popover>
|
</v-popover>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
<hr />
|
<hr role="presentation" />
|
||||||
<p class="content">
|
<p class="content">
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<hr />
|
<hr role="presentation" />
|
||||||
<p class="content">
|
<p class="content">
|
||||||
{{
|
{{
|
||||||
$t(
|
$t(
|
||||||
|
|
|
@ -148,6 +148,11 @@ export default class GroupActivityItem extends mixins(ActivityMixin) {
|
||||||
case Openness.INVITE_ONLY:
|
case Openness.INVITE_ONLY:
|
||||||
details.push("The group can now only be joined with an invite.");
|
details.push("The group can now only be joined with an invite.");
|
||||||
break;
|
break;
|
||||||
|
case Openness.MODERATED:
|
||||||
|
details.push(
|
||||||
|
"The group can now be joined by anyone, but new members need to be approved by an administrator."
|
||||||
|
);
|
||||||
|
break;
|
||||||
case Openness.OPEN:
|
case Openness.OPEN:
|
||||||
details.push("The group can now be joined by anyone.");
|
details.push("The group can now be joined by anyone.");
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -9,13 +9,7 @@
|
||||||
:inline="true"
|
:inline="true"
|
||||||
slot="member"
|
slot="member"
|
||||||
>
|
>
|
||||||
<b>
|
<b> {{ displayName(activity.object.actor) }}</b></popover-actor-card
|
||||||
{{
|
|
||||||
$t("@{username}", {
|
|
||||||
username: usernameWithDomain(activity.object.actor),
|
|
||||||
})
|
|
||||||
}}</b
|
|
||||||
></popover-actor-card
|
|
||||||
>
|
>
|
||||||
<b slot="member" v-else>{{
|
<b slot="member" v-else>{{
|
||||||
subjectParams.member_actor_federated_username
|
subjectParams.member_actor_federated_username
|
||||||
|
@ -25,13 +19,7 @@
|
||||||
:inline="true"
|
:inline="true"
|
||||||
slot="profile"
|
slot="profile"
|
||||||
>
|
>
|
||||||
<b>
|
<b> {{ displayName(activity.author) }}</b></popover-actor-card
|
||||||
{{
|
|
||||||
$t("@{username}", {
|
|
||||||
username: usernameWithDomain(activity.author),
|
|
||||||
})
|
|
||||||
}}</b
|
|
||||||
></popover-actor-card
|
|
||||||
></i18n
|
></i18n
|
||||||
>
|
>
|
||||||
<small class="has-text-grey-dark activity-date">{{
|
<small class="has-text-grey-dark activity-date">{{
|
||||||
|
@ -41,7 +29,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { usernameWithDomain } from "@/types/actor";
|
import { displayName } from "@/types/actor";
|
||||||
import { ActivityMemberSubject, MemberRole } from "@/types/enums";
|
import { ActivityMemberSubject, MemberRole } from "@/types/enums";
|
||||||
import { Component } from "vue-property-decorator";
|
import { Component } from "vue-property-decorator";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
@ -62,7 +50,7 @@ export const MEMBER_ROLE_VALUE: Record<string, number> = {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class MemberActivityItem extends mixins(ActivityMixin) {
|
export default class MemberActivityItem extends mixins(ActivityMixin) {
|
||||||
usernameWithDomain = usernameWithDomain;
|
displayName = displayName;
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
ActivityMemberSubject = ActivityMemberSubject;
|
ActivityMemberSubject = ActivityMemberSubject;
|
||||||
|
|
||||||
|
@ -83,6 +71,14 @@ export default class MemberActivityItem extends mixins(ActivityMixin) {
|
||||||
return "You added the member {member}.";
|
return "You added the member {member}.";
|
||||||
}
|
}
|
||||||
return "{profile} added the member {member}.";
|
return "{profile} added the member {member}.";
|
||||||
|
case ActivityMemberSubject.MEMBER_APPROVED:
|
||||||
|
if (this.isAuthorCurrentActor) {
|
||||||
|
return "You approved {member}'s membership.";
|
||||||
|
}
|
||||||
|
if (this.isObjectMemberCurrentActor) {
|
||||||
|
return "Your membership was approved by {profile}.";
|
||||||
|
}
|
||||||
|
return "{profile} approved {member}'s membership.";
|
||||||
case ActivityMemberSubject.MEMBER_JOINED:
|
case ActivityMemberSubject.MEMBER_JOINED:
|
||||||
return "{member} joined the group.";
|
return "{member} joined the group.";
|
||||||
case ActivityMemberSubject.MEMBER_UPDATED:
|
case ActivityMemberSubject.MEMBER_UPDATED:
|
||||||
|
@ -94,6 +90,12 @@ export default class MemberActivityItem extends mixins(ActivityMixin) {
|
||||||
}
|
}
|
||||||
return "{profile} updated the member {member}.";
|
return "{profile} updated the member {member}.";
|
||||||
case ActivityMemberSubject.MEMBER_REMOVED:
|
case ActivityMemberSubject.MEMBER_REMOVED:
|
||||||
|
if (this.subjectParams.member_role === MemberRole.NOT_APPROVED) {
|
||||||
|
if (this.isAuthorCurrentActor) {
|
||||||
|
return "You rejected {member}'s membership request.";
|
||||||
|
}
|
||||||
|
return "{profile} rejected {member}'s membership request.";
|
||||||
|
}
|
||||||
if (this.isAuthorCurrentActor) {
|
if (this.isAuthorCurrentActor) {
|
||||||
return "You excluded member {member}.";
|
return "You excluded member {member}.";
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,7 +172,8 @@ export default class ResourceActivityItem extends mixins(ActivityMixin) {
|
||||||
if (this.subjectParams.resource_path) {
|
if (this.subjectParams.resource_path) {
|
||||||
const parentPath = this.parentPath(this.subjectParams.resource_path);
|
const parentPath = this.parentPath(this.subjectParams.resource_path);
|
||||||
const directory = parentPath.split("/");
|
const directory = parentPath.split("/");
|
||||||
return directory.pop();
|
const res = directory.pop();
|
||||||
|
res === "" ? null : res;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
125
js/src/components/Address/AddressInfo.vue
Normal file
125
js/src/components/Address/AddressInfo.vue
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
<template>
|
||||||
|
<address dir="auto">
|
||||||
|
<b-icon
|
||||||
|
v-if="showIcon"
|
||||||
|
:icon="address.poiInfos.poiIcon.icon"
|
||||||
|
size="is-medium"
|
||||||
|
class="icon"
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
<span
|
||||||
|
class="addressDescription"
|
||||||
|
:title="address.poiInfos.name"
|
||||||
|
v-if="address.poiInfos.name"
|
||||||
|
>
|
||||||
|
{{ address.poiInfos.name }}
|
||||||
|
</span>
|
||||||
|
<br v-if="address.poiInfos.name" />
|
||||||
|
<span class="has-text-grey-dark">
|
||||||
|
{{ address.poiInfos.alternativeName }}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<small
|
||||||
|
v-if="
|
||||||
|
userTimezoneDifferent &&
|
||||||
|
longShortTimezoneNamesDifferent &&
|
||||||
|
timezoneLongNameValid
|
||||||
|
"
|
||||||
|
class="has-text-grey-dark"
|
||||||
|
>
|
||||||
|
🌐
|
||||||
|
{{
|
||||||
|
$t("{timezoneLongName} ({timezoneShortName})", {
|
||||||
|
timezoneLongName,
|
||||||
|
timezoneShortName,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</small>
|
||||||
|
<small v-else-if="userTimezoneDifferent" class="has-text-grey-dark">
|
||||||
|
🌐 {{ timezoneShortName }}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
</address>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { IAddress } from "@/types/address.model";
|
||||||
|
import { PropType } from "vue";
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class AddressInfo extends Vue {
|
||||||
|
@Prop({ required: true, type: Object as PropType<IAddress> })
|
||||||
|
address!: IAddress;
|
||||||
|
|
||||||
|
@Prop({ required: false, default: false, type: Boolean }) showIcon!: boolean;
|
||||||
|
@Prop({ required: false, default: false, type: Boolean })
|
||||||
|
showTimezone!: boolean;
|
||||||
|
@Prop({ required: false, type: String }) userTimezone!: string;
|
||||||
|
|
||||||
|
get userTimezoneDifferent(): boolean {
|
||||||
|
return (
|
||||||
|
this.userTimezone != undefined &&
|
||||||
|
this.address.timezone != undefined &&
|
||||||
|
this.userTimezone !== this.address.timezone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get longShortTimezoneNamesDifferent(): boolean {
|
||||||
|
return (
|
||||||
|
this.timezoneLongName != undefined &&
|
||||||
|
this.timezoneShortName != undefined &&
|
||||||
|
this.timezoneLongName !== this.timezoneShortName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get timezoneLongName(): string | undefined {
|
||||||
|
return this.timezoneName("long");
|
||||||
|
}
|
||||||
|
|
||||||
|
get timezoneShortName(): string | undefined {
|
||||||
|
return this.timezoneName("short");
|
||||||
|
}
|
||||||
|
|
||||||
|
get timezoneLongNameValid(): boolean {
|
||||||
|
return (
|
||||||
|
this.timezoneLongName != undefined && !this.timezoneLongName.match(/UTC/)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private timezoneName(format: "long" | "short"): string | undefined {
|
||||||
|
return this.extractTimezone(
|
||||||
|
new Intl.DateTimeFormat(undefined, {
|
||||||
|
timeZoneName: format,
|
||||||
|
timeZone: this.address.timezone,
|
||||||
|
}).formatToParts()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractTimezone(
|
||||||
|
parts: Intl.DateTimeFormatPart[]
|
||||||
|
): string | undefined {
|
||||||
|
return parts.find((part) => part.type === "timeZoneName")?.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
|
address {
|
||||||
|
font-style: normal;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
span.addressDescription {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: 4rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.icon {
|
||||||
|
@include padding-right(1rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
36
js/src/components/Address/InlineAddress.vue
Normal file
36
js/src/components/Address/InlineAddress.vue
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="truncate"
|
||||||
|
:title="
|
||||||
|
isDescriptionDifferentFromLocality
|
||||||
|
? `${physicalAddress.description}, ${physicalAddress.locality}`
|
||||||
|
: physicalAddress.description
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<b-icon icon="map-marker" />
|
||||||
|
<span v-if="physicalAddress.locality">
|
||||||
|
{{ physicalAddress.locality }}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{ physicalAddress.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { IAddress } from "@/types/address.model";
|
||||||
|
import { PropType } from "vue";
|
||||||
|
import { Prop, Vue, Component } from "vue-property-decorator";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class InlineAddress extends Vue {
|
||||||
|
@Prop({ required: true, type: Object as PropType<IAddress> })
|
||||||
|
physicalAddress!: IAddress;
|
||||||
|
|
||||||
|
get isDescriptionDifferentFromLocality(): boolean {
|
||||||
|
return (
|
||||||
|
this.physicalAddress?.description !== this.physicalAddress?.locality &&
|
||||||
|
this.physicalAddress?.description !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,260 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<b-table
|
|
||||||
v-show="relayFollowers.elements.length > 0"
|
|
||||||
:data="relayFollowers.elements"
|
|
||||||
:loading="$apollo.queries.relayFollowers.loading"
|
|
||||||
ref="table"
|
|
||||||
:checked-rows.sync="checkedRows"
|
|
||||||
detailed
|
|
||||||
:show-detail-icon="false"
|
|
||||||
paginated
|
|
||||||
backend-pagination
|
|
||||||
:current-page.sync="page"
|
|
||||||
:aria-next-label="$t('Next page')"
|
|
||||||
:aria-previous-label="$t('Previous page')"
|
|
||||||
:aria-page-label="$t('Page')"
|
|
||||||
:aria-current-label="$t('Current page')"
|
|
||||||
:total="relayFollowers.total"
|
|
||||||
:per-page="FOLLOWERS_PER_PAGE"
|
|
||||||
@page-change="onFollowersPageChange"
|
|
||||||
checkable
|
|
||||||
checkbox-position="left"
|
|
||||||
>
|
|
||||||
<b-table-column
|
|
||||||
field="actor.id"
|
|
||||||
label="ID"
|
|
||||||
width="40"
|
|
||||||
numeric
|
|
||||||
v-slot="props"
|
|
||||||
>{{ props.row.actor.id }}</b-table-column
|
|
||||||
>
|
|
||||||
|
|
||||||
<b-table-column
|
|
||||||
field="actor.type"
|
|
||||||
:label="$t('Type')"
|
|
||||||
width="80"
|
|
||||||
v-slot="props"
|
|
||||||
>
|
|
||||||
<b-icon icon="lan" v-if="RelayMixin.isInstance(props.row.actor)" />
|
|
||||||
<b-icon icon="account-circle" v-else />
|
|
||||||
</b-table-column>
|
|
||||||
|
|
||||||
<b-table-column
|
|
||||||
field="approved"
|
|
||||||
:label="$t('Status')"
|
|
||||||
width="100"
|
|
||||||
sortable
|
|
||||||
centered
|
|
||||||
v-slot="props"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
:class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
|
|
||||||
>{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
|
|
||||||
>
|
|
||||||
</b-table-column>
|
|
||||||
|
|
||||||
<b-table-column field="actor.domain" :label="$t('Domain')" sortable>
|
|
||||||
<template v-slot:default="props">
|
|
||||||
<a
|
|
||||||
@click="toggle(props.row)"
|
|
||||||
v-if="RelayMixin.isInstance(props.row.actor)"
|
|
||||||
>{{ props.row.actor.domain }}</a
|
|
||||||
>
|
|
||||||
<a @click="toggle(props.row)" v-else>{{
|
|
||||||
`${props.row.actor.preferredUsername}@${props.row.actor.domain}`
|
|
||||||
}}</a>
|
|
||||||
</template>
|
|
||||||
</b-table-column>
|
|
||||||
|
|
||||||
<b-table-column
|
|
||||||
field="targetActor.updatedAt"
|
|
||||||
:label="$t('Date')"
|
|
||||||
sortable
|
|
||||||
v-slot="props"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
:title="$options.filters.formatDateTimeString(props.row.updatedAt)"
|
|
||||||
>{{
|
|
||||||
formatDistanceToNow(new Date(props.row.updatedAt), {
|
|
||||||
locale: $dateFnsLocale,
|
|
||||||
})
|
|
||||||
}}</span
|
|
||||||
></b-table-column
|
|
||||||
>
|
|
||||||
|
|
||||||
<template #detail="props">
|
|
||||||
<article>
|
|
||||||
<div class="content">
|
|
||||||
<strong>{{ props.row.actor.name }}</strong>
|
|
||||||
<small v-if="props.row.actor.preferredUsername !== 'relay'"
|
|
||||||
>@{{ props.row.actor.preferredUsername }}</small
|
|
||||||
>
|
|
||||||
<p v-html="props.row.actor.summary" />
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template slot="bottom-left" v-if="checkedRows.length > 0">
|
|
||||||
<div class="buttons">
|
|
||||||
<b-button
|
|
||||||
@click="acceptRelays"
|
|
||||||
type="is-success"
|
|
||||||
v-if="checkedRowsHaveAtLeastOneToApprove"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
$tc(
|
|
||||||
"No instance to approve|Approve instance|Approve {number} instances",
|
|
||||||
checkedRows.length,
|
|
||||||
{ number: checkedRows.length }
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</b-button>
|
|
||||||
<b-button @click="rejectRelays" type="is-danger">
|
|
||||||
{{
|
|
||||||
$tc(
|
|
||||||
"No instance to reject|Reject instance|Reject {number} instances",
|
|
||||||
checkedRows.length,
|
|
||||||
{ number: checkedRows.length }
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</b-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</b-table>
|
|
||||||
<b-message type="is-danger" v-if="relayFollowers.elements.length === 0">{{
|
|
||||||
$t("No instance follows your instance yet.")
|
|
||||||
}}</b-message>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Mixins, Ref } from "vue-property-decorator";
|
|
||||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
|
||||||
import { formatDistanceToNow } from "date-fns";
|
|
||||||
import {
|
|
||||||
ACCEPT_RELAY,
|
|
||||||
REJECT_RELAY,
|
|
||||||
RELAY_FOLLOWERS,
|
|
||||||
} from "../../graphql/admin";
|
|
||||||
import { IFollower } from "../../types/actor/follower.model";
|
|
||||||
import RelayMixin from "../../mixins/relay";
|
|
||||||
import RouteName from "@/router/name";
|
|
||||||
import { Paginate } from "@/types/paginate";
|
|
||||||
|
|
||||||
const FOLLOWERS_PER_PAGE = 10;
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
apollo: {
|
|
||||||
relayFollowers: {
|
|
||||||
query: RELAY_FOLLOWERS,
|
|
||||||
variables() {
|
|
||||||
return {
|
|
||||||
page: this.page,
|
|
||||||
limit: FOLLOWERS_PER_PAGE,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
metaInfo() {
|
|
||||||
return {
|
|
||||||
title: this.$t("Followers") as string,
|
|
||||||
titleTemplate: "%s | Mobilizon",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class Followers extends Mixins(RelayMixin) {
|
|
||||||
RelayMixin = RelayMixin;
|
|
||||||
|
|
||||||
formatDistanceToNow = formatDistanceToNow;
|
|
||||||
|
|
||||||
relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
|
|
||||||
|
|
||||||
checkedRows: IFollower[] = [];
|
|
||||||
|
|
||||||
FOLLOWERS_PER_PAGE = FOLLOWERS_PER_PAGE;
|
|
||||||
|
|
||||||
@Ref("table") readonly table!: any;
|
|
||||||
|
|
||||||
toggle(row: Record<string, unknown>): void {
|
|
||||||
this.table.toggleDetails(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
get page(): number {
|
|
||||||
return parseInt((this.$route.query.page as string) || "1", 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
set page(page: number) {
|
|
||||||
this.pushRouter(RouteName.RELAY_FOLLOWERS, {
|
|
||||||
page: page.toString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptRelays(): void {
|
|
||||||
this.checkedRows.forEach((row: IFollower) => {
|
|
||||||
this.acceptRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
rejectRelays(): void {
|
|
||||||
this.checkedRows.forEach((row: IFollower) => {
|
|
||||||
this.rejectRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async acceptRelay(address: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
await this.$apollo.mutate({
|
|
||||||
mutation: ACCEPT_RELAY,
|
|
||||||
variables: {
|
|
||||||
address,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.$apollo.queries.relayFollowers.refetch();
|
|
||||||
this.checkedRows = [];
|
|
||||||
} catch (e) {
|
|
||||||
Snackbar.open({
|
|
||||||
message: e.message,
|
|
||||||
type: "is-danger",
|
|
||||||
position: "is-bottom",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async rejectRelay(address: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
await this.$apollo.mutate({
|
|
||||||
mutation: REJECT_RELAY,
|
|
||||||
variables: {
|
|
||||||
address,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.$apollo.queries.relayFollowers.refetch();
|
|
||||||
this.checkedRows = [];
|
|
||||||
} catch (e) {
|
|
||||||
Snackbar.open({
|
|
||||||
message: e.message,
|
|
||||||
type: "is-danger",
|
|
||||||
position: "is-bottom",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get checkedRowsHaveAtLeastOneToApprove(): boolean {
|
|
||||||
return this.checkedRows.some((checkedRow) => !checkedRow.approved);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onFollowersPageChange(page: number): Promise<void> {
|
|
||||||
this.page = page;
|
|
||||||
try {
|
|
||||||
await this.$apollo.queries.relayFollowers.fetchMore({
|
|
||||||
variables: {
|
|
||||||
page: this.page,
|
|
||||||
limit: FOLLOWERS_PER_PAGE,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,307 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<form @submit="followRelay">
|
|
||||||
<b-field
|
|
||||||
:label="$t('Add an instance')"
|
|
||||||
custom-class="add-relay"
|
|
||||||
horizontal
|
|
||||||
>
|
|
||||||
<b-field grouped expanded size="is-large">
|
|
||||||
<p class="control">
|
|
||||||
<b-input
|
|
||||||
v-model="newRelayAddress"
|
|
||||||
:placeholder="$t('Ex: mobilizon.fr')"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<p class="control">
|
|
||||||
<b-button type="is-primary" native-type="submit">{{
|
|
||||||
$t("Add an instance")
|
|
||||||
}}</b-button>
|
|
||||||
</p>
|
|
||||||
</b-field>
|
|
||||||
</b-field>
|
|
||||||
</form>
|
|
||||||
<b-table
|
|
||||||
v-show="relayFollowings.elements.length > 0"
|
|
||||||
:data="relayFollowings.elements"
|
|
||||||
:loading="$apollo.queries.relayFollowings.loading"
|
|
||||||
ref="table"
|
|
||||||
:checked-rows.sync="checkedRows"
|
|
||||||
:is-row-checkable="(row) => row.id !== 3"
|
|
||||||
detailed
|
|
||||||
:show-detail-icon="false"
|
|
||||||
paginated
|
|
||||||
backend-pagination
|
|
||||||
:current-page.sync="page"
|
|
||||||
:aria-next-label="$t('Next page')"
|
|
||||||
:aria-previous-label="$t('Previous page')"
|
|
||||||
:aria-page-label="$t('Page')"
|
|
||||||
:aria-current-label="$t('Current page')"
|
|
||||||
:total="relayFollowings.total"
|
|
||||||
:per-page="FOLLOWINGS_PER_PAGE"
|
|
||||||
@page-change="onFollowingsPageChange"
|
|
||||||
checkable
|
|
||||||
checkbox-position="left"
|
|
||||||
>
|
|
||||||
<b-table-column
|
|
||||||
field="targetActor.id"
|
|
||||||
label="ID"
|
|
||||||
width="40"
|
|
||||||
numeric
|
|
||||||
v-slot="props"
|
|
||||||
>{{ props.row.targetActor.id }}</b-table-column
|
|
||||||
>
|
|
||||||
|
|
||||||
<b-table-column
|
|
||||||
field="targetActor.type"
|
|
||||||
:label="$t('Type')"
|
|
||||||
width="80"
|
|
||||||
v-slot="props"
|
|
||||||
>
|
|
||||||
<b-icon
|
|
||||||
icon="lan"
|
|
||||||
v-if="RelayMixin.isInstance(props.row.targetActor)"
|
|
||||||
/>
|
|
||||||
<b-icon icon="account-circle" v-else />
|
|
||||||
</b-table-column>
|
|
||||||
|
|
||||||
<b-table-column
|
|
||||||
field="approved"
|
|
||||||
:label="$t('Status')"
|
|
||||||
width="100"
|
|
||||||
sortable
|
|
||||||
centered
|
|
||||||
v-slot="props"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
:class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
|
|
||||||
>{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
|
|
||||||
>
|
|
||||||
</b-table-column>
|
|
||||||
|
|
||||||
<b-table-column field="targetActor.domain" :label="$t('Domain')" sortable>
|
|
||||||
<template v-slot:default="props">
|
|
||||||
<a
|
|
||||||
@click="toggle(props.row)"
|
|
||||||
v-if="RelayMixin.isInstance(props.row.targetActor)"
|
|
||||||
>{{ props.row.targetActor.domain }}</a
|
|
||||||
>
|
|
||||||
<a @click="toggle(props.row)" v-else>{{
|
|
||||||
`${props.row.targetActor.preferredUsername}@${props.row.targetActor.domain}`
|
|
||||||
}}</a>
|
|
||||||
</template>
|
|
||||||
</b-table-column>
|
|
||||||
|
|
||||||
<b-table-column
|
|
||||||
field="targetActor.updatedAt"
|
|
||||||
:label="$t('Date')"
|
|
||||||
sortable
|
|
||||||
v-slot="props"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
:title="$options.filters.formatDateTimeString(props.row.updatedAt)"
|
|
||||||
>{{
|
|
||||||
formatDistanceToNow(new Date(props.row.updatedAt), {
|
|
||||||
locale: $dateFnsLocale,
|
|
||||||
})
|
|
||||||
}}</span
|
|
||||||
></b-table-column
|
|
||||||
>
|
|
||||||
|
|
||||||
<template #detail="props">
|
|
||||||
<article>
|
|
||||||
<div class="content">
|
|
||||||
<strong>{{ props.row.targetActor.name }}</strong>
|
|
||||||
<small v-if="props.row.actor.preferredUsername !== 'relay'"
|
|
||||||
>@{{ props.row.targetActor.preferredUsername }}</small
|
|
||||||
>
|
|
||||||
<p v-html="props.row.targetActor.summary" />
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template slot="bottom-left" v-if="checkedRows.length > 0">
|
|
||||||
<b-button @click="removeRelays" type="is-danger">
|
|
||||||
{{
|
|
||||||
$tc(
|
|
||||||
"No instance to remove|Remove instance|Remove {number} instances",
|
|
||||||
checkedRows.length,
|
|
||||||
{ number: checkedRows.length }
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</b-button>
|
|
||||||
</template>
|
|
||||||
</b-table>
|
|
||||||
<b-message type="is-danger" v-if="relayFollowings.total === 0">{{
|
|
||||||
$t("You don't follow any instances yet.")
|
|
||||||
}}</b-message>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Mixins } from "vue-property-decorator";
|
|
||||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
|
||||||
import { formatDistanceToNow } from "date-fns";
|
|
||||||
import { ADD_RELAY, REMOVE_RELAY } from "../../graphql/admin";
|
|
||||||
import { IFollower } from "../../types/actor/follower.model";
|
|
||||||
import RelayMixin from "../../mixins/relay";
|
|
||||||
import { RELAY_FOLLOWINGS } from "@/graphql/admin";
|
|
||||||
import { Paginate } from "@/types/paginate";
|
|
||||||
import RouteName from "@/router/name";
|
|
||||||
import { ApolloCache, FetchResult, Reference } from "@apollo/client/core";
|
|
||||||
import gql from "graphql-tag";
|
|
||||||
|
|
||||||
const FOLLOWINGS_PER_PAGE = 10;
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
apollo: {
|
|
||||||
relayFollowings: {
|
|
||||||
query: RELAY_FOLLOWINGS,
|
|
||||||
variables() {
|
|
||||||
return {
|
|
||||||
page: this.page,
|
|
||||||
limit: FOLLOWINGS_PER_PAGE,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
metaInfo() {
|
|
||||||
return {
|
|
||||||
title: this.$t("Followings") as string,
|
|
||||||
titleTemplate: "%s | Mobilizon",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class Followings extends Mixins(RelayMixin) {
|
|
||||||
newRelayAddress = "";
|
|
||||||
|
|
||||||
RelayMixin = RelayMixin;
|
|
||||||
|
|
||||||
formatDistanceToNow = formatDistanceToNow;
|
|
||||||
|
|
||||||
relayFollowings: Paginate<IFollower> = { elements: [], total: 0 };
|
|
||||||
|
|
||||||
FOLLOWINGS_PER_PAGE = FOLLOWINGS_PER_PAGE;
|
|
||||||
|
|
||||||
checkedRows: IFollower[] = [];
|
|
||||||
|
|
||||||
get page(): number {
|
|
||||||
return parseInt((this.$route.query.page as string) || "1", 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
set page(page: number) {
|
|
||||||
this.pushRouter(RouteName.RELAY_FOLLOWINGS, {
|
|
||||||
page: page.toString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async onFollowingsPageChange(page: number): Promise<void> {
|
|
||||||
this.page = page;
|
|
||||||
try {
|
|
||||||
await this.$apollo.queries.relayFollowings.fetchMore({
|
|
||||||
variables: {
|
|
||||||
page: this.page,
|
|
||||||
limit: FOLLOWINGS_PER_PAGE,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async followRelay(e: Event): Promise<void> {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
await this.$apollo.mutate<{ relayFollowings: Paginate<IFollower> }>({
|
|
||||||
mutation: ADD_RELAY,
|
|
||||||
variables: {
|
|
||||||
address: this.newRelayAddress.trim(), // trim to fix copy and paste domain name spaces and tabs
|
|
||||||
},
|
|
||||||
update(
|
|
||||||
cache: ApolloCache<{ relayFollowings: Paginate<IFollower> }>,
|
|
||||||
{ data }: FetchResult
|
|
||||||
) {
|
|
||||||
cache.modify({
|
|
||||||
fields: {
|
|
||||||
relayFollowings(
|
|
||||||
existingFollowings = { elements: [], total: 0 },
|
|
||||||
{ readField }
|
|
||||||
) {
|
|
||||||
const newFollowingRef = cache.writeFragment({
|
|
||||||
id: `${data?.addRelay.__typename}:${data?.addRelay.id}`,
|
|
||||||
data: data?.addRelay,
|
|
||||||
fragment: gql`
|
|
||||||
fragment NewFollowing on Follower {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
if (
|
|
||||||
existingFollowings.elements.some(
|
|
||||||
(ref: Reference) =>
|
|
||||||
readField("id", ref) === data?.addRelay.id
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return existingFollowings;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
total: existingFollowings.total + 1,
|
|
||||||
elements: [newFollowingRef, ...existingFollowings.elements],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
broadcast: false,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.newRelayAddress = "";
|
|
||||||
} catch (err) {
|
|
||||||
Snackbar.open({
|
|
||||||
message: err.message,
|
|
||||||
type: "is-danger",
|
|
||||||
position: "is-bottom",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeRelays(): void {
|
|
||||||
this.checkedRows.forEach((row: IFollower) => {
|
|
||||||
this.removeRelay(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeRelay(follower: IFollower): Promise<void> {
|
|
||||||
const address = `${follower.targetActor.preferredUsername}@${follower.targetActor.domain}`;
|
|
||||||
try {
|
|
||||||
await this.$apollo.mutate<{ removeRelay: IFollower }>({
|
|
||||||
mutation: REMOVE_RELAY,
|
|
||||||
variables: {
|
|
||||||
address,
|
|
||||||
},
|
|
||||||
update(cache: ApolloCache<{ removeRelay: IFollower }>) {
|
|
||||||
cache.modify({
|
|
||||||
fields: {
|
|
||||||
relayFollowings(existingFollowingRefs, { readField }) {
|
|
||||||
return {
|
|
||||||
total: existingFollowingRefs.total - 1,
|
|
||||||
elements: existingFollowingRefs.elements.filter(
|
|
||||||
(followingRef: Reference) =>
|
|
||||||
follower.id !== readField("id", followingRef)
|
|
||||||
),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await this.$apollo.queries.relayFollowings.refetch();
|
|
||||||
this.checkedRows = [];
|
|
||||||
} catch (e) {
|
|
||||||
Snackbar.open({
|
|
||||||
message: e.message,
|
|
||||||
type: "is-danger",
|
|
||||||
position: "is-bottom",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -7,7 +7,7 @@
|
||||||
}"
|
}"
|
||||||
class="comment-element"
|
class="comment-element"
|
||||||
>
|
>
|
||||||
<article class="media" :id="commentId">
|
<article class="media" :id="commentId" dir="auto">
|
||||||
<popover-actor-card
|
<popover-actor-card
|
||||||
:actor="comment.actor"
|
:actor="comment.actor"
|
||||||
:inline="true"
|
:inline="true"
|
||||||
|
@ -32,11 +32,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<span class="first-line" v-if="!comment.deletedAt">
|
<span class="first-line" v-if="!comment.deletedAt" dir="auto">
|
||||||
<strong :class="{ organizer: commentFromOrganizer }">{{
|
<strong :class="{ organizer: commentFromOrganizer }">{{
|
||||||
comment.actor.name
|
comment.actor.name
|
||||||
}}</strong>
|
}}</strong>
|
||||||
<small>{{ usernameWithDomain(comment.actor) }}</small>
|
<small dir="ltr">@{{ usernameWithDomain(comment.actor) }}</small>
|
||||||
</span>
|
</span>
|
||||||
<a v-else class="comment-link" :href="commentURL">
|
<a v-else class="comment-link" :href="commentURL">
|
||||||
<span>{{ $t("[deleted]") }}</span>
|
<span>{{ $t("[deleted]") }}</span>
|
||||||
|
@ -63,7 +63,12 @@
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<div v-if="!comment.deletedAt" v-html="comment.text" />
|
<div
|
||||||
|
v-if="!comment.deletedAt"
|
||||||
|
v-html="comment.text"
|
||||||
|
dir="auto"
|
||||||
|
:lang="comment.language"
|
||||||
|
/>
|
||||||
<div v-else>{{ $t("[This comment has been deleted]") }}</div>
|
<div v-else>{{ $t("[This comment has been deleted]") }}</div>
|
||||||
<div class="load-replies" v-if="comment.totalReplies">
|
<div class="load-replies" v-if="comment.totalReplies">
|
||||||
<p v-if="!showReplies" @click="fetchReplies">
|
<p v-if="!showReplies" @click="fetchReplies">
|
||||||
|
@ -128,7 +133,7 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<span class="first-line">
|
<span class="first-line">
|
||||||
<strong>{{ currentActor.name }}</strong>
|
<strong>{{ currentActor.name }}</strong>
|
||||||
<small>@{{ currentActor.preferredUsername }}</small>
|
<small dir="ltr">@{{ currentActor.preferredUsername }}</small>
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span class="editor-line">
|
<span class="editor-line">
|
||||||
|
@ -137,6 +142,7 @@
|
||||||
ref="commentEditor"
|
ref="commentEditor"
|
||||||
v-model="newComment.text"
|
v-model="newComment.text"
|
||||||
mode="comment"
|
mode="comment"
|
||||||
|
:aria-label="$t('Comment body')"
|
||||||
/>
|
/>
|
||||||
<b-button
|
<b-button
|
||||||
:disabled="newComment.text.trim().length === 0"
|
:disabled="newComment.text.trim().length === 0"
|
||||||
|
@ -298,6 +304,10 @@ export default class Comment extends Vue {
|
||||||
onConfirm: this.reportComment,
|
onConfirm: this.reportComment,
|
||||||
outsideDomain: this.comment.actor.domain,
|
outsideDomain: this.comment.actor.domain,
|
||||||
},
|
},
|
||||||
|
// https://github.com/buefy/buefy/pull/3589
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
closeButtonAriaLabel: this.$t("Close"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,17 +332,20 @@ export default class Comment extends Vue {
|
||||||
position: "is-bottom-right",
|
position: "is-bottom-right",
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
Snackbar.open({
|
if (e.message) {
|
||||||
message: e.message,
|
Snackbar.open({
|
||||||
type: "is-danger",
|
message: e.message,
|
||||||
position: "is-bottom",
|
type: "is-danger",
|
||||||
});
|
position: "is-bottom",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
form.reply {
|
form.reply {
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -352,7 +365,7 @@ form.reply {
|
||||||
}
|
}
|
||||||
|
|
||||||
& > small {
|
& > small {
|
||||||
margin-left: 0.3rem;
|
@include margin-left(0.3rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -362,14 +375,14 @@ form.reply {
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-right: 10px;
|
@include padding-right(10px);
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.comment-link {
|
a.comment-link {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin-left: 5px;
|
@include margin-left(5px);
|
||||||
color: $text;
|
color: $text;
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
@ -398,6 +411,7 @@ a.comment-link {
|
||||||
color: $white;
|
color: $white;
|
||||||
.reply-btn,
|
.reply-btn,
|
||||||
small,
|
small,
|
||||||
|
span,
|
||||||
strong,
|
strong,
|
||||||
.icons button {
|
.icons button {
|
||||||
color: $white;
|
color: $white;
|
||||||
|
@ -412,7 +426,7 @@ a.comment-link {
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-left {
|
.media-left {
|
||||||
margin-right: 0.5rem;
|
@include margin-right(5px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -423,7 +437,7 @@ a.comment-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-right: 10px;
|
@include margin-right(10px);
|
||||||
|
|
||||||
.vertical-border {
|
.vertical-border {
|
||||||
width: 3px;
|
width: 3px;
|
||||||
|
@ -441,9 +455,12 @@ a.comment-link {
|
||||||
|
|
||||||
.media .media-content {
|
.media .media-content {
|
||||||
overflow-x: initial;
|
overflow-x: initial;
|
||||||
.content .editor-line {
|
.content {
|
||||||
display: flex;
|
text-align: start;
|
||||||
align-items: center;
|
.editor-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icons {
|
.icons {
|
||||||
|
@ -512,7 +529,7 @@ article {
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-action .icon {
|
.reply-action .icon {
|
||||||
padding-right: 0.4rem;
|
@include padding-right(0.4rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.visually-hidden {
|
.visually-hidden {
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
>{{ $t("Comments are closed for everybody else.") }}</b-notification
|
>{{ $t("Comments are closed for everybody else.") }}</b-notification
|
||||||
>
|
>
|
||||||
<article class="media">
|
<article class="media">
|
||||||
<figure class="media-left">
|
<figure class="media-left" v-if="newComment.actor">
|
||||||
<identity-picker-wrapper :inline="false" v-model="newComment.actor" />
|
<identity-picker-wrapper :inline="false" v-model="newComment.actor" />
|
||||||
</figure>
|
</figure>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
|
@ -23,6 +23,7 @@
|
||||||
ref="commenteditor"
|
ref="commenteditor"
|
||||||
mode="comment"
|
mode="comment"
|
||||||
v-model="newComment.text"
|
v-model="newComment.text"
|
||||||
|
:aria-label="$t('Comment body')"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p class="help is-danger" v-if="emptyCommentError">
|
<p class="help is-danger" v-if="emptyCommentError">
|
||||||
|
@ -30,9 +31,11 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field notify-participants" v-if="isEventOrganiser">
|
<div class="field notify-participants" v-if="isEventOrganiser">
|
||||||
<b-switch v-model="newComment.isAnnouncement">{{
|
<b-switch
|
||||||
$t("Notify participants")
|
aria-labelledby="notify-participants-toggle"
|
||||||
}}</b-switch>
|
v-model="newComment.isAnnouncement"
|
||||||
|
>{{ $t("Notify participants") }}</b-switch
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,8 +45,8 @@
|
||||||
type="is-primary"
|
type="is-primary"
|
||||||
class="comment-button-submit"
|
class="comment-button-submit"
|
||||||
icon-left="send"
|
icon-left="send"
|
||||||
:aria-label="$t('Post a comment')"
|
>{{ $t("Send") }}</b-button
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</form>
|
</form>
|
||||||
|
@ -56,11 +59,11 @@
|
||||||
>
|
>
|
||||||
{{ $t("Loading comments…") }}
|
{{ $t("Loading comments…") }}
|
||||||
</p>
|
</p>
|
||||||
<transition-group name="comment-empty-list" mode="out-in" v-else>
|
<transition-group tag="div" name="comment-empty-list" v-else>
|
||||||
<transition-group
|
<transition-group
|
||||||
key="list"
|
key="list"
|
||||||
name="comment-list"
|
name="comment-list"
|
||||||
v-if="comments.length"
|
v-if="filteredOrderedComments.length"
|
||||||
class="comment-list"
|
class="comment-list"
|
||||||
tag="ul"
|
tag="ul"
|
||||||
>
|
>
|
||||||
|
@ -74,9 +77,9 @@
|
||||||
@delete-comment="deleteComment"
|
@delete-comment="deleteComment"
|
||||||
/>
|
/>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
<div v-else class="no-comments" key="no-comments">
|
<empty-content v-else icon="comment" key="no-comments" :inline="true">
|
||||||
<span>{{ $t("No comments yet") }}</span>
|
<span>{{ $t("No comments yet") }}</span>
|
||||||
</div>
|
</empty-content>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -96,6 +99,7 @@ import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
||||||
import { IPerson } from "../../types/actor";
|
import { IPerson } from "../../types/actor";
|
||||||
import { IEvent } from "../../types/event.model";
|
import { IEvent } from "../../types/event.model";
|
||||||
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
|
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
|
||||||
|
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
apollo: {
|
apollo: {
|
||||||
|
@ -116,6 +120,7 @@ import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
|
||||||
components: {
|
components: {
|
||||||
Comment,
|
Comment,
|
||||||
IdentityPickerWrapper,
|
IdentityPickerWrapper,
|
||||||
|
EmptyContent,
|
||||||
editor: () =>
|
editor: () =>
|
||||||
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
||||||
},
|
},
|
||||||
|
@ -213,7 +218,7 @@ export default class CommentTree extends Vue {
|
||||||
|
|
||||||
// and reset the new comment field
|
// and reset the new comment field
|
||||||
this.newComment = new CommentModel();
|
this.newComment = new CommentModel();
|
||||||
} catch (errors) {
|
} catch (errors: any) {
|
||||||
console.error(errors);
|
console.error(errors);
|
||||||
if (errors.graphQLErrors && errors.graphQLErrors.length > 0) {
|
if (errors.graphQLErrors && errors.graphQLErrors.length > 0) {
|
||||||
const error = errors.graphQLErrors[0];
|
const error = errors.graphQLErrors[0];
|
||||||
|
@ -295,7 +300,7 @@ export default class CommentTree extends Vue {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// this.comments = this.comments.filter(commentItem => commentItem.id !== comment.id);
|
// this.comments = this.comments.filter(commentItem => commentItem.id !== comment.id);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||||
this.$notifier.error(error.graphQLErrors[0].message);
|
this.$notifier.error(error.graphQLErrors[0].message);
|
||||||
|
@ -360,21 +365,35 @@ export default class CommentTree extends Vue {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
|
@import "~bulma/sass/utilities/mixins.sass";
|
||||||
form.new-comment {
|
form.new-comment {
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
|
|
||||||
.media-content {
|
.media {
|
||||||
display: flex;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
align-content: center;
|
.media-left {
|
||||||
|
@include mobile {
|
||||||
|
@include margin-right(0.5rem);
|
||||||
|
@include margin-left(0.5rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.field {
|
.media-content {
|
||||||
flex: 1;
|
display: flex;
|
||||||
padding-right: 10px;
|
align-items: center;
|
||||||
margin-bottom: 0;
|
align-content: center;
|
||||||
|
width: min-content;
|
||||||
|
|
||||||
&.notify-participants {
|
.field {
|
||||||
margin-top: 0.5rem;
|
flex: 1;
|
||||||
|
@include padding-right(10px);
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
&.notify-participants {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<b-icon v-else size="is-large" icon="account-circle" />
|
<b-icon v-else size="is-large" icon="account-circle" />
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="meta">
|
<div class="meta" dir="auto">
|
||||||
<span
|
<span
|
||||||
class="first-line name"
|
class="first-line name"
|
||||||
v-if="comment.actor && !comment.deletedAt"
|
v-if="comment.actor && !comment.deletedAt"
|
||||||
|
@ -64,7 +64,11 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!editMode && !comment.deletedAt" class="text-wrapper">
|
<div
|
||||||
|
v-if="!editMode && !comment.deletedAt"
|
||||||
|
class="text-wrapper"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
<div class="description-content" v-html="comment.text"></div>
|
<div class="description-content" v-html="comment.text"></div>
|
||||||
<p
|
<p
|
||||||
v-if="
|
v-if="
|
||||||
|
@ -88,7 +92,7 @@
|
||||||
{{ $t("[This comment has been deleted by it's author]") }}
|
{{ $t("[This comment has been deleted by it's author]") }}
|
||||||
</div>
|
</div>
|
||||||
<form v-else class="edition" @submit.prevent="updateComment">
|
<form v-else class="edition" @submit.prevent="updateComment">
|
||||||
<editor v-model="updatedComment" />
|
<editor v-model="updatedComment" :aria-label="$t('Comment body')" />
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<b-button
|
<b-button
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
|
@ -141,13 +145,16 @@ export default class DiscussionComment extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateComment(): void {
|
updateComment(): void {
|
||||||
this.comment.text = this.updatedComment;
|
this.$emit("update-comment", {
|
||||||
this.$emit("update-comment", this.comment);
|
...this.comment,
|
||||||
|
text: this.updatedComment,
|
||||||
|
});
|
||||||
this.toggleEditMode();
|
this.toggleEditMode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
article.comment {
|
article.comment {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-top: 1px solid #e9e9e9;
|
border-top: 1px solid #e9e9e9;
|
||||||
|
@ -163,7 +170,7 @@ article.comment {
|
||||||
padding: 0 1rem 0.3em;
|
padding: 0 1rem 0.3em;
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
margin-right: auto;
|
@include margin-right(auto);
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
@ -216,7 +223,7 @@ article.comment {
|
||||||
::v-deep blockquote {
|
::v-deep blockquote {
|
||||||
border-left: 0.2em solid #333;
|
border-left: 0.2em solid #333;
|
||||||
display: block;
|
display: block;
|
||||||
padding-left: 1em;
|
@include padding-left(1em);
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep p {
|
::v-deep p {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<router-link
|
<router-link
|
||||||
class="discussion-minimalist-card-wrapper"
|
class="discussion-minimalist-card-wrapper"
|
||||||
|
dir="auto"
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.DISCUSSION,
|
name: RouteName.DISCUSSION,
|
||||||
params: { slug: discussion.slug, id: discussion.id },
|
params: { slug: discussion.slug, id: discussion.id },
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="ellipsis has-text-grey-dark"
|
class="ellipsis has-text-grey-dark"
|
||||||
|
dir="auto"
|
||||||
v-if="!discussion.lastComment.deletedAt"
|
v-if="!discussion.lastComment.deletedAt"
|
||||||
>
|
>
|
||||||
{{ htmlTextEllipsis }}
|
{{ htmlTextEllipsis }}
|
||||||
|
@ -83,6 +85,7 @@ export default class DiscussionListItem extends Vue {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
.discussion-minimalist-card-wrapper {
|
.discussion-minimalist-card-wrapper {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -92,7 +95,7 @@ export default class DiscussionListItem extends Vue {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.calendar-icon {
|
.calendar-icon {
|
||||||
margin-right: 1rem;
|
@include margin-right(1rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-info-wrapper {
|
.title-info-wrapper {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
:class="{ 'is-active': editor.isActive('bold') }"
|
:class="{ 'is-active': editor.isActive('bold') }"
|
||||||
@click="editor.chain().focus().toggleBold().run()"
|
@click="editor.chain().focus().toggleBold().run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Bold')"
|
||||||
>
|
>
|
||||||
<b-icon icon="format-bold" />
|
<b-icon icon="format-bold" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
:class="{ 'is-active': editor.isActive('italic') }"
|
:class="{ 'is-active': editor.isActive('italic') }"
|
||||||
@click="editor.chain().focus().toggleItalic().run()"
|
@click="editor.chain().focus().toggleItalic().run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Italic')"
|
||||||
>
|
>
|
||||||
<b-icon icon="format-italic" />
|
<b-icon icon="format-italic" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -34,6 +36,7 @@
|
||||||
:class="{ 'is-active': editor.isActive('underline') }"
|
:class="{ 'is-active': editor.isActive('underline') }"
|
||||||
@click="editor.chain().focus().toggleUnderline().run()"
|
@click="editor.chain().focus().toggleUnderline().run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Underline')"
|
||||||
>
|
>
|
||||||
<b-icon icon="format-underline" />
|
<b-icon icon="format-underline" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -44,6 +47,7 @@
|
||||||
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
|
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
|
||||||
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
|
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Heading Level 1')"
|
||||||
>
|
>
|
||||||
<b-icon icon="format-header-1" />
|
<b-icon icon="format-header-1" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -54,6 +58,7 @@
|
||||||
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
|
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
|
||||||
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Heading Level 2')"
|
||||||
>
|
>
|
||||||
<b-icon icon="format-header-2" />
|
<b-icon icon="format-header-2" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -64,6 +69,7 @@
|
||||||
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
|
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
|
||||||
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
|
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Heading Level 3')"
|
||||||
>
|
>
|
||||||
<b-icon icon="format-header-3" />
|
<b-icon icon="format-header-3" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -73,6 +79,7 @@
|
||||||
@click="showLinkMenu()"
|
@click="showLinkMenu()"
|
||||||
:class="{ 'is-active': editor.isActive('link') }"
|
:class="{ 'is-active': editor.isActive('link') }"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Add link')"
|
||||||
>
|
>
|
||||||
<b-icon icon="link" />
|
<b-icon icon="link" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -82,6 +89,7 @@
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
@click="editor.chain().focus().unsetLink().run()"
|
@click="editor.chain().focus().unsetLink().run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Remove link')"
|
||||||
>
|
>
|
||||||
<b-icon icon="link-off" />
|
<b-icon icon="link-off" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -91,6 +99,7 @@
|
||||||
v-if="!isBasicMode"
|
v-if="!isBasicMode"
|
||||||
@click="showImagePrompt()"
|
@click="showImagePrompt()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Add picture')"
|
||||||
>
|
>
|
||||||
<b-icon icon="image" />
|
<b-icon icon="image" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -101,6 +110,7 @@
|
||||||
:class="{ 'is-active': editor.isActive('bulletList') }"
|
:class="{ 'is-active': editor.isActive('bulletList') }"
|
||||||
@click="editor.chain().focus().toggleBulletList().run()"
|
@click="editor.chain().focus().toggleBulletList().run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Bullet list')"
|
||||||
>
|
>
|
||||||
<b-icon icon="format-list-bulleted" />
|
<b-icon icon="format-list-bulleted" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -111,6 +121,7 @@
|
||||||
:class="{ 'is-active': editor.isActive('orderedList') }"
|
:class="{ 'is-active': editor.isActive('orderedList') }"
|
||||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Ordered list')"
|
||||||
>
|
>
|
||||||
<b-icon icon="format-list-numbered" />
|
<b-icon icon="format-list-numbered" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -121,6 +132,7 @@
|
||||||
:class="{ 'is-active': editor.isActive('blockquote') }"
|
:class="{ 'is-active': editor.isActive('blockquote') }"
|
||||||
@click="editor.chain().focus().toggleBlockquote().run()"
|
@click="editor.chain().focus().toggleBlockquote().run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Quote')"
|
||||||
>
|
>
|
||||||
<b-icon icon="format-quote-close" />
|
<b-icon icon="format-quote-close" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -130,6 +142,7 @@
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
@click="editor.chain().focus().undo().run()"
|
@click="editor.chain().focus().undo().run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Undo')"
|
||||||
>
|
>
|
||||||
<b-icon icon="undo" />
|
<b-icon icon="undo" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -139,6 +152,7 @@
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
@click="editor.chain().focus().redo().run()"
|
@click="editor.chain().focus().redo().run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Redo')"
|
||||||
>
|
>
|
||||||
<b-icon icon="redo" />
|
<b-icon icon="redo" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -155,6 +169,7 @@
|
||||||
:class="{ 'is-active': editor.isActive('bold') }"
|
:class="{ 'is-active': editor.isActive('bold') }"
|
||||||
@click="editor.chain().focus().toggleBold().run()"
|
@click="editor.chain().focus().toggleBold().run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Bold')"
|
||||||
>
|
>
|
||||||
<b-icon icon="format-bold" />
|
<b-icon icon="format-bold" />
|
||||||
<span class="visually-hidden">{{ $t("Bold") }}</span>
|
<span class="visually-hidden">{{ $t("Bold") }}</span>
|
||||||
|
@ -165,6 +180,7 @@
|
||||||
:class="{ 'is-active': editor.isActive('italic') }"
|
:class="{ 'is-active': editor.isActive('italic') }"
|
||||||
@click="editor.chain().focus().toggleItalic().run()"
|
@click="editor.chain().focus().toggleItalic().run()"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('Italic')"
|
||||||
>
|
>
|
||||||
<b-icon icon="format-italic" />
|
<b-icon icon="format-italic" />
|
||||||
<span class="visually-hidden">{{ $t("Italic") }}</span>
|
<span class="visually-hidden">{{ $t("Italic") }}</span>
|
||||||
|
@ -179,10 +195,18 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
import { Editor, EditorContent, BubbleMenu } from "@tiptap/vue-2";
|
import { Editor, EditorContent, BubbleMenu } from "@tiptap/vue-2";
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
import Blockquote from "@tiptap/extension-blockquote";
|
||||||
|
import BulletList from "@tiptap/extension-bullet-list";
|
||||||
|
import Heading from "@tiptap/extension-heading";
|
||||||
import Document from "@tiptap/extension-document";
|
import Document from "@tiptap/extension-document";
|
||||||
import Paragraph from "@tiptap/extension-paragraph";
|
import Paragraph from "@tiptap/extension-paragraph";
|
||||||
|
import Bold from "@tiptap/extension-bold";
|
||||||
|
import Italic from "@tiptap/extension-italic";
|
||||||
|
import Strike from "@tiptap/extension-strike";
|
||||||
import Text from "@tiptap/extension-text";
|
import Text from "@tiptap/extension-text";
|
||||||
|
import Dropcursor from "@tiptap/extension-dropcursor";
|
||||||
|
import Gapcursor from "@tiptap/extension-gapcursor";
|
||||||
|
import History from "@tiptap/extension-history";
|
||||||
import { IActor, IPerson, usernameWithDomain } from "../types/actor";
|
import { IActor, IPerson, usernameWithDomain } from "../types/actor";
|
||||||
import CustomImage from "./Editor/Image";
|
import CustomImage from "./Editor/Image";
|
||||||
import { UPLOAD_MEDIA } from "../graphql/upload";
|
import { UPLOAD_MEDIA } from "../graphql/upload";
|
||||||
|
@ -194,7 +218,8 @@ import OrderedList from "@tiptap/extension-ordered-list";
|
||||||
import ListItem from "@tiptap/extension-list-item";
|
import ListItem from "@tiptap/extension-list-item";
|
||||||
import Underline from "@tiptap/extension-underline";
|
import Underline from "@tiptap/extension-underline";
|
||||||
import Link from "@tiptap/extension-link";
|
import Link from "@tiptap/extension-link";
|
||||||
import CharacterCount from "@tiptap/extension-character-count";
|
import { AutoDir } from "./Editor/Autodir";
|
||||||
|
import sanitizeHtml from "sanitize-html";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { EditorContent, BubbleMenu },
|
components: { EditorContent, BubbleMenu },
|
||||||
|
@ -211,6 +236,8 @@ export default class EditorComponent extends Vue {
|
||||||
|
|
||||||
@Prop({ required: false, default: 100_000_000 }) maxSize!: number;
|
@Prop({ required: false, default: 100_000_000 }) maxSize!: number;
|
||||||
|
|
||||||
|
@Prop({ required: false }) ariaLabel!: string;
|
||||||
|
|
||||||
currentActor!: IPerson;
|
currentActor!: IPerson;
|
||||||
|
|
||||||
editor: Editor | null = null;
|
editor: Editor | null = null;
|
||||||
|
@ -240,8 +267,18 @@ export default class EditorComponent extends Vue {
|
||||||
|
|
||||||
mounted(): void {
|
mounted(): void {
|
||||||
this.editor = new Editor({
|
this.editor = new Editor({
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
"aria-multiline": this.isShortMode.toString(),
|
||||||
|
"aria-label": this.ariaLabel,
|
||||||
|
role: "textbox",
|
||||||
|
},
|
||||||
|
transformPastedHTML: this.transformPastedHTML,
|
||||||
|
},
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit,
|
Blockquote,
|
||||||
|
BulletList,
|
||||||
|
Heading,
|
||||||
Document,
|
Document,
|
||||||
Paragraph,
|
Paragraph,
|
||||||
Text,
|
Text,
|
||||||
|
@ -249,13 +286,17 @@ export default class EditorComponent extends Vue {
|
||||||
ListItem,
|
ListItem,
|
||||||
Mention.configure(MentionOptions),
|
Mention.configure(MentionOptions),
|
||||||
CustomImage,
|
CustomImage,
|
||||||
|
AutoDir,
|
||||||
Underline,
|
Underline,
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
Strike,
|
||||||
|
Dropcursor,
|
||||||
|
Gapcursor,
|
||||||
|
History,
|
||||||
Link.configure({
|
Link.configure({
|
||||||
HTMLAttributes: { target: "_blank", rel: "noopener noreferrer ugc" },
|
HTMLAttributes: { target: "_blank", rel: "noopener noreferrer ugc" },
|
||||||
}),
|
}),
|
||||||
CharacterCount.configure({
|
|
||||||
limit: this.maxSize,
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
injectCSS: false,
|
injectCSS: false,
|
||||||
content: this.value,
|
content: this.value,
|
||||||
|
@ -265,6 +306,19 @@ export default class EditorComponent extends Vue {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transformPastedHTML(html: string): string {
|
||||||
|
// When using comment mode, limit to acceptable tags
|
||||||
|
if (this.isCommentMode) {
|
||||||
|
return sanitizeHtml(html, {
|
||||||
|
allowedTags: ["b", "i", "em", "strong", "a"],
|
||||||
|
allowedAttributes: {
|
||||||
|
a: ["href", "rel", "target"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
@Watch("value")
|
@Watch("value")
|
||||||
onValueChanged(val: string): void {
|
onValueChanged(val: string): void {
|
||||||
if (!this.editor) return;
|
if (!this.editor) return;
|
||||||
|
@ -315,7 +369,7 @@ export default class EditorComponent extends Vue {
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||||
this.$notifier.error(error.graphQLErrors[0].message);
|
this.$notifier.error(error.graphQLErrors[0].message);
|
||||||
|
@ -351,6 +405,7 @@ export default class EditorComponent extends Vue {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
@import "./Editor/style.scss";
|
@import "./Editor/style.scss";
|
||||||
|
|
||||||
$color-black: #000;
|
$color-black: #000;
|
||||||
|
@ -367,7 +422,7 @@ $color-white: #eee;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: $color-black;
|
color: $color-black;
|
||||||
padding: 0.2rem 0.5rem;
|
padding: 0.2rem 0.5rem;
|
||||||
margin-right: 0.2rem;
|
@include margin-right(0.2rem);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
@ -439,7 +494,7 @@ $color-white: #eee;
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
padding-left: 1rem;
|
@include padding-left(1rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
@ -455,7 +510,7 @@ $color-white: #eee;
|
||||||
blockquote {
|
blockquote {
|
||||||
border-left: 3px solid rgba($color-black, 0.1);
|
border-left: 3px solid rgba($color-black, 0.1);
|
||||||
color: rgba($color-black, 0.8);
|
color: rgba($color-black, 0.8);
|
||||||
padding-left: 0.8rem;
|
@include padding-left(0.8rem);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
@ -511,7 +566,7 @@ $color-white: #eee;
|
||||||
}
|
}
|
||||||
&.is-selected,
|
&.is-selected,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($color-white, 0.2);
|
background-color: rgba(#eee, 0.2);
|
||||||
}
|
}
|
||||||
&.is-empty {
|
&.is-empty {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
@ -528,7 +583,7 @@ $color-white: #eee;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
text-align: inherit;
|
text-align: inherit;
|
||||||
color: $color-white;
|
color: #eee;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
30
js/src/components/Editor/Autodir.ts
Normal file
30
js/src/components/Editor/Autodir.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { Extension } from "@tiptap/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to set dir="auto" on top nodes
|
||||||
|
* Taken from https://github.com/ueberdosis/tiptap/issues/1621#issuecomment-918990408
|
||||||
|
*/
|
||||||
|
export const AutoDir = Extension.create({
|
||||||
|
name: "AutoDir",
|
||||||
|
addGlobalAttributes() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
types: [
|
||||||
|
"heading",
|
||||||
|
"paragraph",
|
||||||
|
"bulletList",
|
||||||
|
"orderedList",
|
||||||
|
"blockquote",
|
||||||
|
],
|
||||||
|
attributes: {
|
||||||
|
autoDir: {
|
||||||
|
renderHTML: () => ({
|
||||||
|
dir: "auto",
|
||||||
|
}),
|
||||||
|
parseHTML: (element) => element.dir || "auto",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
|
@ -7,6 +7,8 @@ import apolloProvider from "@/vue-apollo";
|
||||||
import { IPerson } from "@/types/actor";
|
import { IPerson } from "@/types/actor";
|
||||||
import pDebounce from "p-debounce";
|
import pDebounce from "p-debounce";
|
||||||
import { NormalizedCacheObject } from "@apollo/client/cache/inmemory/types";
|
import { NormalizedCacheObject } from "@apollo/client/cache/inmemory/types";
|
||||||
|
import { MentionOptions } from "@tiptap/extension-mention";
|
||||||
|
import { Editor } from "@tiptap/core";
|
||||||
|
|
||||||
const client =
|
const client =
|
||||||
apolloProvider.defaultClient as ApolloClient<NormalizedCacheObject>;
|
apolloProvider.defaultClient as ApolloClient<NormalizedCacheObject>;
|
||||||
|
@ -24,12 +26,21 @@ const fetchItems = async (query: string): Promise<IPerson[]> => {
|
||||||
|
|
||||||
const debouncedFetchItems = pDebounce(fetchItems, 200);
|
const debouncedFetchItems = pDebounce(fetchItems, 200);
|
||||||
|
|
||||||
const mentionOptions: Partial<any> = {
|
const mentionOptions: MentionOptions = {
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "mention",
|
class: "mention",
|
||||||
|
dir: "ltr",
|
||||||
|
},
|
||||||
|
renderLabel({ options, node }) {
|
||||||
|
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`;
|
||||||
},
|
},
|
||||||
suggestion: {
|
suggestion: {
|
||||||
items: async (query: string): Promise<IPerson[]> => {
|
items: async ({
|
||||||
|
query,
|
||||||
|
}: {
|
||||||
|
query: string;
|
||||||
|
editor: Editor;
|
||||||
|
}): Promise<IPerson[]> => {
|
||||||
if (query === "") {
|
if (query === "") {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -69,8 +80,12 @@ const mentionOptions: Partial<any> = {
|
||||||
return component.ref?.onKeyDown(props);
|
return component.ref?.onKeyDown(props);
|
||||||
},
|
},
|
||||||
onExit() {
|
onExit() {
|
||||||
popup[0].destroy();
|
if (popup && popup[0]) {
|
||||||
component.destroy();
|
popup[0].destroy();
|
||||||
|
}
|
||||||
|
if (component) {
|
||||||
|
component.destroy();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
:key="index"
|
:key="index"
|
||||||
@click="selectItem(index)"
|
@click="selectItem(index)"
|
||||||
>
|
>
|
||||||
<actor-card :actor="item" />
|
<actor-inline :actor="item" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -16,11 +16,11 @@
|
||||||
import { Vue, Component, Prop, Watch } from "vue-property-decorator";
|
import { Vue, Component, Prop, Watch } from "vue-property-decorator";
|
||||||
import { displayName, usernameWithDomain } from "@/types/actor/actor.model";
|
import { displayName, usernameWithDomain } from "@/types/actor/actor.model";
|
||||||
import { IPerson } from "@/types/actor";
|
import { IPerson } from "@/types/actor";
|
||||||
import ActorCard from "../../components/Account/ActorCard.vue";
|
import ActorInline from "../../components/Account/ActorInline.vue";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
ActorCard,
|
ActorInline,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class MentionList extends Vue {
|
export default class MentionList extends Vue {
|
||||||
|
|
|
@ -5,10 +5,14 @@
|
||||||
|
|
||||||
.ProseMirror {
|
.ProseMirror {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
}
|
||||||
|
.ProseMirror {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
white-space: break-spaces;
|
||||||
-webkit-font-variant-ligatures: none;
|
-webkit-font-variant-ligatures: none;
|
||||||
font-variant-ligatures: none;
|
font-variant-ligatures: none;
|
||||||
|
font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */
|
||||||
|
|
||||||
& [contenteditable="false"] {
|
& [contenteditable="false"] {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
@ -16,14 +20,22 @@
|
||||||
& [contenteditable="false"] [contenteditable="true"] {
|
& [contenteditable="false"] [contenteditable="true"] {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
pre {
|
& pre {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
img.ProseMirror-separator {
|
||||||
|
display: inline !important;
|
||||||
|
border: none !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
}
|
||||||
.ProseMirror-gapcursor {
|
.ProseMirror-gapcursor {
|
||||||
display: none;
|
display: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: "";
|
content: "";
|
||||||
|
@ -40,16 +52,17 @@
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ProseMirror-hideselection * {
|
.ProseMirror-hideselection {
|
||||||
&::selection {
|
*::selection {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
&::-moz-selection {
|
*::-moz-selection {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
caret-color: transparent;
|
* {
|
||||||
|
caret-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-focused .ProseMirror-gapcursor {
|
.ProseMirror-focused .ProseMirror-gapcursor {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,13 +48,75 @@
|
||||||
$t("Mobilizon")
|
$t("Mobilizon")
|
||||||
}}</a>
|
}}</a>
|
||||||
</i18n>
|
</i18n>
|
||||||
{{
|
<span v-if="sentryEnabled && sentryReady">
|
||||||
$t(
|
{{
|
||||||
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):"
|
$t(
|
||||||
)
|
"We collect your feedback and the error information in order to improve this service."
|
||||||
}}
|
)
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
<span v-else>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="content">
|
<form
|
||||||
|
v-if="sentryEnabled && sentryReady && !submittedFeedback"
|
||||||
|
@submit.prevent="sendErrorToSentry"
|
||||||
|
>
|
||||||
|
<b-field :label="$t('What happened?')" label-for="what-happened">
|
||||||
|
<b-input
|
||||||
|
v-model="feedback"
|
||||||
|
type="textarea"
|
||||||
|
id="what-happened"
|
||||||
|
:placeholder="$t(`I've clicked on X, then on Y`)"
|
||||||
|
/>
|
||||||
|
</b-field>
|
||||||
|
<b-button icon-left="send" native-type="submit" type="is-primary">{{
|
||||||
|
$t("Send feedback")
|
||||||
|
}}</b-button>
|
||||||
|
<p class="content">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"Please add as many details as possible to help identify the problem."
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<b-message type="is-danger" v-else-if="feedbackError">
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"Sorry, we wen't able to save your feedback. Don't worry, we'll try to fix this issue anyway."
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<i18n path="You may now close this page or {return_to_the_homepage}.">
|
||||||
|
<template #return_to_the_homepage>
|
||||||
|
<router-link :to="{ name: RouteName.HOME }">{{
|
||||||
|
$t("return to the homepage")
|
||||||
|
}}</router-link>
|
||||||
|
</template>
|
||||||
|
</i18n>
|
||||||
|
</b-message>
|
||||||
|
<b-message type="is-success" v-else-if="submittedFeedback">
|
||||||
|
<p>{{ $t("Thanks a lot, your feedback was submitted!") }}</p>
|
||||||
|
<i18n path="You may now close this page or {return_to_the_homepage}.">
|
||||||
|
<template #return_to_the_homepage>
|
||||||
|
<router-link :to="{ name: RouteName.HOME }">{{
|
||||||
|
$t("return to the homepage")
|
||||||
|
}}</router-link>
|
||||||
|
</template>
|
||||||
|
</i18n>
|
||||||
|
</b-message>
|
||||||
|
<div
|
||||||
|
class="content"
|
||||||
|
v-if="!(sentryEnabled && sentryReady) || submittedFeedback"
|
||||||
|
>
|
||||||
|
<p v-if="submittedFeedback">{{ $t("You may also:") }}</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
|
@ -65,7 +127,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="https://framagit.org/framasoft/mobilizon/-/issues/new?issuable_template=Bug"
|
href="https://framagit.org/framasoft/mobilizon/-/issues/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>{{
|
>{{
|
||||||
$t("Open an issue on our bug tracker (advanced users)")
|
$t("Open an issue on our bug tracker (advanced users)")
|
||||||
|
@ -74,7 +136,7 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<p class="content">
|
<p class="content" v-if="!sentryEnabled">
|
||||||
{{
|
{{
|
||||||
$t(
|
$t(
|
||||||
"Please add as many details as possible to help identify the problem."
|
"Please add as many details as possible to help identify the problem."
|
||||||
|
@ -89,23 +151,25 @@
|
||||||
<p>{{ $t("Error stacktrace") }}</p>
|
<p>{{ $t("Error stacktrace") }}</p>
|
||||||
<pre>{{ error.stack }}</pre>
|
<pre>{{ error.stack }}</pre>
|
||||||
</details>
|
</details>
|
||||||
<p>
|
<p v-if="!sentryEnabled">
|
||||||
{{
|
{{
|
||||||
$t(
|
$t(
|
||||||
"The technical details of the error can help developers solve the problem more easily. Please add them to your feedback."
|
"The technical details of the error can help developers solve the problem more easily. Please add them to your feedback."
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
<div class="buttons">
|
<div class="buttons" v-if="!sentryEnabled">
|
||||||
<b-tooltip
|
<b-tooltip
|
||||||
:label="tooltipConfig.label"
|
:label="tooltipConfig.label"
|
||||||
:type="tooltipConfig.type"
|
:type="tooltipConfig.type"
|
||||||
:active="copied !== false"
|
:active="copied !== false"
|
||||||
always
|
always
|
||||||
>
|
>
|
||||||
<b-button @click="copyErrorToClipboard">{{
|
<b-button
|
||||||
$t("Copy details to clipboard")
|
@click="copyErrorToClipboard"
|
||||||
}}</b-button>
|
@keyup.enter="copyErrorToClipboard"
|
||||||
|
>{{ $t("Copy details to clipboard") }}</b-button
|
||||||
|
>
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -113,14 +177,20 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { CONTACT } from "@/graphql/config";
|
import { CONFIG } from "@/graphql/config";
|
||||||
|
import { checkProviderConfig, convertConfig } from "@/services/statistics";
|
||||||
|
import { IAnalyticsConfig, IConfig } from "@/types/config.model";
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import { LOGGED_USER } from "@/graphql/user";
|
||||||
|
import { IUser } from "@/types/current-user.model";
|
||||||
|
import { ISentryConfiguration } from "@/types/analytics/sentry.model";
|
||||||
|
import { submitFeedback } from "@/services/statistics/sentry";
|
||||||
|
import RouteName from "@/router/name";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
apollo: {
|
apollo: {
|
||||||
config: {
|
config: CONFIG,
|
||||||
query: CONTACT,
|
loggedUser: LOGGED_USER,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
metaInfo() {
|
metaInfo() {
|
||||||
return {
|
return {
|
||||||
|
@ -136,7 +206,17 @@ export default class ErrorComponent extends Vue {
|
||||||
|
|
||||||
copied: "success" | "error" | false = false;
|
copied: "success" | "error" | false = false;
|
||||||
|
|
||||||
config!: { contact: string | null; name: string };
|
config!: IConfig;
|
||||||
|
|
||||||
|
feedback = "";
|
||||||
|
|
||||||
|
submittedFeedback = false;
|
||||||
|
|
||||||
|
feedbackError = false;
|
||||||
|
|
||||||
|
loggedUser!: IUser;
|
||||||
|
|
||||||
|
RouteName = RouteName;
|
||||||
|
|
||||||
async copyErrorToClipboard(): Promise<void> {
|
async copyErrorToClipboard(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
@ -191,6 +271,56 @@ export default class ErrorComponent extends Vue {
|
||||||
|
|
||||||
document.body.removeChild(textArea);
|
document.body.removeChild(textArea);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get sentryEnabled(): boolean {
|
||||||
|
return this.sentryProvider?.enabled === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sentryProvider(): IAnalyticsConfig | undefined {
|
||||||
|
return this.config && checkProviderConfig(this.config, "sentry");
|
||||||
|
}
|
||||||
|
|
||||||
|
get sentryConfig(): ISentryConfiguration | undefined {
|
||||||
|
if (this.sentryProvider?.configuration) {
|
||||||
|
return convertConfig(
|
||||||
|
this.sentryProvider?.configuration
|
||||||
|
) as ISentryConfiguration;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sentryReady() {
|
||||||
|
const eventId = window.sessionStorage.getItem("lastEventId");
|
||||||
|
const dsn = this.sentryConfig?.dsn;
|
||||||
|
const organization = this.sentryConfig?.organization;
|
||||||
|
const project = this.sentryConfig?.project;
|
||||||
|
const host = this.sentryConfig?.host;
|
||||||
|
return eventId && dsn && organization && project && host;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendErrorToSentry() {
|
||||||
|
try {
|
||||||
|
const eventId = window.sessionStorage.getItem("lastEventId");
|
||||||
|
const dsn = this.sentryConfig?.dsn;
|
||||||
|
const organization = this.sentryConfig?.organization;
|
||||||
|
const project = this.sentryConfig?.project;
|
||||||
|
const host = this.sentryConfig?.host;
|
||||||
|
const endpoint = `https://${host}/api/0/projects/${organization}/${project}/user-feedback/`;
|
||||||
|
if (eventId && dsn && this.sentryReady) {
|
||||||
|
await submitFeedback(endpoint, dsn, {
|
||||||
|
event_id: eventId,
|
||||||
|
name:
|
||||||
|
this.loggedUser?.defaultActor?.preferredUsername || "Unknown user",
|
||||||
|
email: this.loggedUser?.email || "unknown@email.org",
|
||||||
|
comments: this.feedback,
|
||||||
|
});
|
||||||
|
this.submittedFeedback = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
this.feedbackError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
icon="map-marker"
|
icon="map-marker"
|
||||||
expanded
|
expanded
|
||||||
@select="updateSelected"
|
@select="updateSelected"
|
||||||
|
v-bind="$attrs"
|
||||||
|
dir="auto"
|
||||||
>
|
>
|
||||||
<template #default="{ option }">
|
<template #default="{ option }">
|
||||||
<b-icon :icon="option.poiInfos.poiIcon.icon" />
|
<b-icon :icon="option.poiInfos.poiIcon.icon" />
|
||||||
|
@ -20,12 +22,17 @@
|
||||||
</template>
|
</template>
|
||||||
</b-autocomplete>
|
</b-autocomplete>
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field v-if="canDoGeoLocation">
|
<b-field
|
||||||
|
v-if="canDoGeoLocation"
|
||||||
|
:message="fieldErrors"
|
||||||
|
:type="{ 'is-danger': fieldErrors.length }"
|
||||||
|
>
|
||||||
<b-button
|
<b-button
|
||||||
type="is-text"
|
type="is-text"
|
||||||
v-if="!gettingLocation"
|
v-if="!gettingLocation"
|
||||||
icon-right="target"
|
icon-right="target"
|
||||||
@click="locateMe"
|
@click="locateMe"
|
||||||
|
@keyup.enter="locateMe"
|
||||||
>{{ $t("Use my location") }}</b-button
|
>{{ $t("Use my location") }}</b-button
|
||||||
>
|
>
|
||||||
<span v-else>{{ $t("Getting location") }}</span>
|
<span v-else>{{ $t("Getting location") }}</span>
|
||||||
|
@ -52,26 +59,16 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
import { Component, Mixins, Prop, Watch } from "vue-property-decorator";
|
||||||
import { LatLng } from "leaflet";
|
|
||||||
import debounce from "lodash/debounce";
|
|
||||||
import { DebouncedFunc } from "lodash";
|
|
||||||
import { Address, IAddress } from "../../types/address.model";
|
import { Address, IAddress } from "../../types/address.model";
|
||||||
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
|
import AddressAutoCompleteMixin from "@/mixins/AddressAutoCompleteMixin";
|
||||||
import { CONFIG } from "../../graphql/config";
|
|
||||||
import { IConfig } from "../../types/config.model";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
inheritAttrs: false,
|
||||||
"map-leaflet": () =>
|
|
||||||
import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
|
|
||||||
},
|
|
||||||
apollo: {
|
|
||||||
config: CONFIG,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
export default class AddressAutoComplete extends Vue {
|
export default class AddressAutoComplete extends Mixins(
|
||||||
@Prop({ required: true }) value!: IAddress;
|
AddressAutoCompleteMixin
|
||||||
|
) {
|
||||||
@Prop({ required: false, default: false }) type!: string | false;
|
@Prop({ required: false, default: false }) type!: string | false;
|
||||||
@Prop({ required: false, default: true, type: Boolean })
|
@Prop({ required: false, default: true, type: Boolean })
|
||||||
doGeoLocation!: boolean;
|
doGeoLocation!: boolean;
|
||||||
|
@ -80,84 +77,20 @@ export default class AddressAutoComplete extends Vue {
|
||||||
|
|
||||||
selected: IAddress = new Address();
|
selected: IAddress = new Address();
|
||||||
|
|
||||||
isFetching = false;
|
|
||||||
|
|
||||||
initialQueryText = "";
|
initialQueryText = "";
|
||||||
|
|
||||||
addressModalActive = false;
|
addressModalActive = false;
|
||||||
|
|
||||||
showmap = false;
|
showmap = false;
|
||||||
|
|
||||||
private gettingLocation = false;
|
get queryText2(): string {
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
private location!: GeolocationPosition;
|
|
||||||
|
|
||||||
private gettingLocationError: any;
|
|
||||||
|
|
||||||
private mapDefaultZoom = 15;
|
|
||||||
|
|
||||||
config!: IConfig;
|
|
||||||
|
|
||||||
fetchAsyncData!: DebouncedFunc<(query: string) => Promise<void>>;
|
|
||||||
|
|
||||||
// We put this in data because of issues like
|
|
||||||
// https://github.com/vuejs/vue-class-component/issues/263
|
|
||||||
data(): Record<string, unknown> {
|
|
||||||
return {
|
|
||||||
fetchAsyncData: debounce(this.asyncData, 200),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async asyncData(query: string): Promise<void> {
|
|
||||||
if (!query.length) {
|
|
||||||
this.addressData = [];
|
|
||||||
this.selected = new Address();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.length < 3) {
|
|
||||||
this.addressData = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isFetching = true;
|
|
||||||
const variables: { query: string; locale: string; type?: string } = {
|
|
||||||
query,
|
|
||||||
locale: this.$i18n.locale,
|
|
||||||
};
|
|
||||||
if (this.type) {
|
|
||||||
variables.type = this.type;
|
|
||||||
}
|
|
||||||
const result = await this.$apollo.query({
|
|
||||||
query: ADDRESS,
|
|
||||||
fetchPolicy: "network-only",
|
|
||||||
variables,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addressData = result.data.searchAddress.map(
|
|
||||||
(address: IAddress) => new Address(address)
|
|
||||||
);
|
|
||||||
this.isFetching = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("config")
|
|
||||||
watchConfig(config: IConfig): void {
|
|
||||||
if (!config.geocoding.autocomplete) {
|
|
||||||
// If autocomplete is disabled, we put a larger debounce value
|
|
||||||
// so that we don't request with incomplete address
|
|
||||||
this.fetchAsyncData = debounce(this.asyncData, 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get queryText(): string {
|
|
||||||
if (this.value !== undefined) {
|
if (this.value !== undefined) {
|
||||||
return new Address(this.value).fullName;
|
return new Address(this.value).fullName;
|
||||||
}
|
}
|
||||||
return this.initialQueryText;
|
return this.initialQueryText;
|
||||||
}
|
}
|
||||||
|
|
||||||
set queryText(queryText: string) {
|
set queryText2(queryText: string) {
|
||||||
this.initialQueryText = queryText;
|
this.initialQueryText = queryText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,80 +119,6 @@ export default class AddressAutoComplete extends Vue {
|
||||||
this.showmap = !this.showmap;
|
this.showmap = !this.showmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
async reverseGeoCode(e: LatLng, zoom: number): Promise<void> {
|
|
||||||
// If the position has been updated through autocomplete selection, no need to geocode it!
|
|
||||||
if (this.checkCurrentPosition(e)) return;
|
|
||||||
const result = await this.$apollo.query({
|
|
||||||
query: REVERSE_GEOCODE,
|
|
||||||
variables: {
|
|
||||||
latitude: e.lat,
|
|
||||||
longitude: e.lng,
|
|
||||||
zoom,
|
|
||||||
locale: this.$i18n.locale,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addressData = result.data.reverseGeocode.map(
|
|
||||||
(address: IAddress) => new Address(address)
|
|
||||||
);
|
|
||||||
if (this.addressData.length > 0) {
|
|
||||||
const defaultAddress = new Address(this.addressData[0]);
|
|
||||||
this.selected = defaultAddress;
|
|
||||||
this.$emit("input", this.selected);
|
|
||||||
this.queryText = `${defaultAddress.poiInfos.name} ${defaultAddress.poiInfos.alternativeName}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkCurrentPosition(e: LatLng): boolean {
|
|
||||||
if (!this.selected || !this.selected.geom) return false;
|
|
||||||
const lat = parseFloat(this.selected.geom.split(";")[1]);
|
|
||||||
const lon = parseFloat(this.selected.geom.split(";")[0]);
|
|
||||||
|
|
||||||
return e.lat === lat && e.lng === lon;
|
|
||||||
}
|
|
||||||
|
|
||||||
async locateMe(): Promise<void> {
|
|
||||||
this.gettingLocation = true;
|
|
||||||
try {
|
|
||||||
this.location = await AddressAutoComplete.getLocation();
|
|
||||||
this.mapDefaultZoom = 12;
|
|
||||||
this.reverseGeoCode(
|
|
||||||
new LatLng(
|
|
||||||
this.location.coords.latitude,
|
|
||||||
this.location.coords.longitude
|
|
||||||
),
|
|
||||||
12
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
this.gettingLocationError = e.message;
|
|
||||||
}
|
|
||||||
this.gettingLocation = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
static async getLocation(): Promise<GeolocationPosition> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!("geolocation" in navigator)) {
|
|
||||||
reject(new Error("Geolocation is not available."));
|
|
||||||
}
|
|
||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
|
||||||
(pos) => {
|
|
||||||
resolve(pos);
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
get isSecureContext(): boolean {
|
|
||||||
return window.isSecureContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
get canDoGeoLocation(): boolean {
|
get canDoGeoLocation(): boolean {
|
||||||
return this.isSecureContext && this.doGeoLocation;
|
return this.isSecureContext && this.doGeoLocation;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,18 +12,17 @@
|
||||||
</docs>
|
</docs>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<time
|
<div
|
||||||
class="datetime-container"
|
class="datetime-container"
|
||||||
:class="{ small }"
|
:class="{ small }"
|
||||||
:datetime="dateObj.getUTCSeconds()"
|
|
||||||
:style="`--small: ${smallStyle}`"
|
:style="`--small: ${smallStyle}`"
|
||||||
>
|
>
|
||||||
<div class="datetime-container-header" />
|
<div class="datetime-container-header" />
|
||||||
<div class="datetime-container-content">
|
<div class="datetime-container-content">
|
||||||
<span class="day">{{ day }}</span>
|
<time :datetime="dateObj.toISOString()" class="day">{{ day }}</time>
|
||||||
<span class="month">{{ month }}</span>
|
<time :datetime="dateObj.toISOString()" class="month">{{ month }}</time>
|
||||||
</div>
|
</div>
|
||||||
</time>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
@ -54,14 +53,13 @@ export default class DateCalendarIcon extends Vue {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
time.datetime-container {
|
div.datetime-container {
|
||||||
background: $chapril_blue_light;
|
background: #fff;
|
||||||
border: 1px solid $borders;
|
border: 1px solid $borders;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 50px;
|
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
@ -80,7 +78,7 @@ time.datetime-container {
|
||||||
height: calc(30px * var(--small));
|
height: calc(30px * var(--small));
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
time {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: $violet-3;
|
color: $violet-3;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<router-link
|
<router-link
|
||||||
class="card"
|
class="card"
|
||||||
:to="{ name: 'Event', params: { uuid: event.uuid } }"
|
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
||||||
>
|
>
|
||||||
<div class="card-image">
|
<div class="card-image">
|
||||||
<figure class="image is-16by9">
|
<figure class="image is-16by9">
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
v-for="tag in (event.tags || []).slice(0, 3)"
|
v-for="tag in (event.tags || []).slice(0, 3)"
|
||||||
:key="tag.slug"
|
:key="tag.slug"
|
||||||
>
|
>
|
||||||
<b-tag type="is-light">{{ tag.title }}</b-tag>
|
<b-tag type="is-light" dir="auto">{{ tag.title }}</b-tag>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</figure>
|
</figure>
|
||||||
|
@ -39,79 +39,71 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="event-title" :title="event.title">{{ event.title }}</p>
|
<h3
|
||||||
<div
|
class="event-title"
|
||||||
class="event-subtitle"
|
:title="event.title"
|
||||||
v-if="event.physicalAddress"
|
dir="auto"
|
||||||
:title="
|
:lang="event.language"
|
||||||
isDescriptionDifferentFromLocality
|
|
||||||
? `${event.physicalAddress.description}, ${event.physicalAddress.locality}`
|
|
||||||
: event.physicalAddress.description
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<!-- <p>{{ $t('By @{username}', { username: actor.preferredUsername }) }}</p>-->
|
{{ event.title }}
|
||||||
<span v-if="isDescriptionDifferentFromLocality">
|
</h3>
|
||||||
{{ event.physicalAddress.description }},
|
<div class="content-end">
|
||||||
{{ event.physicalAddress.locality }}
|
<div class="event-organizer" dir="auto">
|
||||||
</span>
|
<figure
|
||||||
<span v-else>
|
class="image is-24x24"
|
||||||
{{ event.physicalAddress.description }}
|
v-if="organizer(event) && organizer(event).avatar"
|
||||||
</span>
|
>
|
||||||
|
<img
|
||||||
|
class="is-rounded"
|
||||||
|
:src="organizer(event).avatar.url"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
<b-icon v-else icon="account-circle" />
|
||||||
|
<span class="organizer-name">
|
||||||
|
{{ organizerDisplayName(event) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<inline-address
|
||||||
|
dir="auto"
|
||||||
|
v-if="event.physicalAddress"
|
||||||
|
:physical-address="event.physicalAddress"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="event-subtitle"
|
||||||
|
dir="auto"
|
||||||
|
v-else-if="event.options && event.options.isOnline"
|
||||||
|
>
|
||||||
|
<b-icon icon="video" />
|
||||||
|
<span>{{ $t("Online") }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="date-and-title">-->
|
|
||||||
<!-- <div class="date-component">-->
|
|
||||||
<!-- <date-calendar-icon v-if="!mergedOptions.hideDate" :date="event.beginsOn" />-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <div class="title-wrapper">-->
|
|
||||||
<!-- <h4>{{ event.title }}</h4>-->
|
|
||||||
<!-- <div class="organizer-place-wrapper has-text-grey">-->
|
|
||||||
<!-- <span>{{ $t('By @{username}', { username: actor.preferredUsername }) }}</span>-->
|
|
||||||
<!-- ·-->
|
|
||||||
<!-- <span v-if="event.physicalAddress">-->
|
|
||||||
<!-- {{ event.physicalAddress.description }}, {{ event.physicalAddress.locality }}-->
|
|
||||||
<!-- </span>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <div v-if="!mergedOptions.hideDetails" class="details">-->
|
|
||||||
<!-- <div v-if="event.participants.length > 0 &&-->
|
|
||||||
<!-- mergedOptions.loggedPerson &&-->
|
|
||||||
<!-- event.participants[0].actor.id === mergedOptions.loggedPerson.id">-->
|
|
||||||
<!-- <b-tag type="is-info"><translate>Organizer</translate></b-tag>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <div v-else-if="event.participants.length === 1">-->
|
|
||||||
<!-- <translate-->
|
|
||||||
<!-- :translate-params="{name: event.participants[0].actor.preferredUsername}"-->
|
|
||||||
<!-- >{name} organizes this event</translate>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <div v-else>-->
|
|
||||||
<!-- <span v-for="participant in event.participants" :key="participant.actor.uuid">-->
|
|
||||||
<!-- {{ participant.actor.preferredUsername }}-->
|
|
||||||
<!-- <span v-if="participant.role === ParticipantRole.CREATOR">(organizer)</span>,-->
|
|
||||||
<!-- <!– <translate-->
|
|
||||||
<!-- :translate-params="{name: participant.actor.preferredUsername}"-->
|
|
||||||
<!-- > {name} is in,</translate>–>-->
|
|
||||||
<!-- </span>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { IEvent, IEventCardOptions } from "@/types/event.model";
|
import {
|
||||||
|
IEvent,
|
||||||
|
IEventCardOptions,
|
||||||
|
organizerDisplayName,
|
||||||
|
organizer,
|
||||||
|
} from "@/types/event.model";
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||||
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
|
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
|
||||||
import { Actor, Person } from "@/types/actor";
|
import { Actor, Person } from "@/types/actor";
|
||||||
import { EventStatus, ParticipantRole } from "@/types/enums";
|
import { EventStatus, ParticipantRole } from "@/types/enums";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
import InlineAddress from "@/components/Address/InlineAddress.vue";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
DateCalendarIcon,
|
DateCalendarIcon,
|
||||||
LazyImageWrapper,
|
LazyImageWrapper,
|
||||||
|
InlineAddress,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class EventCard extends Vue {
|
export default class EventCard extends Vue {
|
||||||
|
@ -125,6 +117,10 @@ export default class EventCard extends Vue {
|
||||||
|
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
|
|
||||||
|
organizerDisplayName = organizerDisplayName;
|
||||||
|
|
||||||
|
organizer = organizer;
|
||||||
|
|
||||||
defaultOptions: IEventCardOptions = {
|
defaultOptions: IEventCardOptions = {
|
||||||
hideDate: false,
|
hideDate: false,
|
||||||
loggedPerson: false,
|
loggedPerson: false,
|
||||||
|
@ -143,18 +139,13 @@ export default class EventCard extends Vue {
|
||||||
this.event.organizerActor || this.mergedOptions.organizerActor
|
this.event.organizerActor || this.mergedOptions.organizerActor
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isDescriptionDifferentFromLocality(): boolean {
|
|
||||||
return (
|
|
||||||
this.event?.physicalAddress?.description !==
|
|
||||||
this.event?.physicalAddress?.locality &&
|
|
||||||
this.event?.physicalAddress?.description !== undefined
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
|
@use "@/styles/_event-card";
|
||||||
|
|
||||||
a.card {
|
a.card {
|
||||||
display: block;
|
display: block;
|
||||||
background: $secondary;
|
background: $secondary;
|
||||||
|
@ -190,22 +181,28 @@ a.card {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin-right: -3px;
|
@include margin-right(-3px);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
max-width: 40%;
|
max-width: 40%;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
span.tag {
|
span.tag {
|
||||||
margin: 5px auto;
|
margin: 5px auto;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
line-height: 1.75em;
|
line-height: 1.75em;
|
||||||
|
|
||||||
|
&:not(.is-info, .is-danger) {
|
||||||
background-color: #e6e4f4;
|
background-color: #e6e4f4;
|
||||||
color: #3c376e;
|
color: $violet-3;
|
||||||
|
}
|
||||||
|
&.is-info {
|
||||||
|
color: $violet-3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,12 +217,14 @@ a.card {
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-content {
|
.card-content {
|
||||||
|
height: 100%;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
|
||||||
& > .media {
|
& > .media {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
& > .media-left {
|
& > .media-left {
|
||||||
margin-top: -15px;
|
margin-top: -15px;
|
||||||
|
@ -234,37 +233,39 @@ a.card {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
margin-left: 0rem;
|
@include margin-left(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .media-content {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: inherit;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-title {
|
.event-title {
|
||||||
font-size: 1.2rem;
|
font-size: 18px;
|
||||||
line-height: 1.25rem;
|
line-height: 24px;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 3;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 2.4rem;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-end {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.event-subtitle {
|
.event-subtitle {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
display: inline-flex;
|
}
|
||||||
flex-wrap: wrap;
|
|
||||||
color: #3c376e;
|
|
||||||
|
|
||||||
span {
|
.organizer-name {
|
||||||
width: 14rem;
|
font-size: 14px;
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,66 +18,100 @@
|
||||||
</docs>
|
</docs>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span v-if="!endsOn">{{
|
<p v-if="!endsOn">
|
||||||
beginsOn | formatDateTimeString(showStartTime)
|
<span>{{
|
||||||
}}</span>
|
formatDateTimeString(beginsOn, timezoneToShow, showStartTime)
|
||||||
<span v-else-if="isSameDay() && showStartTime && showEndTime">
|
}}</span>
|
||||||
{{
|
<br />
|
||||||
|
<b-switch
|
||||||
|
size="is-small"
|
||||||
|
v-model="showLocalTimezone"
|
||||||
|
v-if="differentFromUserTimezone"
|
||||||
|
>
|
||||||
|
{{ singleTimeZone }}
|
||||||
|
</b-switch>
|
||||||
|
</p>
|
||||||
|
<p v-else-if="isSameDay() && showStartTime && showEndTime">
|
||||||
|
<span>{{
|
||||||
$t("On {date} from {startTime} to {endTime}", {
|
$t("On {date} from {startTime} to {endTime}", {
|
||||||
date: formatDate(beginsOn),
|
date: formatDate(beginsOn),
|
||||||
startTime: formatTime(beginsOn),
|
startTime: formatTime(beginsOn, timezoneToShow),
|
||||||
endTime: formatTime(endsOn),
|
endTime: formatTime(endsOn, timezoneToShow),
|
||||||
})
|
})
|
||||||
}}
|
}}</span>
|
||||||
</span>
|
<br />
|
||||||
<span v-else-if="isSameDay() && !showStartTime && showEndTime">
|
<b-switch
|
||||||
{{
|
size="is-small"
|
||||||
$t("On {date} ending at {endTime}", {
|
v-model="showLocalTimezone"
|
||||||
date: formatDate(beginsOn),
|
v-if="differentFromUserTimezone"
|
||||||
endTime: formatTime(endsOn),
|
>
|
||||||
})
|
{{ singleTimeZone }}
|
||||||
}}
|
</b-switch>
|
||||||
</span>
|
</p>
|
||||||
<span v-else-if="isSameDay() && showStartTime && !showEndTime">
|
<p v-else-if="isSameDay() && showStartTime && !showEndTime">
|
||||||
{{
|
{{
|
||||||
$t("On {date} starting at {startTime}", {
|
$t("On {date} starting at {startTime}", {
|
||||||
date: formatDate(beginsOn),
|
date: formatDate(beginsOn),
|
||||||
startTime: formatTime(beginsOn),
|
startTime: formatTime(beginsOn),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</p>
|
||||||
<span v-else-if="isSameDay()">{{
|
<p v-else-if="isSameDay()">
|
||||||
$t("On {date}", { date: formatDate(beginsOn) })
|
{{ $t("On {date}", { date: formatDate(beginsOn) }) }}
|
||||||
}}</span>
|
</p>
|
||||||
<span v-else-if="endsOn && showStartTime && showEndTime">
|
<p v-else-if="endsOn && showStartTime && showEndTime">
|
||||||
{{
|
<span>
|
||||||
$t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
|
{{
|
||||||
startDate: formatDate(beginsOn),
|
$t(
|
||||||
startTime: formatTime(beginsOn),
|
"From the {startDate} at {startTime} to the {endDate} at {endTime}",
|
||||||
endDate: formatDate(endsOn),
|
{
|
||||||
endTime: formatTime(endsOn),
|
startDate: formatDate(beginsOn),
|
||||||
})
|
startTime: formatTime(beginsOn, timezoneToShow),
|
||||||
}}
|
endDate: formatDate(endsOn),
|
||||||
</span>
|
endTime: formatTime(endsOn, timezoneToShow),
|
||||||
<span v-else-if="endsOn && showStartTime">
|
}
|
||||||
{{
|
)
|
||||||
$t("From the {startDate} at {startTime} to the {endDate}", {
|
}}
|
||||||
startDate: formatDate(beginsOn),
|
</span>
|
||||||
startTime: formatTime(beginsOn),
|
<br />
|
||||||
endDate: formatDate(endsOn),
|
<b-switch
|
||||||
})
|
size="is-small"
|
||||||
}}
|
v-model="showLocalTimezone"
|
||||||
</span>
|
v-if="differentFromUserTimezone"
|
||||||
<span v-else-if="endsOn">
|
>
|
||||||
|
{{ multipleTimeZones }}
|
||||||
|
</b-switch>
|
||||||
|
</p>
|
||||||
|
<p v-else-if="endsOn && showStartTime">
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
$t("From the {startDate} at {startTime} to the {endDate}", {
|
||||||
|
startDate: formatDate(beginsOn),
|
||||||
|
startTime: formatTime(beginsOn, timezoneToShow),
|
||||||
|
endDate: formatDate(endsOn),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<b-switch
|
||||||
|
size="is-small"
|
||||||
|
v-model="showLocalTimezone"
|
||||||
|
v-if="differentFromUserTimezone"
|
||||||
|
>
|
||||||
|
{{ singleTimeZone }}
|
||||||
|
</b-switch>
|
||||||
|
</p>
|
||||||
|
<p v-else-if="endsOn">
|
||||||
{{
|
{{
|
||||||
$t("From the {startDate} to the {endDate}", {
|
$t("From the {startDate} to the {endDate}", {
|
||||||
startDate: formatDate(beginsOn),
|
startDate: formatDate(beginsOn),
|
||||||
endDate: formatDate(endsOn),
|
endDate: formatDate(endsOn),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { getTimezoneOffset } from "date-fns-tz";
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
@ -90,21 +124,89 @@ export default class EventFullDate extends Vue {
|
||||||
|
|
||||||
@Prop({ required: false, default: true }) showEndTime!: boolean;
|
@Prop({ required: false, default: true }) showEndTime!: boolean;
|
||||||
|
|
||||||
|
@Prop({ required: false }) timezone!: string;
|
||||||
|
|
||||||
|
@Prop({ required: false }) userTimezone!: string;
|
||||||
|
|
||||||
|
showLocalTimezone = true;
|
||||||
|
|
||||||
|
get timezoneToShow(): string {
|
||||||
|
if (this.showLocalTimezone) {
|
||||||
|
return this.timezone;
|
||||||
|
}
|
||||||
|
return this.userActualTimezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
get userActualTimezone(): string {
|
||||||
|
if (this.userTimezone) {
|
||||||
|
return this.userTimezone;
|
||||||
|
}
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
}
|
||||||
|
|
||||||
formatDate(value: Date): string | undefined {
|
formatDate(value: Date): string | undefined {
|
||||||
if (!this.$options.filters) return undefined;
|
if (!this.$options.filters) return undefined;
|
||||||
return this.$options.filters.formatDateString(value);
|
return this.$options.filters.formatDateString(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
formatTime(value: Date): string | undefined {
|
formatTime(value: Date, timezone: string): string | undefined {
|
||||||
if (!this.$options.filters) return undefined;
|
if (!this.$options.filters) return undefined;
|
||||||
return this.$options.filters.formatTimeString(value);
|
return this.$options.filters.formatTimeString(value, timezone || undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDateTimeString(
|
||||||
|
value: Date,
|
||||||
|
timezone: string,
|
||||||
|
showTime: boolean
|
||||||
|
): string | undefined {
|
||||||
|
if (!this.$options.filters) return undefined;
|
||||||
|
return this.$options.filters.formatDateTimeString(
|
||||||
|
value,
|
||||||
|
timezone,
|
||||||
|
showTime
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isSameDay(): boolean {
|
isSameDay(): boolean {
|
||||||
const sameDay =
|
const sameDay =
|
||||||
new Date(this.beginsOn).toDateString() ===
|
this.beginsOnDate.toDateString() === new Date(this.endsOn).toDateString();
|
||||||
new Date(this.endsOn).toDateString();
|
|
||||||
return this.endsOn !== undefined && sameDay;
|
return this.endsOn !== undefined && sameDay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get beginsOnDate(): Date {
|
||||||
|
return new Date(this.beginsOn);
|
||||||
|
}
|
||||||
|
|
||||||
|
get differentFromUserTimezone(): boolean {
|
||||||
|
return (
|
||||||
|
!!this.timezone &&
|
||||||
|
!!this.userActualTimezone &&
|
||||||
|
getTimezoneOffset(this.timezone, this.beginsOnDate) !==
|
||||||
|
getTimezoneOffset(this.userActualTimezone, this.beginsOnDate) &&
|
||||||
|
this.timezone !== this.userActualTimezone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get singleTimeZone(): string {
|
||||||
|
if (this.showLocalTimezone) {
|
||||||
|
return this.$t("Local time ({timezone})", {
|
||||||
|
timezone: this.timezoneToShow,
|
||||||
|
}) as string;
|
||||||
|
}
|
||||||
|
return this.$t("Time in your timezone ({timezone})", {
|
||||||
|
timezone: this.timezoneToShow,
|
||||||
|
}) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
get multipleTimeZones(): string {
|
||||||
|
if (this.showLocalTimezone) {
|
||||||
|
return this.$t("Local time ({timezone})", {
|
||||||
|
timezone: this.timezoneToShow,
|
||||||
|
}) as string;
|
||||||
|
}
|
||||||
|
return this.$t("Times in your timezone ({timezone})", {
|
||||||
|
timezone: this.timezoneToShow,
|
||||||
|
}) as string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<h2 class="title">{{ event.title }}</h2>
|
<h2 class="title">{{ event.title }}</h2>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="participation-actor has-text-grey">
|
<div class="participation-actor has-text-grey-dark">
|
||||||
<span v-if="event.physicalAddress && event.physicalAddress.locality">
|
<span v-if="event.physicalAddress && event.physicalAddress.locality">
|
||||||
{{ event.physicalAddress.locality }}
|
{{ event.physicalAddress.locality }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -128,6 +128,7 @@ export default class EventListViewCard extends mixins(ActorMixin, EventMixin) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
article.box {
|
article.box {
|
||||||
div.content {
|
div.content {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
@ -148,7 +149,7 @@ article.box {
|
||||||
|
|
||||||
div.date-component {
|
div.date-component {
|
||||||
flex: 0;
|
flex: 0;
|
||||||
margin-right: 16px;
|
@include margin-right(16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|
176
js/src/components/Event/EventMap.vue
Normal file
176
js/src/components/Event/EventMap.vue
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal-card">
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<button type="button" class="delete" @click="$emit('close')" />
|
||||||
|
</header>
|
||||||
|
<div class="modal-card-body">
|
||||||
|
<section class="map">
|
||||||
|
<map-leaflet
|
||||||
|
:coords="physicalAddress.geom"
|
||||||
|
:marker="{
|
||||||
|
text: physicalAddress.fullName,
|
||||||
|
icon: physicalAddress.poiInfos.poiIcon.icon,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section class="columns is-centered map-footer">
|
||||||
|
<div class="column is-half has-text-centered">
|
||||||
|
<p class="address">
|
||||||
|
<i class="mdi mdi-map-marker"></i>
|
||||||
|
{{ physicalAddress.fullName }}
|
||||||
|
</p>
|
||||||
|
<p class="getting-there">{{ $t("Getting there") }}</p>
|
||||||
|
<div
|
||||||
|
class="buttons"
|
||||||
|
v-if="
|
||||||
|
addressLinkToRouteByCar ||
|
||||||
|
addressLinkToRouteByBike ||
|
||||||
|
addressLinkToRouteByFeet
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
target="_blank"
|
||||||
|
v-if="addressLinkToRouteByFeet"
|
||||||
|
:href="addressLinkToRouteByFeet"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-walk"></i
|
||||||
|
></a>
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
target="_blank"
|
||||||
|
v-if="addressLinkToRouteByBike"
|
||||||
|
:href="addressLinkToRouteByBike"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-bike"></i
|
||||||
|
></a>
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
target="_blank"
|
||||||
|
v-if="addressLinkToRouteByTransit"
|
||||||
|
:href="addressLinkToRouteByTransit"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-bus"></i
|
||||||
|
></a>
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
target="_blank"
|
||||||
|
v-if="addressLinkToRouteByCar"
|
||||||
|
:href="addressLinkToRouteByCar"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-car"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Address, IAddress } from "@/types/address.model";
|
||||||
|
import { RoutingTransportationType, RoutingType } from "@/types/enums";
|
||||||
|
import { PropType } from "vue";
|
||||||
|
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||||
|
|
||||||
|
const RoutingParamType = {
|
||||||
|
[RoutingType.OPENSTREETMAP]: {
|
||||||
|
[RoutingTransportationType.FOOT]: "engine=fossgis_osrm_foot",
|
||||||
|
[RoutingTransportationType.BIKE]: "engine=fossgis_osrm_bike",
|
||||||
|
[RoutingTransportationType.TRANSIT]: null,
|
||||||
|
[RoutingTransportationType.CAR]: "engine=fossgis_osrm_car",
|
||||||
|
},
|
||||||
|
[RoutingType.GOOGLE_MAPS]: {
|
||||||
|
[RoutingTransportationType.FOOT]: "dirflg=w",
|
||||||
|
[RoutingTransportationType.BIKE]: "dirflg=b",
|
||||||
|
[RoutingTransportationType.TRANSIT]: "dirflg=r",
|
||||||
|
[RoutingTransportationType.CAR]: "driving",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
"map-leaflet": () =>
|
||||||
|
import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class EventMap extends Vue {
|
||||||
|
@Prop({ type: Object as PropType<IAddress> }) address!: IAddress;
|
||||||
|
@Prop({ type: String }) routingType!: RoutingType;
|
||||||
|
|
||||||
|
get physicalAddress(): Address | null {
|
||||||
|
if (!this.address) return null;
|
||||||
|
|
||||||
|
return new Address(this.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
makeNavigationPath(
|
||||||
|
transportationType: RoutingTransportationType
|
||||||
|
): string | undefined {
|
||||||
|
const geometry = this.physicalAddress?.geom;
|
||||||
|
if (geometry) {
|
||||||
|
/**
|
||||||
|
* build urls to routing map
|
||||||
|
*/
|
||||||
|
if (!RoutingParamType[this.routingType][transportationType]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlGeometry = geometry.split(";").reverse().join(",");
|
||||||
|
|
||||||
|
switch (this.routingType) {
|
||||||
|
case RoutingType.GOOGLE_MAPS:
|
||||||
|
return `https://maps.google.com/?saddr=Current+Location&daddr=${urlGeometry}&${
|
||||||
|
RoutingParamType[this.routingType][transportationType]
|
||||||
|
}`;
|
||||||
|
case RoutingType.OPENSTREETMAP:
|
||||||
|
default: {
|
||||||
|
const bboxX = geometry.split(";").reverse()[0];
|
||||||
|
const bboxY = geometry.split(";").reverse()[1];
|
||||||
|
return `https://www.openstreetmap.org/directions?from=&to=${urlGeometry}&${
|
||||||
|
RoutingParamType[this.routingType][transportationType]
|
||||||
|
}#map=14/${bboxX}/${bboxY}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get addressLinkToRouteByCar(): undefined | string {
|
||||||
|
return this.makeNavigationPath(RoutingTransportationType.CAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
get addressLinkToRouteByBike(): undefined | string {
|
||||||
|
return this.makeNavigationPath(RoutingTransportationType.BIKE);
|
||||||
|
}
|
||||||
|
|
||||||
|
get addressLinkToRouteByFeet(): undefined | string {
|
||||||
|
return this.makeNavigationPath(RoutingTransportationType.FOOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
get addressLinkToRouteByTransit(): undefined | string {
|
||||||
|
return this.makeNavigationPath(RoutingTransportationType.TRANSIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
|
.modal-card-head {
|
||||||
|
justify-content: flex-end;
|
||||||
|
button.delete {
|
||||||
|
@include margin-right(1rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.map {
|
||||||
|
height: calc(100% - 8rem);
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
section.map-footer {
|
||||||
|
p.address {
|
||||||
|
margin: 1rem auto;
|
||||||
|
}
|
||||||
|
div.buttons {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -14,9 +14,9 @@
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<b-icon v-else-if="icon" :icon="icon" size="is-medium" />
|
<b-icon v-else-if="icon" :icon="icon" size="is-medium" />
|
||||||
<p :class="{ 'padding-left': icon }">
|
<div class="content-wrapper" :class="{ 'padding-left': icon }">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -42,8 +42,10 @@ div.eventMetadataBlock {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 1.75rem;
|
margin-bottom: 1.75rem;
|
||||||
|
|
||||||
p {
|
.content-wrapper {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
max-width: calc(100vw - 32px - 20px);
|
||||||
|
|
||||||
&.padding-left {
|
&.padding-left {
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
:src="`/img/${metadataItem.icon.substring(8)}_monochrome.svg`"
|
:src="`/img/${metadataItem.icon.substring(8)}_monochrome.svg`"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
|
alt=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<b-icon v-else-if="metadataItem.icon" :icon="metadataItem.icon" />
|
<b-icon v-else-if="metadataItem.icon" :icon="metadataItem.icon" />
|
||||||
|
@ -130,11 +131,12 @@ export default class EventMetadataItem extends Vue {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
.card .media {
|
.card .media {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
& > button {
|
& > button {
|
||||||
margin-left: 1rem;
|
@include margin-left(1rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -9,9 +9,14 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<b-field grouped :label="$t('Find or add an element')">
|
<b-field
|
||||||
|
grouped
|
||||||
|
:label="$t('Find or add an element')"
|
||||||
|
label-for="event-metadata-autocomplete"
|
||||||
|
>
|
||||||
<b-autocomplete
|
<b-autocomplete
|
||||||
expanded
|
expanded
|
||||||
|
:clear-on-select="true"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
ref="autocomplete"
|
ref="autocomplete"
|
||||||
:data="filteredDataArray"
|
:data="filteredDataArray"
|
||||||
|
@ -19,7 +24,9 @@
|
||||||
group-options="items"
|
group-options="items"
|
||||||
open-on-focus
|
open-on-focus
|
||||||
:placeholder="$t('e.g. Accessibility, Twitch, PeerTube')"
|
:placeholder="$t('e.g. Accessibility, Twitch, PeerTube')"
|
||||||
|
id="event-metadata-autocomplete"
|
||||||
@select="(option) => addElement(option)"
|
@select="(option) => addElement(option)"
|
||||||
|
dir="auto"
|
||||||
>
|
>
|
||||||
<template slot-scope="props">
|
<template slot-scope="props">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
|
@ -32,6 +39,7 @@
|
||||||
:src="`/img/${props.option.icon.substring(8)}_monochrome.svg`"
|
:src="`/img/${props.option.icon.substring(8)}_monochrome.svg`"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
|
alt=""
|
||||||
/>
|
/>
|
||||||
<b-icon v-else-if="props.option.icon" :icon="props.option.icon" />
|
<b-icon v-else-if="props.option.icon" :icon="props.option.icon" />
|
||||||
<b-icon v-else icon="help-circle" />
|
<b-icon v-else icon="help-circle" />
|
||||||
|
@ -55,7 +63,11 @@
|
||||||
</b-button>
|
</b-button>
|
||||||
</p>
|
</p>
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-modal has-modal-card v-model="showNewElementModal">
|
<b-modal
|
||||||
|
has-modal-card
|
||||||
|
v-model="showNewElementModal"
|
||||||
|
:close-button-aria-label="$t('Close')"
|
||||||
|
>
|
||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
<header class="modal-card-head">
|
<header class="modal-card-head">
|
||||||
<button
|
<button
|
||||||
|
@ -129,7 +141,10 @@ export default class EventMetadataList extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
set metadata(metadata: IEventMetadata[]) {
|
set metadata(metadata: IEventMetadata[]) {
|
||||||
this.$emit("input", metadata);
|
this.$emit(
|
||||||
|
"input",
|
||||||
|
metadata.filter((elem) => elem)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
localizedCategories: Record<EventMetadataCategories, string> = {
|
localizedCategories: Record<EventMetadataCategories, string> = {
|
||||||
|
|
|
@ -1,31 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<event-metadata-block
|
<event-metadata-block
|
||||||
|
v-if="!event.options.isOnline"
|
||||||
:title="$t('Location')"
|
:title="$t('Location')"
|
||||||
:icon="physicalAddress ? physicalAddress.poiInfos.poiIcon.icon : 'earth'"
|
:icon="physicalAddress ? physicalAddress.poiInfos.poiIcon.icon : 'earth'"
|
||||||
>
|
>
|
||||||
<div class="address-wrapper">
|
<div class="address-wrapper">
|
||||||
<span v-if="!physicalAddress">{{ $t("No address defined") }}</span>
|
<span v-if="!physicalAddress">{{ $t("No address defined") }}</span>
|
||||||
<div class="address" v-if="physicalAddress">
|
<div class="address" v-if="physicalAddress">
|
||||||
<div>
|
<address-info :address="physicalAddress" />
|
||||||
<address>
|
<b-button
|
||||||
<p
|
type="is-text"
|
||||||
class="addressDescription"
|
|
||||||
:title="physicalAddress.poiInfos.name"
|
|
||||||
>
|
|
||||||
{{ physicalAddress.poiInfos.name }}
|
|
||||||
</p>
|
|
||||||
<p class="has-text-grey-dark">
|
|
||||||
{{ physicalAddress.poiInfos.alternativeName }}
|
|
||||||
</p>
|
|
||||||
</address>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="map-show-button"
|
class="map-show-button"
|
||||||
@click="showMap = !showMap"
|
@click="$emit('showMapModal', true)"
|
||||||
v-if="physicalAddress.geom"
|
v-if="physicalAddress.geom"
|
||||||
>{{ $t("Show map") }}</span
|
|
||||||
>
|
>
|
||||||
|
{{ $t("Show map") }}
|
||||||
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</event-metadata-block>
|
</event-metadata-block>
|
||||||
|
@ -34,6 +25,8 @@
|
||||||
:beginsOn="event.beginsOn"
|
:beginsOn="event.beginsOn"
|
||||||
:show-start-time="event.options.showStartTime"
|
:show-start-time="event.options.showStartTime"
|
||||||
:show-end-time="event.options.showEndTime"
|
:show-end-time="event.options.showEndTime"
|
||||||
|
:timezone="event.options.timezone"
|
||||||
|
:userTimezone="userTimezone"
|
||||||
:endsOn="event.endsOn"
|
:endsOn="event.endsOn"
|
||||||
/>
|
/>
|
||||||
</event-metadata-block>
|
</event-metadata-block>
|
||||||
|
@ -41,14 +34,9 @@
|
||||||
class="metadata-organized-by"
|
class="metadata-organized-by"
|
||||||
:title="$t('Organized by')"
|
:title="$t('Organized by')"
|
||||||
>
|
>
|
||||||
<popover-actor-card
|
|
||||||
:actor="event.organizerActor"
|
|
||||||
v-if="!event.attributedTo"
|
|
||||||
>
|
|
||||||
<actor-card :actor="event.organizerActor" />
|
|
||||||
</popover-actor-card>
|
|
||||||
<router-link
|
<router-link
|
||||||
v-if="event.attributedTo"
|
v-if="event.attributedTo"
|
||||||
|
class="hover:underline"
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.GROUP,
|
name: RouteName.GROUP,
|
||||||
params: {
|
params: {
|
||||||
|
@ -56,23 +44,21 @@
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<popover-actor-card
|
<actor-card
|
||||||
:actor="event.attributedTo"
|
|
||||||
v-if="
|
v-if="
|
||||||
!event.attributedTo || !event.options.hideOrganizerWhenGroupEvent
|
!event.attributedTo || !event.options.hideOrganizerWhenGroupEvent
|
||||||
"
|
"
|
||||||
>
|
:actor="event.attributedTo"
|
||||||
<actor-card :actor="event.attributedTo" />
|
:inline="true"
|
||||||
</popover-actor-card>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<actor-card v-else :actor="event.organizerActor" :inline="true" />
|
||||||
<popover-actor-card
|
<actor-card
|
||||||
|
:inline="true"
|
||||||
:actor="contact"
|
:actor="contact"
|
||||||
v-for="contact in event.contacts"
|
v-for="contact in event.contacts"
|
||||||
:key="contact.id"
|
:key="contact.id"
|
||||||
>
|
/>
|
||||||
<actor-card :actor="contact" />
|
|
||||||
</popover-actor-card>
|
|
||||||
</event-metadata-block>
|
</event-metadata-block>
|
||||||
<event-metadata-block
|
<event-metadata-block
|
||||||
v-if="event.onlineAddress && urlToHostname(event.onlineAddress)"
|
v-if="event.onlineAddress && urlToHostname(event.onlineAddress)"
|
||||||
|
@ -81,6 +67,7 @@
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
class="hover:underline"
|
||||||
rel="noopener noreferrer ugc"
|
rel="noopener noreferrer ugc"
|
||||||
:href="event.onlineAddress"
|
:href="event.onlineAddress"
|
||||||
:title="
|
:title="
|
||||||
|
@ -140,91 +127,12 @@
|
||||||
>
|
>
|
||||||
<span v-else>{{ extra.value }}</span>
|
<span v-else>{{ extra.value }}</span>
|
||||||
</event-metadata-block>
|
</event-metadata-block>
|
||||||
<b-modal
|
|
||||||
class="map-modal"
|
|
||||||
v-if="physicalAddress && physicalAddress.geom"
|
|
||||||
:active.sync="showMap"
|
|
||||||
has-modal-card
|
|
||||||
full-screen
|
|
||||||
>
|
|
||||||
<div class="modal-card">
|
|
||||||
<header class="modal-card-head">
|
|
||||||
<button type="button" class="delete" @click="showMap = false" />
|
|
||||||
</header>
|
|
||||||
<div class="modal-card-body">
|
|
||||||
<section class="map">
|
|
||||||
<map-leaflet
|
|
||||||
:coords="physicalAddress.geom"
|
|
||||||
:marker="{
|
|
||||||
text: physicalAddress.fullName,
|
|
||||||
icon: physicalAddress.poiInfos.poiIcon.icon,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<section class="columns is-centered map-footer">
|
|
||||||
<div class="column is-half has-text-centered">
|
|
||||||
<p class="address">
|
|
||||||
<i class="mdi mdi-map-marker"></i>
|
|
||||||
{{ physicalAddress.fullName }}
|
|
||||||
</p>
|
|
||||||
<p class="getting-there">{{ $t("Getting there") }}</p>
|
|
||||||
<div
|
|
||||||
class="buttons"
|
|
||||||
v-if="
|
|
||||||
addressLinkToRouteByCar ||
|
|
||||||
addressLinkToRouteByBike ||
|
|
||||||
addressLinkToRouteByFeet
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="button"
|
|
||||||
target="_blank"
|
|
||||||
v-if="addressLinkToRouteByFeet"
|
|
||||||
:href="addressLinkToRouteByFeet"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-walk"></i
|
|
||||||
></a>
|
|
||||||
<a
|
|
||||||
class="button"
|
|
||||||
target="_blank"
|
|
||||||
v-if="addressLinkToRouteByBike"
|
|
||||||
:href="addressLinkToRouteByBike"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-bike"></i
|
|
||||||
></a>
|
|
||||||
<a
|
|
||||||
class="button"
|
|
||||||
target="_blank"
|
|
||||||
v-if="addressLinkToRouteByTransit"
|
|
||||||
:href="addressLinkToRouteByTransit"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-bus"></i
|
|
||||||
></a>
|
|
||||||
<a
|
|
||||||
class="button"
|
|
||||||
target="_blank"
|
|
||||||
v-if="addressLinkToRouteByCar"
|
|
||||||
:href="addressLinkToRouteByCar"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-car"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</b-modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Address } from "@/types/address.model";
|
import { Address } from "@/types/address.model";
|
||||||
import { IConfig } from "@/types/config.model";
|
import { IConfig } from "@/types/config.model";
|
||||||
import {
|
import { EventMetadataKeyType, EventMetadataType } from "@/types/enums";
|
||||||
EventMetadataKeyType,
|
|
||||||
EventMetadataType,
|
|
||||||
RoutingTransportationType,
|
|
||||||
RoutingType,
|
|
||||||
} from "@/types/enums";
|
|
||||||
import { IEvent } from "@/types/event.model";
|
import { IEvent } from "@/types/event.model";
|
||||||
import { PropType } from "vue";
|
import { PropType } from "vue";
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
@ -234,11 +142,13 @@ import EventMetadataBlock from "./EventMetadataBlock.vue";
|
||||||
import EventFullDate from "./EventFullDate.vue";
|
import EventFullDate from "./EventFullDate.vue";
|
||||||
import PopoverActorCard from "../Account/PopoverActorCard.vue";
|
import PopoverActorCard from "../Account/PopoverActorCard.vue";
|
||||||
import ActorCard from "../../components/Account/ActorCard.vue";
|
import ActorCard from "../../components/Account/ActorCard.vue";
|
||||||
|
import AddressInfo from "../../components/Address/AddressInfo.vue";
|
||||||
import {
|
import {
|
||||||
IEventMetadata,
|
IEventMetadata,
|
||||||
IEventMetadataDescription,
|
IEventMetadataDescription,
|
||||||
} from "@/types/event-metadata";
|
} from "@/types/event-metadata";
|
||||||
import { eventMetaDataList } from "../../services/EventMetadata";
|
import { eventMetaDataList } from "../../services/EventMetadata";
|
||||||
|
import { IUser } from "@/types/current-user.model";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
@ -246,15 +156,14 @@ import { eventMetaDataList } from "../../services/EventMetadata";
|
||||||
EventFullDate,
|
EventFullDate,
|
||||||
PopoverActorCard,
|
PopoverActorCard,
|
||||||
ActorCard,
|
ActorCard,
|
||||||
"map-leaflet": () =>
|
AddressInfo,
|
||||||
import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class EventMetadataSidebar extends Vue {
|
export default class EventMetadataSidebar extends Vue {
|
||||||
@Prop({ type: Object as PropType<IEvent>, required: true }) event!: IEvent;
|
@Prop({ type: Object as PropType<IEvent>, required: true }) event!: IEvent;
|
||||||
@Prop({ type: Object as PropType<IConfig>, required: true }) config!: IConfig;
|
@Prop({ type: Object as PropType<IConfig>, required: true }) config!: IConfig;
|
||||||
|
@Prop({ required: true }) user!: IUser | undefined;
|
||||||
showMap = false;
|
@Prop({ required: false, default: false }) showMap!: boolean;
|
||||||
|
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
|
|
||||||
|
@ -265,21 +174,6 @@ export default class EventMetadataSidebar extends Vue {
|
||||||
EventMetadataType = EventMetadataType;
|
EventMetadataType = EventMetadataType;
|
||||||
EventMetadataKeyType = EventMetadataKeyType;
|
EventMetadataKeyType = EventMetadataKeyType;
|
||||||
|
|
||||||
RoutingParamType = {
|
|
||||||
[RoutingType.OPENSTREETMAP]: {
|
|
||||||
[RoutingTransportationType.FOOT]: "engine=fossgis_osrm_foot",
|
|
||||||
[RoutingTransportationType.BIKE]: "engine=fossgis_osrm_bike",
|
|
||||||
[RoutingTransportationType.TRANSIT]: null,
|
|
||||||
[RoutingTransportationType.CAR]: "engine=fossgis_osrm_car",
|
|
||||||
},
|
|
||||||
[RoutingType.GOOGLE_MAPS]: {
|
|
||||||
[RoutingTransportationType.FOOT]: "dirflg=w",
|
|
||||||
[RoutingTransportationType.BIKE]: "dirflg=b",
|
|
||||||
[RoutingTransportationType.TRANSIT]: "dirflg=r",
|
|
||||||
[RoutingTransportationType.CAR]: "driving",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
get physicalAddress(): Address | null {
|
get physicalAddress(): Address | null {
|
||||||
if (!this.event.physicalAddress) return null;
|
if (!this.event.physicalAddress) return null;
|
||||||
|
|
||||||
|
@ -296,50 +190,6 @@ export default class EventMetadataSidebar extends Vue {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
makeNavigationPath(
|
|
||||||
transportationType: RoutingTransportationType
|
|
||||||
): string | undefined {
|
|
||||||
const geometry = this.physicalAddress?.geom;
|
|
||||||
if (geometry) {
|
|
||||||
const routingType = this.config.maps.routing.type;
|
|
||||||
/**
|
|
||||||
* build urls to routing map
|
|
||||||
*/
|
|
||||||
if (!this.RoutingParamType[routingType][transportationType]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlGeometry = geometry.split(";").reverse().join(",");
|
|
||||||
|
|
||||||
switch (routingType) {
|
|
||||||
case RoutingType.GOOGLE_MAPS:
|
|
||||||
return `https://maps.google.com/?saddr=Current+Location&daddr=${urlGeometry}&${this.RoutingParamType[routingType][transportationType]}`;
|
|
||||||
case RoutingType.OPENSTREETMAP:
|
|
||||||
default: {
|
|
||||||
const bboxX = geometry.split(";").reverse()[0];
|
|
||||||
const bboxY = geometry.split(";").reverse()[1];
|
|
||||||
return `https://www.openstreetmap.org/directions?from=&to=${urlGeometry}&${this.RoutingParamType[routingType][transportationType]}#map=14/${bboxX}/${bboxY}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get addressLinkToRouteByCar(): undefined | string {
|
|
||||||
return this.makeNavigationPath(RoutingTransportationType.CAR);
|
|
||||||
}
|
|
||||||
|
|
||||||
get addressLinkToRouteByBike(): undefined | string {
|
|
||||||
return this.makeNavigationPath(RoutingTransportationType.BIKE);
|
|
||||||
}
|
|
||||||
|
|
||||||
get addressLinkToRouteByFeet(): undefined | string {
|
|
||||||
return this.makeNavigationPath(RoutingTransportationType.FOOT);
|
|
||||||
}
|
|
||||||
|
|
||||||
get addressLinkToRouteByTransit(): undefined | string {
|
|
||||||
return this.makeNavigationPath(RoutingTransportationType.TRANSIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
urlToHostname(url: string): string | null {
|
urlToHostname(url: string): string | null {
|
||||||
try {
|
try {
|
||||||
return new URL(url).hostname;
|
return new URL(url).hostname;
|
||||||
|
@ -372,6 +222,10 @@ export default class EventMetadataSidebar extends Vue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get userTimezone(): string | undefined {
|
||||||
|
return this.user?.settings?.timezone;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -401,50 +255,6 @@ div.address-wrapper {
|
||||||
.map-show-button {
|
.map-show-button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
address {
|
|
||||||
font-style: normal;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
span.addressDescription {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
min-width: 100%;
|
|
||||||
max-width: 4rem;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(.addressDescription) {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.map-modal {
|
|
||||||
.modal-card-head {
|
|
||||||
justify-content: flex-end;
|
|
||||||
button.delete {
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section.map {
|
|
||||||
height: calc(100% - 8rem);
|
|
||||||
width: calc(100% - 20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
section.map-footer {
|
|
||||||
p.address {
|
|
||||||
margin: 1rem auto;
|
|
||||||
}
|
|
||||||
div.buttons {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,19 +1,72 @@
|
||||||
<template>
|
<template>
|
||||||
<router-link
|
<router-link
|
||||||
class="event-minimalist-card-wrapper"
|
class="event-minimalist-card-wrapper bg-white rounded-lg shadow-md"
|
||||||
|
dir="auto"
|
||||||
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
||||||
>
|
>
|
||||||
<date-calendar-icon
|
<div class="event-preview mr-0 ml-0">
|
||||||
class="calendar-icon"
|
<div>
|
||||||
:date="event.beginsOn"
|
<div class="date-component">
|
||||||
:small="true"
|
<date-calendar-icon :date="event.beginsOn" :small="true" />
|
||||||
/>
|
</div>
|
||||||
<div class="title-info-wrapper">
|
<lazy-image-wrapper
|
||||||
<p class="event-minimalist-title">{{ event.title }}</p>
|
:picture="event.picture"
|
||||||
<p v-if="event.physicalAddress" class="has-text-grey">
|
:rounded="true"
|
||||||
{{ event.physicalAddress.description }}
|
style="height: 100%; position: absolute; top: 0; left: 0; width: 100%"
|
||||||
</p>
|
/>
|
||||||
<p v-else>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="title-info-wrapper has-text-grey-dark">
|
||||||
|
<h3 class="event-minimalist-title" :lang="event.language" dir="auto">
|
||||||
|
<b-tag
|
||||||
|
type="is-info"
|
||||||
|
class="mr-1"
|
||||||
|
v-if="event.status === EventStatus.TENTATIVE"
|
||||||
|
>
|
||||||
|
{{ $t("Tentative") }}
|
||||||
|
</b-tag>
|
||||||
|
<b-tag
|
||||||
|
type="is-danger"
|
||||||
|
class="mr-1"
|
||||||
|
v-if="event.status === EventStatus.CANCELLED"
|
||||||
|
>
|
||||||
|
{{ $t("Cancelled") }}
|
||||||
|
</b-tag>
|
||||||
|
<b-tag
|
||||||
|
class="mr-2"
|
||||||
|
type="is-warning"
|
||||||
|
size="is-medium"
|
||||||
|
v-if="event.draft"
|
||||||
|
>{{ $t("Draft") }}</b-tag
|
||||||
|
>
|
||||||
|
{{ event.title }}
|
||||||
|
</h3>
|
||||||
|
<inline-address
|
||||||
|
v-if="event.physicalAddress"
|
||||||
|
class="event-subtitle"
|
||||||
|
:physical-address="event.physicalAddress"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="event-subtitle"
|
||||||
|
v-else-if="event.options && event.options.isOnline"
|
||||||
|
>
|
||||||
|
<b-icon icon="video" />
|
||||||
|
<span>{{ $t("Online") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="event-subtitle event-organizer" v-if="showOrganizer">
|
||||||
|
<figure
|
||||||
|
class="image is-24x24"
|
||||||
|
v-if="organizer(event) && organizer(event).avatar"
|
||||||
|
>
|
||||||
|
<img class="is-rounded" :src="organizer(event).avatar.url" alt="" />
|
||||||
|
</figure>
|
||||||
|
<b-icon v-else icon="account-circle" />
|
||||||
|
<span class="organizer-name">
|
||||||
|
{{ organizerDisplayName(event) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="participant-metadata">
|
||||||
|
<b-icon icon="account-multiple" />
|
||||||
<span v-if="event.options.maximumAttendeeCapacity !== 0">
|
<span v-if="event.options.maximumAttendeeCapacity !== 0">
|
||||||
{{
|
{{
|
||||||
$tc(
|
$tc(
|
||||||
|
@ -64,44 +117,90 @@
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import { IEvent } from "@/types/event.model";
|
import { IEvent, organizer, organizerDisplayName } from "@/types/event.model";
|
||||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||||
import { ParticipantRole } from "@/types/enums";
|
import { EventStatus, ParticipantRole } from "@/types/enums";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
|
||||||
|
import InlineAddress from "@/components/Address/InlineAddress.vue";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
DateCalendarIcon,
|
DateCalendarIcon,
|
||||||
|
LazyImageWrapper,
|
||||||
|
InlineAddress,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class EventMinimalistCard extends Vue {
|
export default class EventMinimalistCard extends Vue {
|
||||||
@Prop({ required: true, type: Object }) event!: IEvent;
|
@Prop({ required: true, type: Object }) event!: IEvent;
|
||||||
|
@Prop({ required: false, type: Boolean, default: false })
|
||||||
|
showOrganizer!: boolean;
|
||||||
|
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
|
|
||||||
ParticipantRole = ParticipantRole;
|
ParticipantRole = ParticipantRole;
|
||||||
|
|
||||||
|
organizerDisplayName = organizerDisplayName;
|
||||||
|
|
||||||
|
organizer = organizer;
|
||||||
|
|
||||||
|
EventStatus = EventStatus;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
|
@use "@/styles/_event-card";
|
||||||
|
@import "~bulma/sass/utilities/mixins.sass";
|
||||||
|
@import "@/variables.scss";
|
||||||
|
|
||||||
.event-minimalist-card-wrapper {
|
.event-minimalist-card-wrapper {
|
||||||
display: flex;
|
display: grid;
|
||||||
width: 100%;
|
grid-gap: 5px 10px;
|
||||||
|
grid-template-areas: "preview" "body";
|
||||||
color: initial;
|
color: initial;
|
||||||
align-items: flex-start;
|
|
||||||
|
@include desktop {
|
||||||
|
grid-template-columns: 200px 3fr;
|
||||||
|
grid-template-areas: "preview body";
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-preview {
|
||||||
|
& > div {
|
||||||
|
position: relative;
|
||||||
|
height: 120px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
div.date-component {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 5px;
|
||||||
|
left: 5px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.calendar-icon {
|
.calendar-icon {
|
||||||
margin-right: 1rem;
|
@include margin-right(1rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-info-wrapper {
|
.title-info-wrapper {
|
||||||
flex: 2;
|
flex: 2;
|
||||||
|
|
||||||
.event-minimalist-title {
|
.event-minimalist-title {
|
||||||
color: #3c376e;
|
padding-bottom: 5px;
|
||||||
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
|
font-size: 18px;
|
||||||
serif;
|
line-height: 24px;
|
||||||
font-size: 1.25rem;
|
display: -webkit-box;
|
||||||
font-weight: 700;
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: bold;
|
||||||
|
color: $title-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .icon {
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<article class="box">
|
<article class="box mb-5 mt-4">
|
||||||
<div class="identity-header">
|
<div class="identity-header" dir="auto">
|
||||||
<figure class="image is-24x24" v-if="participation.actor.avatar">
|
<figure class="image is-24x24" v-if="participation.actor.avatar">
|
||||||
<img
|
<img
|
||||||
class="is-rounded"
|
class="is-rounded"
|
||||||
|
@ -10,80 +10,123 @@
|
||||||
width="24"
|
width="24"
|
||||||
/>
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
|
<b-icon v-else icon="account-circle" />
|
||||||
{{ displayNameAndUsername(participation.actor) }}
|
{{ displayNameAndUsername(participation.actor) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="list-card">
|
<div class="list-card">
|
||||||
<div class="date-component">
|
|
||||||
<date-calendar-icon
|
|
||||||
:date="participation.event.beginsOn"
|
|
||||||
:small="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="content-and-actions">
|
<div class="content-and-actions">
|
||||||
<div class="list-card-content">
|
<div class="event-preview mr-0 ml-0">
|
||||||
<div class="title-wrapper">
|
<div>
|
||||||
|
<div class="date-component">
|
||||||
|
<date-calendar-icon
|
||||||
|
:date="participation.event.beginsOn"
|
||||||
|
:small="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.EVENT,
|
name: RouteName.EVENT,
|
||||||
params: { uuid: participation.event.uuid },
|
params: { uuid: participation.event.uuid },
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<h3 class="title">{{ participation.event.title }}</h3>
|
<lazy-image-wrapper
|
||||||
|
:rounded="true"
|
||||||
|
:picture="participation.event.picture"
|
||||||
|
style="
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
"
|
||||||
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="participation-actor">
|
</div>
|
||||||
<span>
|
<div class="list-card-content">
|
||||||
<b-icon
|
<div class="title-wrapper" dir="auto">
|
||||||
icon="earth"
|
<b-tag
|
||||||
v-if="participation.event.visibility === EventVisibility.PUBLIC"
|
type="is-info"
|
||||||
/>
|
class="mr-1 mb-1"
|
||||||
<b-icon
|
size="is-medium"
|
||||||
icon="link"
|
v-if="participation.event.status === EventStatus.TENTATIVE"
|
||||||
v-else-if="
|
|
||||||
participation.event.visibility === EventVisibility.UNLISTED
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<b-icon
|
|
||||||
icon="lock"
|
|
||||||
v-else-if="
|
|
||||||
participation.event.visibility === EventVisibility.PRIVATE
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="
|
|
||||||
participation.event.physicalAddress &&
|
|
||||||
participation.event.physicalAddress.locality
|
|
||||||
"
|
|
||||||
>{{ participation.event.physicalAddress.locality }} -</span
|
|
||||||
>
|
>
|
||||||
<i18n
|
{{ $t("Tentative") }}
|
||||||
tag="span"
|
</b-tag>
|
||||||
path="Organized by {name}"
|
<b-tag
|
||||||
v-if="organizerActor.id !== currentActor.id"
|
type="is-danger"
|
||||||
|
class="mr-1 mb-1"
|
||||||
|
size="is-medium"
|
||||||
|
v-if="participation.event.status === EventStatus.CANCELLED"
|
||||||
>
|
>
|
||||||
<popover-actor-card
|
{{ $t("Cancelled") }}
|
||||||
slot="name"
|
</b-tag>
|
||||||
:actor="organizerActor"
|
<router-link
|
||||||
:inline="true"
|
:to="{
|
||||||
>
|
name: RouteName.EVENT,
|
||||||
{{ organizerActor.displayName() }}
|
params: { uuid: participation.event.uuid },
|
||||||
</popover-actor-card>
|
}"
|
||||||
</i18n>
|
>
|
||||||
<span v-else>{{ $t("Organized by you") }}</span>
|
<h3 class="title" :lang="participation.event.language">
|
||||||
|
{{ participation.event.title }}
|
||||||
|
</h3>
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<inline-address
|
||||||
|
v-if="participation.event.physicalAddress"
|
||||||
|
class="event-subtitle"
|
||||||
|
:physical-address="participation.event.physicalAddress"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="event-subtitle"
|
||||||
|
v-else-if="
|
||||||
|
participation.event.options &&
|
||||||
|
participation.event.options.isOnline
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<b-icon icon="video" />
|
||||||
|
<span>{{ $t("Online") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="event-subtitle event-organizer">
|
||||||
|
<figure
|
||||||
|
class="image is-24x24"
|
||||||
|
v-if="
|
||||||
|
organizer(participation.event) &&
|
||||||
|
organizer(participation.event).avatar
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="is-rounded"
|
||||||
|
:src="organizer(participation.event).avatar.url"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
<b-icon v-else icon="account-circle" />
|
||||||
|
<span class="organizer-name">
|
||||||
|
{{ organizerDisplayName(participation.event) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="event-subtitle event-participants">
|
||||||
|
<b-icon
|
||||||
|
:class="{ 'has-text-danger': lastSeatsLeft }"
|
||||||
|
icon="account-group"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
class="participant-stats"
|
class="participant-stats"
|
||||||
v-if="
|
v-if="participation.role !== ParticipantRole.NOT_APPROVED"
|
||||||
![
|
|
||||||
ParticipantRole.PARTICIPANT,
|
|
||||||
ParticipantRole.NOT_APPROVED,
|
|
||||||
].includes(participation.role)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
|
<!-- Less than 10 seats left -->
|
||||||
|
<span class="has-text-danger" v-if="lastSeatsLeft">
|
||||||
|
{{
|
||||||
|
$t("{number} seats left", {
|
||||||
|
number: seatsLeft,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="participation.event.options.maximumAttendeeCapacity !== 0"
|
v-else-if="
|
||||||
|
participation.event.options.maximumAttendeeCapacity !== 0
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
$tc(
|
$tc(
|
||||||
|
@ -111,28 +154,27 @@
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="participation.event.participantStats.notApproved > 0">
|
<b-button
|
||||||
<b-button
|
v-if="participation.event.participantStats.notApproved > 0"
|
||||||
type="is-text"
|
type="is-text"
|
||||||
@click="
|
@click="
|
||||||
gotToWithCheck(participation, {
|
gotToWithCheck(participation, {
|
||||||
name: RouteName.PARTICIPATIONS,
|
name: RouteName.PARTICIPATIONS,
|
||||||
query: { role: ParticipantRole.NOT_APPROVED },
|
query: { role: ParticipantRole.NOT_APPROVED },
|
||||||
params: { eventId: participation.event.uuid },
|
params: { eventId: participation.event.uuid },
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
$tc(
|
$tc(
|
||||||
"{count} requests waiting",
|
"{count} requests waiting",
|
||||||
participation.event.participantStats.notApproved,
|
participation.event.participantStats.notApproved,
|
||||||
{
|
{
|
||||||
count: participation.event.participantStats.notApproved,
|
count: participation.event.participantStats.notApproved,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</b-button>
|
</b-button>
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -231,9 +273,13 @@ import { Component, Prop } from "vue-property-decorator";
|
||||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||||
import { mixins } from "vue-class-component";
|
import { mixins } from "vue-class-component";
|
||||||
import { RawLocation, Route } from "vue-router";
|
import { RawLocation, Route } from "vue-router";
|
||||||
import { EventVisibility, ParticipantRole } from "@/types/enums";
|
import { EventStatus, EventVisibility, ParticipantRole } from "@/types/enums";
|
||||||
import { IParticipant } from "../../types/participant.model";
|
import { IParticipant } from "../../types/participant.model";
|
||||||
import { IEventCardOptions } from "../../types/event.model";
|
import {
|
||||||
|
IEventCardOptions,
|
||||||
|
organizer,
|
||||||
|
organizerDisplayName,
|
||||||
|
} from "../../types/event.model";
|
||||||
import { displayNameAndUsername, IActor, IPerson } from "../../types/actor";
|
import { displayNameAndUsername, IActor, IPerson } from "../../types/actor";
|
||||||
import ActorMixin from "../../mixins/actor";
|
import ActorMixin from "../../mixins/actor";
|
||||||
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
||||||
|
@ -241,6 +287,9 @@ import EventMixin from "../../mixins/event";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
import { changeIdentity } from "../../utils/auth";
|
import { changeIdentity } from "../../utils/auth";
|
||||||
import PopoverActorCard from "../Account/PopoverActorCard.vue";
|
import PopoverActorCard from "../Account/PopoverActorCard.vue";
|
||||||
|
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
|
||||||
|
import InlineAddress from "@/components/Address/InlineAddress.vue";
|
||||||
|
import { PropType } from "vue";
|
||||||
|
|
||||||
const defaultOptions: IEventCardOptions = {
|
const defaultOptions: IEventCardOptions = {
|
||||||
hideDate: true,
|
hideDate: true,
|
||||||
|
@ -254,6 +303,8 @@ const defaultOptions: IEventCardOptions = {
|
||||||
components: {
|
components: {
|
||||||
DateCalendarIcon,
|
DateCalendarIcon,
|
||||||
PopoverActorCard,
|
PopoverActorCard,
|
||||||
|
LazyImageWrapper,
|
||||||
|
InlineAddress,
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
currentActor: {
|
currentActor: {
|
||||||
|
@ -261,11 +312,15 @@ const defaultOptions: IEventCardOptions = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class EventListCard extends mixins(ActorMixin, EventMixin) {
|
export default class EventParticipationCard extends mixins(
|
||||||
|
ActorMixin,
|
||||||
|
EventMixin
|
||||||
|
) {
|
||||||
/**
|
/**
|
||||||
* The participation associated
|
* The participation associated
|
||||||
*/
|
*/
|
||||||
@Prop({ required: true }) participation!: IParticipant;
|
@Prop({ required: true, type: Object as PropType<IParticipant> })
|
||||||
|
participation!: IParticipant;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options are merged with default options
|
* Options are merged with default options
|
||||||
|
@ -281,8 +336,14 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
|
||||||
|
|
||||||
displayNameAndUsername = displayNameAndUsername;
|
displayNameAndUsername = displayNameAndUsername;
|
||||||
|
|
||||||
|
organizerDisplayName = organizerDisplayName;
|
||||||
|
|
||||||
|
organizer = organizer;
|
||||||
|
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
|
|
||||||
|
EventStatus = EventStatus;
|
||||||
|
|
||||||
get mergedOptions(): IEventCardOptions {
|
get mergedOptions(): IEventCardOptions {
|
||||||
return { ...defaultOptions, ...this.options };
|
return { ...defaultOptions, ...this.options };
|
||||||
}
|
}
|
||||||
|
@ -304,13 +365,13 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
|
||||||
participation.actor.id !== this.currentActor.id &&
|
participation.actor.id !== this.currentActor.id &&
|
||||||
participation.event.organizerActor
|
participation.event.organizerActor
|
||||||
) {
|
) {
|
||||||
const organizer = participation.event.organizerActor as IPerson;
|
const organizerActor = participation.event.organizerActor as IPerson;
|
||||||
await changeIdentity(this.$apollo.provider.defaultClient, organizer);
|
await changeIdentity(this.$apollo.provider.defaultClient, organizerActor);
|
||||||
this.$buefy.notification.open({
|
this.$buefy.notification.open({
|
||||||
message: this.$t(
|
message: this.$t(
|
||||||
"Current identity has been changed to {identityName} in order to manage this event.",
|
"Current identity has been changed to {identityName} in order to manage this event.",
|
||||||
{
|
{
|
||||||
identityName: organizer.preferredUsername,
|
identityName: organizerActor.preferredUsername,
|
||||||
}
|
}
|
||||||
) as string,
|
) as string,
|
||||||
type: "is-info",
|
type: "is-info",
|
||||||
|
@ -330,16 +391,37 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
|
||||||
}
|
}
|
||||||
return this.participation.event.organizerActor;
|
return this.participation.event.organizerActor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get seatsLeft(): number | null {
|
||||||
|
if (this.participation.event.options.maximumAttendeeCapacity > 0) {
|
||||||
|
return (
|
||||||
|
this.participation.event.options.maximumAttendeeCapacity -
|
||||||
|
this.participation.event.participantStats.participant
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastSeatsLeft(): boolean {
|
||||||
|
if (this.seatsLeft) {
|
||||||
|
return this.seatsLeft < 10;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
|
@use "@/styles/_event-card";
|
||||||
|
@import "~bulma/sass/utilities/mixins.sass";
|
||||||
|
|
||||||
article.box {
|
article.box {
|
||||||
div.tag-container {
|
div.tag-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin-right: -5px;
|
@include margin-left(-5px);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
max-width: 40%;
|
max-width: 40%;
|
||||||
|
|
||||||
|
@ -359,49 +441,67 @@ article.box {
|
||||||
|
|
||||||
.list-card {
|
.list-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 6px;
|
padding: 0 6px 0 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
div.date-component {
|
|
||||||
align-self: flex-start;
|
|
||||||
padding: 5px;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
margin-top: 1px;
|
|
||||||
height: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
margin-left: 0rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-and-actions {
|
.content-and-actions {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-gap: 5px 10px;
|
||||||
align-items: center;
|
grid-template-areas: "preview" "body" "actions";
|
||||||
justify-content: center;
|
|
||||||
padding-bottom: 1rem;
|
@include tablet {
|
||||||
|
grid-template-columns: 1fr 3fr;
|
||||||
|
grid-template-areas: "preview body" "actions actions";
|
||||||
|
}
|
||||||
|
|
||||||
|
@include desktop {
|
||||||
|
grid-template-columns: 1fr 3fr 1fr;
|
||||||
|
grid-template-areas: "preview body actions";
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-preview {
|
||||||
|
grid-area: preview;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
height: 128px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
div.date-component {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 5px;
|
||||||
|
left: 5px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
object-position: center;
|
||||||
|
object-fit: cover;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
padding-right: 7.5px;
|
padding: 7px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: center;
|
||||||
|
grid-area: actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.list-card-content {
|
div.list-card-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
min-width: 350px;
|
grid-area: body;
|
||||||
|
|
||||||
.participation-actor span,
|
.participant-stats {
|
||||||
.participant-stats span {
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
|
|
||||||
button {
|
|
||||||
height: auto;
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.title-wrapper {
|
div.title-wrapper {
|
||||||
|
@ -419,11 +519,11 @@ article.box {
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 3;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-weight: 400;
|
font-size: 18px;
|
||||||
line-height: 1em;
|
line-height: 24px;
|
||||||
font-size: 1.4em;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
margin: auto 0;
|
margin: auto 0;
|
||||||
|
font-weight: bold;
|
||||||
|
color: $title-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -434,10 +534,10 @@ article.box {
|
||||||
background: $yellow-2;
|
background: $yellow-2;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
padding-left: calc(48px + 15px);
|
|
||||||
|
|
||||||
figure {
|
figure,
|
||||||
padding-right: 3px;
|
span.icon {
|
||||||
|
@include padding-right(3px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,59 +1,94 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="address-autocomplete">
|
<div class="address-autocomplete columns is-desktop">
|
||||||
<b-field expanded>
|
<div class="column">
|
||||||
<template slot="label">
|
<b-field
|
||||||
{{ actualLabel }}
|
:label-for="id"
|
||||||
<b-button
|
|
||||||
v-if="canShowLocateMeButton && !gettingLocation"
|
|
||||||
size="is-small"
|
|
||||||
icon-right="map-marker"
|
|
||||||
@click="locateMe"
|
|
||||||
/>
|
|
||||||
<span v-else-if="gettingLocation">{{ $t("Getting location") }}</span>
|
|
||||||
</template>
|
|
||||||
<b-autocomplete
|
|
||||||
:data="addressData"
|
|
||||||
v-model="queryText"
|
|
||||||
:placeholder="$t('e.g. 10 Rue Jangot')"
|
|
||||||
field="fullName"
|
|
||||||
:loading="isFetching"
|
|
||||||
@typing="fetchAsyncData"
|
|
||||||
icon="map-marker"
|
|
||||||
expanded
|
expanded
|
||||||
@select="updateSelected"
|
:message="fieldErrors"
|
||||||
|
:type="{ 'is-danger': fieldErrors.length }"
|
||||||
>
|
>
|
||||||
<template #default="{ option }">
|
<template slot="label">
|
||||||
<b-icon :icon="option.poiInfos.poiIcon.icon" />
|
{{ actualLabel }}
|
||||||
<b>{{ option.poiInfos.name }}</b
|
<span
|
||||||
><br />
|
class="is-size-6 has-text-weight-normal"
|
||||||
<small>{{ option.poiInfos.alternativeName }}</small>
|
v-if="gettingLocation"
|
||||||
|
>{{ $t("Getting location") }}</span
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<template slot="empty">
|
<p class="control" v-if="canShowLocateMeButton && !gettingLocation">
|
||||||
<span v-if="isFetching">{{ $t("Searching…") }}</span>
|
<b-button
|
||||||
<div v-else-if="queryText.length >= 3" class="is-enabled">
|
icon-right="map-marker"
|
||||||
<span>{{ $t('No results for "{queryText}"', { queryText }) }}</span>
|
@click="locateMe"
|
||||||
<span>{{
|
:title="$t('Use my location')"
|
||||||
$t(
|
/>
|
||||||
"You can try another search term or drag and drop the marker on the map",
|
</p>
|
||||||
{
|
<b-autocomplete
|
||||||
queryText,
|
:data="addressData"
|
||||||
}
|
v-model="queryText"
|
||||||
)
|
:placeholder="$t('e.g. 10 Rue Jangot')"
|
||||||
}}</span>
|
field="fullName"
|
||||||
<!-- <p class="control" @click="openNewAddressModal">-->
|
:loading="isFetching"
|
||||||
<!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
|
@typing="fetchAsyncData"
|
||||||
<!-- </p>-->
|
:icon="canShowLocateMeButton ? null : 'map-marker'"
|
||||||
</div>
|
expanded
|
||||||
</template>
|
@select="updateSelected"
|
||||||
</b-autocomplete>
|
v-bind="$attrs"
|
||||||
<b-button
|
:id="id"
|
||||||
:disabled="!queryText"
|
:disabled="disabled"
|
||||||
@click="resetAddress"
|
dir="auto"
|
||||||
class="reset-area"
|
>
|
||||||
icon-left="close"
|
<template #default="{ option }">
|
||||||
/>
|
<b-icon :icon="option.poiInfos.poiIcon.icon" />
|
||||||
</b-field>
|
<b>{{ option.poiInfos.name }}</b
|
||||||
<div class="map" v-if="selected && selected.geom && selected.poiInfos">
|
><br />
|
||||||
|
<small>{{ option.poiInfos.alternativeName }}</small>
|
||||||
|
</template>
|
||||||
|
<template #empty>
|
||||||
|
<span v-if="isFetching">{{ $t("Searching…") }}</span>
|
||||||
|
<div v-else-if="queryText.length >= 3" class="is-enabled">
|
||||||
|
<span>{{
|
||||||
|
$t('No results for "{queryText}"', { queryText })
|
||||||
|
}}</span>
|
||||||
|
<span>{{
|
||||||
|
$t(
|
||||||
|
"You can try another search term or drag and drop the marker on the map",
|
||||||
|
{
|
||||||
|
queryText,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}}</span>
|
||||||
|
<!-- <p class="control" @click="openNewAddressModal">-->
|
||||||
|
<!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
|
||||||
|
<!-- </p>-->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</b-autocomplete>
|
||||||
|
<b-button
|
||||||
|
:disabled="!queryText"
|
||||||
|
@click="resetAddress"
|
||||||
|
class="reset-area"
|
||||||
|
icon-left="close"
|
||||||
|
:title="$t('Clear address field')"
|
||||||
|
/>
|
||||||
|
</b-field>
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
v-if="!hideSelected && (selected.originId || selected.url)"
|
||||||
|
>
|
||||||
|
<div class="card-content">
|
||||||
|
<address-info
|
||||||
|
:address="selected"
|
||||||
|
:show-icon="true"
|
||||||
|
:show-timezone="true"
|
||||||
|
:user-timezone="userTimezone"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="map column"
|
||||||
|
v-if="!hideMap && selected && selected.geom && selected.poiInfos"
|
||||||
|
>
|
||||||
<map-leaflet
|
<map-leaflet
|
||||||
:coords="selected.geom"
|
:coords="selected.geom"
|
||||||
:marker="{
|
:marker="{
|
||||||
|
@ -65,149 +100,47 @@
|
||||||
:readOnly="false"
|
:readOnly="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- <b-modal v-if="selected" :active.sync="addressModalActive" :width="640" has-modal-card scroll="keep">-->
|
|
||||||
<!-- <div class="modal-card" style="width: auto">-->
|
|
||||||
<!-- <header class="modal-card-head">-->
|
|
||||||
<!-- <p class="modal-card-title">{{ $t('Add an address') }}</p>-->
|
|
||||||
<!-- </header>-->
|
|
||||||
<!-- <section class="modal-card-body">-->
|
|
||||||
<!-- <form>-->
|
|
||||||
<!-- <b-field :label="$t('Name')">-->
|
|
||||||
<!-- <b-input aria-required="true" required v-model="selected.description" />-->
|
|
||||||
<!-- </b-field>-->
|
|
||||||
|
|
||||||
<!-- <b-field :label="$t('Street')">-->
|
|
||||||
<!-- <b-input v-model="selected.street" />-->
|
|
||||||
<!-- </b-field>-->
|
|
||||||
|
|
||||||
<!-- <b-field grouped>-->
|
|
||||||
<!-- <b-field :label="$t('Postal Code')">-->
|
|
||||||
<!-- <b-input v-model="selected.postalCode" />-->
|
|
||||||
<!-- </b-field>-->
|
|
||||||
|
|
||||||
<!-- <b-field :label="$t('Locality')">-->
|
|
||||||
<!-- <b-input v-model="selected.locality" />-->
|
|
||||||
<!-- </b-field>-->
|
|
||||||
<!-- </b-field>-->
|
|
||||||
|
|
||||||
<!-- <b-field grouped>-->
|
|
||||||
<!-- <b-field :label="$t('Region')">-->
|
|
||||||
<!-- <b-input v-model="selected.region" />-->
|
|
||||||
<!-- </b-field>-->
|
|
||||||
|
|
||||||
<!-- <b-field :label="$t('Country')">-->
|
|
||||||
<!-- <b-input v-model="selected.country" />-->
|
|
||||||
<!-- </b-field>-->
|
|
||||||
<!-- </b-field>-->
|
|
||||||
<!-- </form>-->
|
|
||||||
<!-- </section>-->
|
|
||||||
<!-- <footer class="modal-card-foot">-->
|
|
||||||
<!-- <button class="button" type="button" @click="resetPopup()">{{ $t('Clear') }}</button>-->
|
|
||||||
<!-- </footer>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </b-modal>-->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
import { Component, Prop, Watch, Mixins } from "vue-property-decorator";
|
||||||
import { LatLng } from "leaflet";
|
import { LatLng } from "leaflet";
|
||||||
import debounce from "lodash/debounce";
|
|
||||||
import { DebouncedFunc } from "lodash";
|
|
||||||
import { Address, IAddress } from "../../types/address.model";
|
import { Address, IAddress } from "../../types/address.model";
|
||||||
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
|
import AddressAutoCompleteMixin from "../../mixins/AddressAutoCompleteMixin";
|
||||||
import { CONFIG } from "../../graphql/config";
|
import AddressInfo from "../../components/Address/AddressInfo.vue";
|
||||||
import { IConfig } from "../../types/config.model";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
inheritAttrs: false,
|
||||||
components: {
|
components: {
|
||||||
"map-leaflet": () =>
|
AddressInfo,
|
||||||
import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
|
|
||||||
},
|
|
||||||
apollo: {
|
|
||||||
config: CONFIG,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class FullAddressAutoComplete extends Vue {
|
export default class FullAddressAutoComplete extends Mixins(
|
||||||
@Prop({ required: true }) value!: IAddress;
|
AddressAutoCompleteMixin
|
||||||
|
) {
|
||||||
@Prop({ required: false, default: "" }) label!: string;
|
@Prop({ required: false, default: "" }) label!: string;
|
||||||
|
@Prop({ required: false }) userTimezone!: string;
|
||||||
addressData: IAddress[] = [];
|
@Prop({ required: false, default: false, type: Boolean }) disabled!: boolean;
|
||||||
|
@Prop({ required: false, default: false, type: Boolean }) hideMap!: boolean;
|
||||||
selected: IAddress = new Address();
|
@Prop({ required: false, default: false, type: Boolean })
|
||||||
|
hideSelected!: boolean;
|
||||||
isFetching = false;
|
|
||||||
|
|
||||||
queryText: string = (this.value && new Address(this.value).fullName) || "";
|
|
||||||
|
|
||||||
addressModalActive = false;
|
addressModalActive = false;
|
||||||
|
|
||||||
private gettingLocation = false;
|
private static componentId = 0;
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
created(): void {
|
||||||
private location!: GeolocationPosition;
|
FullAddressAutoComplete.componentId += 1;
|
||||||
|
|
||||||
private gettingLocationError: any;
|
|
||||||
|
|
||||||
private mapDefaultZoom = 15;
|
|
||||||
|
|
||||||
config!: IConfig;
|
|
||||||
|
|
||||||
fetchAsyncData!: DebouncedFunc<(query: string) => Promise<void>>;
|
|
||||||
|
|
||||||
// We put this in data because of issues like
|
|
||||||
// https://github.com/vuejs/vue-class-component/issues/263
|
|
||||||
data(): Record<string, unknown> {
|
|
||||||
return {
|
|
||||||
fetchAsyncData: debounce(this.asyncData, 200),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async asyncData(query: string): Promise<void> {
|
get id(): string {
|
||||||
if (!query.length) {
|
return `full-address-autocomplete-${FullAddressAutoComplete.componentId}`;
|
||||||
this.addressData = [];
|
|
||||||
this.selected = new Address();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.length < 3) {
|
|
||||||
this.addressData = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isFetching = true;
|
|
||||||
const result = await this.$apollo.query({
|
|
||||||
query: ADDRESS,
|
|
||||||
fetchPolicy: "network-only",
|
|
||||||
variables: {
|
|
||||||
query,
|
|
||||||
locale: this.$i18n.locale,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addressData = result.data.searchAddress.map(
|
|
||||||
(address: IAddress) => new Address(address)
|
|
||||||
);
|
|
||||||
this.isFetching = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("config")
|
|
||||||
watchConfig(config: IConfig): void {
|
|
||||||
if (!config.geocoding.autocomplete) {
|
|
||||||
// If autocomplete is disabled, we put a larger debounce value
|
|
||||||
// so that we don't request with incomplete address
|
|
||||||
this.fetchAsyncData = debounce(this.asyncData, 2000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch("value")
|
@Watch("value")
|
||||||
updateEditing(): void {
|
updateEditing(): void {
|
||||||
if (!(this.value && this.value.id)) return;
|
if (!(this.value && this.value.id)) return;
|
||||||
this.selected = this.value;
|
this.selected = this.value;
|
||||||
const address = new Address(this.selected);
|
|
||||||
if (address.poiInfos) {
|
|
||||||
this.queryText = `${address.poiInfos.name} ${address.poiInfos.alternativeName}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelected(option: IAddress): void {
|
updateSelected(option: IAddress): void {
|
||||||
|
@ -225,30 +158,6 @@ export default class FullAddressAutoComplete extends Vue {
|
||||||
this.addressModalActive = true;
|
this.addressModalActive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async reverseGeoCode(e: LatLng, zoom: number): Promise<void> {
|
|
||||||
// If the position has been updated through autocomplete selection, no need to geocode it!
|
|
||||||
if (this.checkCurrentPosition(e)) return;
|
|
||||||
const result = await this.$apollo.query({
|
|
||||||
query: REVERSE_GEOCODE,
|
|
||||||
variables: {
|
|
||||||
latitude: e.lat,
|
|
||||||
longitude: e.lng,
|
|
||||||
zoom,
|
|
||||||
locale: this.$i18n.locale,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addressData = result.data.reverseGeocode.map(
|
|
||||||
(address: IAddress) => new Address(address)
|
|
||||||
);
|
|
||||||
if (this.addressData.length > 0) {
|
|
||||||
const defaultAddress = new Address(this.addressData[0]);
|
|
||||||
this.selected = defaultAddress;
|
|
||||||
this.$emit("input", this.selected);
|
|
||||||
this.queryText = `${defaultAddress.poiInfos.name} ${defaultAddress.poiInfos.alternativeName}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkCurrentPosition(e: LatLng): boolean {
|
checkCurrentPosition(e: LatLng): boolean {
|
||||||
if (!this.selected || !this.selected.geom) return false;
|
if (!this.selected || !this.selected.geom) return false;
|
||||||
const lat = parseFloat(this.selected.geom.split(";")[1]);
|
const lat = parseFloat(this.selected.geom.split(";")[1]);
|
||||||
|
@ -257,25 +166,6 @@ export default class FullAddressAutoComplete extends Vue {
|
||||||
return e.lat === lat && e.lng === lon;
|
return e.lat === lat && e.lng === lon;
|
||||||
}
|
}
|
||||||
|
|
||||||
async locateMe(): Promise<void> {
|
|
||||||
this.gettingLocation = true;
|
|
||||||
try {
|
|
||||||
this.gettingLocation = false;
|
|
||||||
this.location = await FullAddressAutoComplete.getLocation();
|
|
||||||
this.mapDefaultZoom = 12;
|
|
||||||
this.reverseGeoCode(
|
|
||||||
new LatLng(
|
|
||||||
this.location.coords.latitude,
|
|
||||||
this.location.coords.longitude
|
|
||||||
),
|
|
||||||
12
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
this.gettingLocation = false;
|
|
||||||
this.gettingLocationError = e.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get actualLabel(): string {
|
get actualLabel(): string {
|
||||||
return this.label || (this.$t("Find an address") as string);
|
return this.label || (this.$t("Find an address") as string);
|
||||||
}
|
}
|
||||||
|
@ -284,38 +174,6 @@ export default class FullAddressAutoComplete extends Vue {
|
||||||
get canShowLocateMeButton(): boolean {
|
get canShowLocateMeButton(): boolean {
|
||||||
return window.isSecureContext;
|
return window.isSecureContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
static async getLocation(): Promise<GeolocationPosition> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!("geolocation" in navigator)) {
|
|
||||||
reject(new Error("Geolocation is not available."));
|
|
||||||
}
|
|
||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
|
||||||
(pos) => {
|
|
||||||
resolve(pos);
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("queryText")
|
|
||||||
resetAddressOnEmptyField(queryText: string): void {
|
|
||||||
if (queryText === "" && this.selected?.id) {
|
|
||||||
console.log("doing reset");
|
|
||||||
this.resetAddress();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetAddress(): void {
|
|
||||||
this.$emit("input", null);
|
|
||||||
this.queryText = "";
|
|
||||||
this.selected = new Address();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
90
js/src/components/Event/GroupedMultiEventMinimalistCard.vue
Normal file
90
js/src/components/Event/GroupedMultiEventMinimalistCard.vue
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<template>
|
||||||
|
<div class="events-wrapper">
|
||||||
|
<div class="month-group" v-for="key of keys" :key="key">
|
||||||
|
<h2 class="is-size-5 month-name">
|
||||||
|
{{ monthName(groupEvents(key)[0]) }}
|
||||||
|
</h2>
|
||||||
|
<event-minimalist-card
|
||||||
|
class="py-4"
|
||||||
|
v-for="event in groupEvents(key)"
|
||||||
|
:key="event.id"
|
||||||
|
:event="event"
|
||||||
|
:isCurrentActorMember="isCurrentActorMember"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { IEvent } from "@/types/event.model";
|
||||||
|
import { PropType } from "vue";
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import EventMinimalistCard from "./EventMinimalistCard.vue";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
EventMinimalistCard,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class GroupedMultiEventMinimalistCard extends Vue {
|
||||||
|
@Prop({ type: Array as PropType<IEvent[]>, required: true })
|
||||||
|
events!: IEvent[];
|
||||||
|
@Prop({ required: false, type: Boolean, default: false })
|
||||||
|
isCurrentActorMember!: boolean;
|
||||||
|
|
||||||
|
get monthlyGroupedEvents(): Map<string, IEvent[]> {
|
||||||
|
return this.events.reduce((acc: Map<string, IEvent[]>, event: IEvent) => {
|
||||||
|
const beginsOn = new Date(event.beginsOn);
|
||||||
|
const month = `${beginsOn.getUTCMonth()}-${beginsOn.getUTCFullYear()}`;
|
||||||
|
const monthEvents = acc.get(month) || [];
|
||||||
|
acc.set(month, [...monthEvents, event]);
|
||||||
|
return acc;
|
||||||
|
}, new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
get keys(): string[] {
|
||||||
|
return Array.from(this.monthlyGroupedEvents.keys()).sort((a, b) =>
|
||||||
|
b.localeCompare(a)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
groupEvents(key: string): IEvent[] {
|
||||||
|
return this.monthlyGroupedEvents.get(key) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
monthName(event: IEvent): string {
|
||||||
|
const beginsOn = new Date(event.beginsOn);
|
||||||
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
}).format(beginsOn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.events-wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 20px;
|
||||||
|
grid-template: 1fr;
|
||||||
|
}
|
||||||
|
.month-group {
|
||||||
|
.month-name {
|
||||||
|
text-transform: capitalize;
|
||||||
|
text-transform: capitalize;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background: $orange-3;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 100%;
|
||||||
|
content: "";
|
||||||
|
width: calc(100% + 30px);
|
||||||
|
height: 3px;
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
39
js/src/components/Event/MultiCard.vue
Normal file
39
js/src/components/Event/MultiCard.vue
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<div class="multi-card-event">
|
||||||
|
<event-card
|
||||||
|
class="event-card"
|
||||||
|
v-for="event in events"
|
||||||
|
:event="event"
|
||||||
|
:key="event.uuid"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { IEvent } from "@/types/event.model";
|
||||||
|
import { PropType } from "vue";
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import EventCard from "./EventCard.vue";
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
EventCard,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class MultiCard extends Vue {
|
||||||
|
@Prop({ type: Array as PropType<IEvent[]>, required: true })
|
||||||
|
events!: IEvent[];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.multi-card-event {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-rows: 1fr;
|
||||||
|
grid-column-gap: 20px;
|
||||||
|
grid-row-gap: 30px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
.event-card {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
38
js/src/components/Event/MultiEventMinimalistCard.vue
Normal file
38
js/src/components/Event/MultiEventMinimalistCard.vue
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<div class="events-wrapper">
|
||||||
|
<event-minimalist-card
|
||||||
|
v-for="event in events"
|
||||||
|
:key="event.id"
|
||||||
|
:event="event"
|
||||||
|
:isCurrentActorMember="isCurrentActorMember"
|
||||||
|
:showOrganizer="showOrganizer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { IEvent } from "@/types/event.model";
|
||||||
|
import { PropType } from "vue";
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import EventMinimalistCard from "./EventMinimalistCard.vue";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
EventMinimalistCard,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class MultiEventMinimalistCard extends Vue {
|
||||||
|
@Prop({ type: Array as PropType<IEvent[]>, required: true })
|
||||||
|
events!: IEvent[];
|
||||||
|
@Prop({ required: false, type: Boolean, default: false })
|
||||||
|
isCurrentActorMember!: boolean;
|
||||||
|
@Prop({ required: false, type: Boolean, default: false })
|
||||||
|
showOrganizer!: boolean;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.events-wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 20px;
|
||||||
|
grid-template: 1fr;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,36 +1,52 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="list is-hoverable">
|
<div class="list is-hoverable">
|
||||||
<b-input
|
<b-input
|
||||||
|
dir="auto"
|
||||||
:placeholder="$t('Filter by profile or group name')"
|
:placeholder="$t('Filter by profile or group name')"
|
||||||
v-model="actorFilter"
|
v-model="actorFilter"
|
||||||
/>
|
/>
|
||||||
<b-radio-button
|
<transition-group
|
||||||
v-model="selectedActor"
|
tag="ul"
|
||||||
:native-value="availableActor"
|
class="grid grid-cols-1 gap-y-3 m-5 max-w-md mx-auto"
|
||||||
class="list-item"
|
enter-active-class="duration-300 ease-out"
|
||||||
v-for="availableActor in actualFilteredAvailableActors"
|
enter-from-class="transform opacity-0"
|
||||||
:key="availableActor.id"
|
enter-to-class="opacity-100"
|
||||||
|
leave-active-class="duration-200 ease-in"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="transform opacity-0"
|
||||||
>
|
>
|
||||||
<div class="media">
|
<li
|
||||||
<figure class="image is-48x48" v-if="availableActor.avatar">
|
class="relative focus-within:shadow-lg"
|
||||||
<img
|
v-for="availableActor in actualFilteredAvailableActors"
|
||||||
class="media-left is-rounded"
|
:key="availableActor.id"
|
||||||
:src="availableActor.avatar.url"
|
>
|
||||||
alt=""
|
<input
|
||||||
/>
|
class="sr-only peer"
|
||||||
</figure>
|
type="radio"
|
||||||
<b-icon
|
:value="availableActor"
|
||||||
class="media-left"
|
name="availableActors"
|
||||||
v-else
|
v-model="selectedActor"
|
||||||
size="is-large"
|
:id="`availableActor-${availableActor.id}`"
|
||||||
icon="account-circle"
|
|
||||||
/>
|
/>
|
||||||
<div class="media-content">
|
<label
|
||||||
<h3>{{ availableActor.name }}</h3>
|
class="flex flex-wrap p-3 bg-white border border-gray-300 rounded-lg cursor-pointer hover:bg-gray-50 peer-checked:ring-primary peer-checked:ring-2 peer-checked:border-transparent"
|
||||||
<small>{{ `@${availableActor.preferredUsername}` }}</small>
|
:for="`availableActor-${availableActor.id}`"
|
||||||
</div>
|
>
|
||||||
</div>
|
<figure class="image is-48x48" v-if="availableActor.avatar">
|
||||||
</b-radio-button>
|
<img
|
||||||
|
class="image is-rounded"
|
||||||
|
:src="availableActor.avatar.url"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
<b-icon v-else size="is-large" icon="account-circle" />
|
||||||
|
<div>
|
||||||
|
<h3>{{ availableActor.name }}</h3>
|
||||||
|
<small>{{ `@${availableActor.preferredUsername}` }}</small>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -50,6 +66,13 @@ import { MemberRole } from "@/types/enums";
|
||||||
groupMemberships: {
|
groupMemberships: {
|
||||||
query: LOGGED_USER_MEMBERSHIPS,
|
query: LOGGED_USER_MEMBERSHIPS,
|
||||||
update: (data) => data.loggedUser.memberships,
|
update: (data) => data.loggedUser.memberships,
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
membershipName: this.actorFilter,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
identities: IDENTITIES,
|
identities: IDENTITIES,
|
||||||
currentActor: CURRENT_ACTOR_CLIENT,
|
currentActor: CURRENT_ACTOR_CLIENT,
|
||||||
|
@ -121,6 +144,7 @@ export default class OrganizerPicker extends Vue {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
::v-deep .list-item {
|
::v-deep .list-item {
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
@ -133,11 +157,11 @@ export default class OrganizerPicker extends Vue {
|
||||||
|
|
||||||
figure.image,
|
figure.image,
|
||||||
span.icon.media-left {
|
span.icon.media-left {
|
||||||
margin-right: 0.5rem;
|
@include margin-right(0.5rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
span.icon.media-left {
|
span.icon.media-left {
|
||||||
margin-left: -0.25rem;
|
@include margin-left(-0.25rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="organizer-picker" v-if="selectedActor">
|
<div
|
||||||
|
class="bg-white border border-gray-300 rounded-lg cursor-pointer"
|
||||||
|
v-if="selectedActor"
|
||||||
|
>
|
||||||
<!-- If we have a current actor (inline) -->
|
<!-- If we have a current actor (inline) -->
|
||||||
<div
|
<div
|
||||||
v-if="inline && selectedActor.id"
|
v-if="inline && selectedActor.id"
|
||||||
class="inline box"
|
class="inline box"
|
||||||
|
dir="auto"
|
||||||
@click="isComponentModalActive = true"
|
@click="isComponentModalActive = true"
|
||||||
>
|
>
|
||||||
<div class="media">
|
<div class="media">
|
||||||
|
@ -12,14 +16,14 @@
|
||||||
<img
|
<img
|
||||||
class="image is-rounded"
|
class="image is-rounded"
|
||||||
:src="selectedActor.avatar.url"
|
:src="selectedActor.avatar.url"
|
||||||
:alt="selectedActor.avatar.alt"
|
:alt="selectedActor.avatar.alt || ''"
|
||||||
/>
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
<b-icon v-else size="is-large" icon="account-circle" />
|
<b-icon v-else size="is-large" icon="account-circle" />
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content" v-if="selectedActor.name">
|
<div class="media-content" v-if="selectedActor.name">
|
||||||
<p class="is-4">{{ selectedActor.name }}</p>
|
<p class="is-4">{{ selectedActor.name }}</p>
|
||||||
<p class="is-6 has-text-grey">
|
<p class="is-6 has-text-grey-dark">
|
||||||
{{ `@${selectedActor.preferredUsername}` }}
|
{{ `@${selectedActor.preferredUsername}` }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,7 +49,11 @@
|
||||||
/>
|
/>
|
||||||
<b-icon v-else size="is-large" icon="account-circle" />
|
<b-icon v-else size="is-large" icon="account-circle" />
|
||||||
</span>
|
</span>
|
||||||
<b-modal :active.sync="isComponentModalActive" has-modal-card>
|
<b-modal
|
||||||
|
:active.sync="isComponentModalActive"
|
||||||
|
has-modal-card
|
||||||
|
:close-button-aria-label="$t('Close')"
|
||||||
|
>
|
||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
<header class="modal-card-head">
|
<header class="modal-card-head">
|
||||||
<p class="modal-card-title">{{ $t("Pick a profile or a group") }}</p>
|
<p class="modal-card-title">{{ $t("Pick a profile or a group") }}</p>
|
||||||
|
@ -60,41 +68,61 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="column contact-picker">
|
<div class="column contact-picker">
|
||||||
<div v-if="isSelectedActorAGroup && actorMembers.length > 0">
|
<div v-if="isSelectedActorAGroup">
|
||||||
<p>{{ $t("Add a contact") }}</p>
|
<p>{{ $t("Add a contact") }}</p>
|
||||||
<b-input
|
<b-input
|
||||||
:placeholder="$t('Filter by name')"
|
:placeholder="$t('Filter by name')"
|
||||||
v-model="contactFilter"
|
:value="contactFilter"
|
||||||
|
@input="debounceSetFilterByName"
|
||||||
|
dir="auto"
|
||||||
/>
|
/>
|
||||||
<p
|
<div v-if="actorMembers.length > 0">
|
||||||
class="field"
|
<p
|
||||||
v-for="actor in filteredActorMembers"
|
class="field"
|
||||||
:key="actor.id"
|
v-for="actor in filteredActorMembers"
|
||||||
>
|
:key="actor.id"
|
||||||
<b-checkbox v-model="actualContacts" :native-value="actor.id">
|
>
|
||||||
<div class="media">
|
<b-checkbox
|
||||||
<div class="media-left">
|
v-model="actualContacts"
|
||||||
<figure class="image is-48x48" v-if="actor.avatar">
|
:native-value="actor.id"
|
||||||
<img
|
>
|
||||||
class="image is-rounded"
|
<div class="media">
|
||||||
:src="actor.avatar.url"
|
<div class="media-left">
|
||||||
:alt="actor.avatar.alt"
|
<figure class="image is-48x48" v-if="actor.avatar">
|
||||||
|
<img
|
||||||
|
class="image is-rounded"
|
||||||
|
:src="actor.avatar.url"
|
||||||
|
:alt="actor.avatar.alt"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
<b-icon
|
||||||
|
v-else
|
||||||
|
size="is-large"
|
||||||
|
icon="account-circle"
|
||||||
/>
|
/>
|
||||||
</figure>
|
</div>
|
||||||
<b-icon v-else size="is-large" icon="account-circle" />
|
<div class="media-content" v-if="actor.name">
|
||||||
</div>
|
<p class="is-4">{{ actor.name }}</p>
|
||||||
<div class="media-content" v-if="actor.name">
|
<p class="is-6 has-text-grey-dark">
|
||||||
<p class="is-4">{{ actor.name }}</p>
|
{{ `@${usernameWithDomain(actor)}` }}
|
||||||
<p class="is-6 has-text-grey-dark">
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="media-content" v-else>
|
||||||
{{ `@${usernameWithDomain(actor)}` }}
|
{{ `@${usernameWithDomain(actor)}` }}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content" v-else>
|
</b-checkbox>
|
||||||
{{ `@${usernameWithDomain(actor)}` }}
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
</b-checkbox>
|
v-else-if="
|
||||||
</p>
|
actorMembers.length === 0 && contactFilter.length > 0
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<empty-content icon="account-multiple" :inline="true">
|
||||||
|
{{ $t("No group member found") }}
|
||||||
|
</empty-content>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="content has-text-grey-dark has-text-centered">
|
<div v-else class="content has-text-grey-dark has-text-centered">
|
||||||
<p>{{ $t("Your profile will be shown as contact.") }}</p>
|
<p>{{ $t("Your profile will be shown as contact.") }}</p>
|
||||||
|
@ -116,14 +144,16 @@ import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
import { IMember } from "@/types/actor/member.model";
|
import { IMember } from "@/types/actor/member.model";
|
||||||
import { IActor, IGroup, IPerson, usernameWithDomain } from "../../types/actor";
|
import { IActor, IGroup, IPerson, usernameWithDomain } from "../../types/actor";
|
||||||
import OrganizerPicker from "./OrganizerPicker.vue";
|
import OrganizerPicker from "./OrganizerPicker.vue";
|
||||||
|
import EmptyContent from "../Utils/EmptyContent.vue";
|
||||||
import {
|
import {
|
||||||
CURRENT_ACTOR_CLIENT,
|
CURRENT_ACTOR_CLIENT,
|
||||||
IDENTITIES,
|
IDENTITIES,
|
||||||
LOGGED_USER_MEMBERSHIPS,
|
PERSON_GROUP_MEMBERSHIPS,
|
||||||
} from "../../graphql/actor";
|
} from "../../graphql/actor";
|
||||||
import { Paginate } from "../../types/paginate";
|
import { Paginate } from "../../types/paginate";
|
||||||
import { GROUP_MEMBERS } from "@/graphql/member";
|
import { GROUP_MEMBERS } from "@/graphql/member";
|
||||||
import { ActorType, MemberRole } from "@/types/enums";
|
import { ActorType, MemberRole } from "@/types/enums";
|
||||||
|
import debounce from "lodash/debounce";
|
||||||
|
|
||||||
const MEMBER_ROLES = [
|
const MEMBER_ROLES = [
|
||||||
MemberRole.CREATOR,
|
MemberRole.CREATOR,
|
||||||
|
@ -133,16 +163,17 @@ const MEMBER_ROLES = [
|
||||||
];
|
];
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { OrganizerPicker },
|
components: { OrganizerPicker, EmptyContent },
|
||||||
apollo: {
|
apollo: {
|
||||||
members: {
|
members: {
|
||||||
query: GROUP_MEMBERS,
|
query: GROUP_MEMBERS,
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
name: usernameWithDomain(this.selectedActor),
|
groupName: usernameWithDomain(this.selectedActor),
|
||||||
page: this.membersPage,
|
page: this.membersPage,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
roles: MEMBER_ROLES.join(","),
|
roles: MEMBER_ROLES.join(","),
|
||||||
|
name: this.contactFilter,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
update: (data) => data.group.members,
|
update: (data) => data.group.members,
|
||||||
|
@ -153,13 +184,17 @@ const MEMBER_ROLES = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
currentActor: CURRENT_ACTOR_CLIENT,
|
currentActor: CURRENT_ACTOR_CLIENT,
|
||||||
userMemberships: {
|
personMemberships: {
|
||||||
query: LOGGED_USER_MEMBERSHIPS,
|
query: PERSON_GROUP_MEMBERSHIPS,
|
||||||
variables: {
|
variables() {
|
||||||
page: 1,
|
return {
|
||||||
limit: 100,
|
id: this.currentActor?.id,
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
groupId: this.$route.query?.actorId,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
update: (data) => data.loggedUser.memberships,
|
update: (data) => data.person.memberships,
|
||||||
},
|
},
|
||||||
identities: IDENTITIES,
|
identities: IDENTITIES,
|
||||||
},
|
},
|
||||||
|
@ -169,6 +204,9 @@ export default class OrganizerPickerWrapper extends Vue {
|
||||||
|
|
||||||
@Prop({ default: true, type: Boolean }) inline!: boolean;
|
@Prop({ default: true, type: Boolean }) inline!: boolean;
|
||||||
|
|
||||||
|
@Prop({ type: Array, required: false, default: () => [] })
|
||||||
|
contacts!: IActor[];
|
||||||
|
|
||||||
currentActor!: IPerson;
|
currentActor!: IPerson;
|
||||||
|
|
||||||
identities!: IPerson[];
|
identities!: IPerson[];
|
||||||
|
@ -179,13 +217,17 @@ export default class OrganizerPickerWrapper extends Vue {
|
||||||
|
|
||||||
usernameWithDomain = usernameWithDomain;
|
usernameWithDomain = usernameWithDomain;
|
||||||
|
|
||||||
@Prop({ type: Array, required: false, default: () => [] })
|
|
||||||
contacts!: IActor[];
|
|
||||||
members: Paginate<IMember> = { elements: [], total: 0 };
|
members: Paginate<IMember> = { elements: [], total: 0 };
|
||||||
|
|
||||||
membersPage = 1;
|
membersPage = 1;
|
||||||
|
|
||||||
userMemberships: Paginate<IMember> = { elements: [], total: 0 };
|
personMemberships: Paginate<IMember> = { elements: [], total: 0 };
|
||||||
|
|
||||||
|
data(): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
debounceSetFilterByName: debounce(this.setContactFilter, 1000),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
get actualContacts(): (string | undefined)[] {
|
get actualContacts(): (string | undefined)[] {
|
||||||
return this.contacts.map(({ id }) => id);
|
return this.contacts.map(({ id }) => id);
|
||||||
|
@ -198,15 +240,17 @@ export default class OrganizerPickerWrapper extends Vue {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch("userMemberships")
|
setContactFilter(contactFilter: string) {
|
||||||
|
this.contactFilter = contactFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch("personMemberships")
|
||||||
setInitialActor(): void {
|
setInitialActor(): void {
|
||||||
if (this.$route.query?.actorId) {
|
if (
|
||||||
const actorId = this.$route.query?.actorId as string;
|
this.personMemberships?.elements[0]?.parent?.id ===
|
||||||
const actor = this.userMemberships.elements.find(
|
this.$route.query?.actorId
|
||||||
({ parent: { id }, role }) =>
|
) {
|
||||||
actorId === id && MEMBER_ROLES.includes(role)
|
this.selectedActor = this.personMemberships?.elements[0]?.parent;
|
||||||
)?.parent as IActor;
|
|
||||||
this.selectedActor = actor;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,7 +292,7 @@ export default class OrganizerPickerWrapper extends Vue {
|
||||||
actor.preferredUsername.toLowerCase(),
|
actor.preferredUsername.toLowerCase(),
|
||||||
actor.name?.toLowerCase(),
|
actor.name?.toLowerCase(),
|
||||||
actor.domain?.toLowerCase(),
|
actor.domain?.toLowerCase(),
|
||||||
].some((match) => match?.includes(this.contactFilter.toLowerCase()));
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,18 +30,22 @@ A button to set your participation
|
||||||
position="is-bottom-left"
|
position="is-bottom-left"
|
||||||
v-if="participation && participation.role === ParticipantRole.PARTICIPANT"
|
v-if="participation && participation.role === ParticipantRole.PARTICIPANT"
|
||||||
>
|
>
|
||||||
<button class="button is-success is-large" type="button" slot="trigger">
|
<template #trigger="{ active }">
|
||||||
<b-icon icon="check" />
|
<b-button
|
||||||
<template>
|
type="is-success"
|
||||||
<span>{{ $t("I participate") }}</span>
|
size="is-large"
|
||||||
</template>
|
icon-left="check"
|
||||||
<b-icon icon="menu-down" />
|
:icon-right="active ? 'menu-up' : 'menu-down'"
|
||||||
</button>
|
>
|
||||||
|
{{ $t("I participate") }}
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<b-dropdown-item
|
<b-dropdown-item
|
||||||
:value="false"
|
:value="false"
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
@click="confirmLeave"
|
@click="confirmLeave"
|
||||||
|
@keyup.enter="confirmLeave"
|
||||||
class="has-text-danger"
|
class="has-text-danger"
|
||||||
>{{ $t("Cancel my participation…") }}</b-dropdown-item
|
>{{ $t("Cancel my participation…") }}</b-dropdown-item
|
||||||
>
|
>
|
||||||
|
@ -73,6 +77,7 @@ A button to set your participation
|
||||||
:value="false"
|
:value="false"
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
@click="confirmLeave"
|
@click="confirmLeave"
|
||||||
|
@keyup.enter="confirmLeave"
|
||||||
class="has-text-danger"
|
class="has-text-danger"
|
||||||
>{{ $t("Cancel my participation request…") }}</b-dropdown-item
|
>{{ $t("Cancel my participation request…") }}</b-dropdown-item
|
||||||
>
|
>
|
||||||
|
@ -101,21 +106,25 @@ A button to set your participation
|
||||||
position="is-bottom-left"
|
position="is-bottom-left"
|
||||||
v-else-if="!participation && currentActor.id"
|
v-else-if="!participation && currentActor.id"
|
||||||
>
|
>
|
||||||
<button class="button is-primary is-large" type="button" slot="trigger">
|
<template #trigger="{ active }">
|
||||||
<template>
|
<b-button
|
||||||
<span>{{ $t("Participate") }}</span>
|
type="is-primary"
|
||||||
</template>
|
size="is-large"
|
||||||
<b-icon icon="menu-down" />
|
:icon-right="active ? 'menu-up' : 'menu-down'"
|
||||||
</button>
|
>
|
||||||
|
{{ $t("Participate") }}
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<b-dropdown-item
|
<b-dropdown-item
|
||||||
:value="true"
|
:value="true"
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
@click="joinEvent(currentActor)"
|
@click="joinEvent(currentActor)"
|
||||||
|
@keyup.enter="joinEvent(currentActor)"
|
||||||
>
|
>
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left">
|
<div class="media-left" v-if="currentActor.avatar">
|
||||||
<figure class="image is-32x32" v-if="currentActor.avatar">
|
<figure class="image is-32x32">
|
||||||
<img class="is-rounded" :src="currentActor.avatar.url" alt />
|
<img class="is-rounded" :src="currentActor.avatar.url" alt />
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
|
@ -136,11 +145,13 @@ A button to set your participation
|
||||||
:value="false"
|
:value="false"
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
@click="joinModal"
|
@click="joinModal"
|
||||||
|
@keyup.enter="joinModal"
|
||||||
v-if="identities.length > 1"
|
v-if="identities.length > 1"
|
||||||
>{{ $t("with another identity…") }}</b-dropdown-item
|
>{{ $t("with another identity…") }}</b-dropdown-item
|
||||||
>
|
>
|
||||||
</b-dropdown>
|
</b-dropdown>
|
||||||
<b-button
|
<b-button
|
||||||
|
rel="nofollow"
|
||||||
tag="router-link"
|
tag="router-link"
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.EVENT_PARTICIPATE_LOGGED_OUT,
|
name: RouteName.EVENT_PARTICIPATE_LOGGED_OUT,
|
||||||
|
@ -154,6 +165,7 @@ A button to set your participation
|
||||||
>
|
>
|
||||||
<b-button
|
<b-button
|
||||||
tag="router-link"
|
tag="router-link"
|
||||||
|
rel="nofollow"
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT,
|
name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT,
|
||||||
params: { uuid: event.uuid },
|
params: { uuid: event.uuid },
|
||||||
|
|
|
@ -86,11 +86,13 @@
|
||||||
/></a>
|
/></a>
|
||||||
<a
|
<a
|
||||||
:href="telegramShareUrl"
|
:href="telegramShareUrl"
|
||||||
|
class="telegram"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="nofollow noopener"
|
rel="nofollow noopener"
|
||||||
title="Telegram"
|
title="Telegram"
|
||||||
><b-icon icon="telegram" size="is-large" type="is-primary"
|
>
|
||||||
/></a>
|
<telegram-logo />
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
:href="linkedInShareUrl"
|
:href="linkedInShareUrl"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -126,11 +128,13 @@ import { EventStatus, EventVisibility } from "@/types/enums";
|
||||||
import { IEvent } from "../../types/event.model";
|
import { IEvent } from "../../types/event.model";
|
||||||
import DiasporaLogo from "../Share/DiasporaLogo.vue";
|
import DiasporaLogo from "../Share/DiasporaLogo.vue";
|
||||||
import MastodonLogo from "../Share/MastodonLogo.vue";
|
import MastodonLogo from "../Share/MastodonLogo.vue";
|
||||||
|
import TelegramLogo from "../Share/TelegramLogo.vue";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
DiasporaLogo,
|
DiasporaLogo,
|
||||||
MastodonLogo,
|
MastodonLogo,
|
||||||
|
TelegramLogo,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class ShareEventModal extends Vue {
|
export default class ShareEventModal extends Vue {
|
||||||
|
@ -186,7 +190,7 @@ export default class ShareEventModal extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
get mastodonShareUrl(): string {
|
get mastodonShareUrl(): string {
|
||||||
return `https://toot.karamoff.dev/?text=${encodeURIComponent(
|
return `https://toot.kytta.dev/?text=${encodeURIComponent(
|
||||||
this.basicTextToEncode
|
this.basicTextToEncode
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
@ -207,7 +211,8 @@ export default class ShareEventModal extends Vue {
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.diaspora,
|
.diaspora,
|
||||||
.mastodon {
|
.mastodon,
|
||||||
|
.telegram {
|
||||||
::v-deep span svg {
|
::v-deep span svg {
|
||||||
width: 2.25rem;
|
width: 2.25rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<b-field>
|
<b-field :label-for="id">
|
||||||
<template slot="label">
|
<template slot="label">
|
||||||
{{ $t("Add some tags") }}
|
{{ $t("Add some tags") }}
|
||||||
<b-tooltip
|
<b-tooltip
|
||||||
|
@ -16,57 +16,89 @@
|
||||||
:data="filteredTags"
|
:data="filteredTags"
|
||||||
autocomplete
|
autocomplete
|
||||||
:allow-new="true"
|
:allow-new="true"
|
||||||
:field="path"
|
:field="'title'"
|
||||||
icon="label"
|
icon="label"
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
maxtags="10"
|
maxtags="10"
|
||||||
:placeholder="$t('Eg: Stockholm, Dance, Chess…')"
|
:placeholder="$t('Eg: Stockholm, Dance, Chess…')"
|
||||||
@typing="getFilteredTags"
|
@typing="debouncedGetFilteredTags"
|
||||||
|
:id="id"
|
||||||
|
dir="auto"
|
||||||
>
|
>
|
||||||
</b-taginput>
|
</b-taginput>
|
||||||
</b-field>
|
</b-field>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import get from "lodash/get";
|
|
||||||
import differenceBy from "lodash/differenceBy";
|
import differenceBy from "lodash/differenceBy";
|
||||||
import { ITag } from "../../types/tag.model";
|
import { ITag } from "../../types/tag.model";
|
||||||
|
import { FILTER_TAGS } from "@/graphql/tags";
|
||||||
|
import debounce from "lodash/debounce";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
computed: {
|
apollo: {
|
||||||
tagsStrings: {
|
tags: {
|
||||||
get() {
|
query: FILTER_TAGS,
|
||||||
return this.$props.value.map((tag: ITag) => tag.title);
|
variables() {
|
||||||
},
|
return {
|
||||||
set(tagStrings) {
|
filter: this.text,
|
||||||
const tagEntities = tagStrings.map((tag: string | ITag) => {
|
};
|
||||||
if (typeof tag !== "string") {
|
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
return { title: tag, slug: tag } as ITag;
|
|
||||||
});
|
|
||||||
this.$emit("input", tagEntities);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class TagInput extends Vue {
|
export default class TagInput extends Vue {
|
||||||
@Prop({ required: false, default: () => [] }) data!: ITag[];
|
|
||||||
|
|
||||||
@Prop({ required: true, default: "value" }) path!: string;
|
|
||||||
|
|
||||||
@Prop({ required: true }) value!: ITag[];
|
@Prop({ required: true }) value!: ITag[];
|
||||||
|
|
||||||
filteredTags: ITag[] = [];
|
tags!: ITag[];
|
||||||
|
|
||||||
getFilteredTags(text: string): void {
|
text = "";
|
||||||
this.filteredTags = differenceBy(this.data, this.value, "id").filter(
|
|
||||||
|
private static componentId = 0;
|
||||||
|
|
||||||
|
created(): void {
|
||||||
|
TagInput.componentId += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id(): string {
|
||||||
|
return `tag-input-${TagInput.componentId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
data(): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
debouncedGetFilteredTags: debounce(this.getFilteredTags, 200),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFilteredTags(text: string): Promise<void> {
|
||||||
|
this.text = text;
|
||||||
|
await this.$apollo.queries.tags.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
get filteredTags(): ITag[] {
|
||||||
|
return differenceBy(this.tags, this.value, "id").filter(
|
||||||
(option) =>
|
(option) =>
|
||||||
get(option, this.path)
|
option.title
|
||||||
.toString()
|
.toString()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.indexOf(text.toLowerCase()) >= 0
|
.indexOf(this.text.toLowerCase()) >= 0 ||
|
||||||
|
option.slug.toString().toLowerCase().indexOf(this.text.toLowerCase()) >=
|
||||||
|
0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get tagsStrings(): string[] {
|
||||||
|
return (this.value || []).map((tag: ITag) => tag.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
set tagsStrings(tagsStrings: string[]) {
|
||||||
|
const tagEntities = tagsStrings.map((tag: string | ITag) => {
|
||||||
|
if (typeof tag !== "string") {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
return { title: tag, slug: tag } as ITag;
|
||||||
|
});
|
||||||
|
this.$emit("input", tagEntities);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -20,11 +20,17 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<b-select
|
<b-select
|
||||||
|
:aria-label="$t('Language')"
|
||||||
v-if="$i18n"
|
v-if="$i18n"
|
||||||
v-model="locale"
|
v-model="locale"
|
||||||
:placeholder="$t('Select a language')"
|
:placeholder="$t('Select a language')"
|
||||||
>
|
>
|
||||||
<option v-for="(language, lang) in langs" :value="lang" :key="lang">
|
<option
|
||||||
|
v-for="(language, lang) in langs"
|
||||||
|
:value="lang"
|
||||||
|
:key="lang"
|
||||||
|
:selected="isLangSelected(lang)"
|
||||||
|
>
|
||||||
{{ language }}
|
{{ language }}
|
||||||
</option>
|
</option>
|
||||||
</b-select>
|
</b-select>
|
||||||
|
@ -46,6 +52,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
|
rel="external"
|
||||||
hreflang="en"
|
hreflang="en"
|
||||||
href="https://forge.april.org/Chapril/mobilizon.chapril.org-mobilizon/src/branch/chapril/LICENSE"
|
href="https://forge.april.org/Chapril/mobilizon.chapril.org-mobilizon/src/branch/chapril/LICENSE"
|
||||||
>
|
>
|
||||||
|
@ -55,19 +62,25 @@
|
||||||
<li>
|
<li>
|
||||||
<a href="mailto:mobilizon-support@chapril.org">{{ $t("Contact") }}</a>
|
<a href="mailto:mobilizon-support@chapril.org">{{ $t("Contact") }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#navbar">{{ $t("Back to top") }}</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="content has-text-centered">
|
<div class="content has-text-centered">
|
||||||
<i18n
|
<i18n
|
||||||
tag="span"
|
tag="span"
|
||||||
path="Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}."
|
path="Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}."
|
||||||
>
|
>
|
||||||
<a slot="mobilizon" href="https://joinmobilizon.org">{{
|
<a rel="external" slot="mobilizon" href="https://joinmobilizon.org">{{
|
||||||
$t("Mobilizon")
|
$t("Mobilizon")
|
||||||
}}</a>
|
}}</a>
|
||||||
<span slot="date">{{ new Date().getFullYear() }}</span>
|
<span slot="date">{{ new Date().getFullYear() }}</span>
|
||||||
<a href="https://joinmobilizon.org/hall-of-fame" slot="contributors">{{
|
<a
|
||||||
$t("more than 1360 contributors")
|
rel="external"
|
||||||
}}</a>
|
href="https://joinmobilizon.org/hall-of-fame"
|
||||||
|
slot="contributors"
|
||||||
|
>{{ $t("more than 1360 contributors") }}</a
|
||||||
|
>
|
||||||
</i18n>
|
</i18n>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -108,6 +121,10 @@ export default class Footer extends Vue {
|
||||||
this.locale = locale;
|
this.locale = locale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLangSelected(lang: string): boolean {
|
||||||
|
return lang === this.locale;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -152,6 +169,13 @@ footer.footer {
|
||||||
color: $white;
|
color: $white;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-color: $secondary;
|
text-decoration-color: $secondary;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
outline: 3px solid #000;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep span.select {
|
::v-deep span.select {
|
||||||
|
|
|
@ -1,7 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: RouteName.GROUP,
|
||||||
|
params: { preferredUsername: usernameWithDomain(group) },
|
||||||
|
}"
|
||||||
|
class="card"
|
||||||
|
>
|
||||||
|
<div class="card-image">
|
||||||
|
<figure class="image is-16by9">
|
||||||
|
<lazy-image-wrapper
|
||||||
|
:picture="group.banner"
|
||||||
|
style="height: 100%; position: absolute; top: 0; left: 0; width: 100%"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="media">
|
<div class="media mb-2">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-48x48" v-if="group.avatar">
|
<figure class="image is-48x48" v-if="group.avatar">
|
||||||
<img class="is-rounded" :src="group.avatar.url" alt="" />
|
<img class="is-rounded" :src="group.avatar.url" alt="" />
|
||||||
|
@ -9,40 +23,117 @@
|
||||||
<b-icon v-else size="is-large" icon="account-group" />
|
<b-icon v-else size="is-large" icon="account-group" />
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<router-link
|
<h3 class="is-size-5 group-title" dir="auto">
|
||||||
:to="{
|
{{ displayName(group) }}
|
||||||
name: RouteName.GROUP,
|
</h3>
|
||||||
params: { preferredUsername: usernameWithDomain(group) },
|
<span class="is-6 has-text-grey-dark group-federated-username">
|
||||||
}"
|
{{ `@${usernameWithDomain(group)}` }}
|
||||||
>
|
</span>
|
||||||
<h3>{{ group.name }}</h3>
|
|
||||||
<p class="is-6 has-text-grey">
|
|
||||||
<span v-if="group.domain">{{
|
|
||||||
`@${group.preferredUsername}@${group.domain}`
|
|
||||||
}}</span>
|
|
||||||
<span v-else>{{ `@${group.preferredUsername}` }}</span>
|
|
||||||
</p>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="mb-2 line-clamp-3" dir="auto" v-html="group.summary" />
|
||||||
<p>{{ group.summary }}</p>
|
<div>
|
||||||
|
<inline-address
|
||||||
|
class="has-text-grey-dark"
|
||||||
|
v-if="group.physicalAddress && addressFullName(group.physicalAddress)"
|
||||||
|
:physicalAddress="group.physicalAddress"
|
||||||
|
/>
|
||||||
|
<p class="has-text-grey-dark">
|
||||||
|
<b-icon icon="account" />
|
||||||
|
{{
|
||||||
|
$tc(
|
||||||
|
"{count} members or followers",
|
||||||
|
group.members.total + group.followers.total,
|
||||||
|
{
|
||||||
|
count: group.members.total + group.followers.total,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import { IGroup, usernameWithDomain } from "@/types/actor";
|
import { displayName, IGroup, usernameWithDomain } from "@/types/actor";
|
||||||
|
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
import InlineAddress from "@/components/Address/InlineAddress.vue";
|
||||||
|
import { addressFullName } from "@/types/address.model";
|
||||||
|
|
||||||
@Component
|
@Component({
|
||||||
|
components: {
|
||||||
|
LazyImageWrapper,
|
||||||
|
InlineAddress,
|
||||||
|
},
|
||||||
|
})
|
||||||
export default class GroupCard extends Vue {
|
export default class GroupCard extends Vue {
|
||||||
@Prop({ required: true }) group!: IGroup;
|
@Prop({ required: true }) group!: IGroup;
|
||||||
|
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
|
|
||||||
usernameWithDomain = usernameWithDomain;
|
usernameWithDomain = usernameWithDomain;
|
||||||
|
|
||||||
|
displayName = displayName;
|
||||||
|
|
||||||
|
addressFullName = addressFullName;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card {
|
||||||
|
.card-content {
|
||||||
|
padding: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .content {
|
||||||
|
& > *:first-child {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-weight: normal;
|
||||||
|
text-transform: none;
|
||||||
|
font-style: normal;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& > *:not(:first-child) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-left {
|
||||||
|
margin-right: inherit;
|
||||||
|
margin-inline-end: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-content {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.group-title {
|
||||||
|
line-height: 1.5rem;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.group-federated-username {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="identity-header">
|
<div class="identity-header" dir="auto">
|
||||||
<figure class="image is-24x24" v-if="member.actor.avatar">
|
<figure class="image is-24x24" v-if="member.actor.avatar">
|
||||||
<img class="is-rounded" :src="member.actor.avatar.url" alt="" />
|
<img class="is-rounded" :src="member.actor.avatar.url" alt="" />
|
||||||
</figure>
|
</figure>
|
||||||
|
<b-icon v-else icon="account-circle" />
|
||||||
{{ displayNameAndUsername(member.actor) }}
|
{{ displayNameAndUsername(member.actor) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content" dir="auto">
|
||||||
<div>
|
<div>
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
|
@ -15,7 +16,7 @@
|
||||||
</figure>
|
</figure>
|
||||||
<b-icon v-else size="is-large" icon="account-group" />
|
<b-icon v-else size="is-large" icon="account-group" />
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content" dir="auto">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.GROUP,
|
name: RouteName.GROUP,
|
||||||
|
@ -24,12 +25,9 @@
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<h3>{{ member.parent.name }}</h3>
|
<h2>{{ member.parent.name }}</h2>
|
||||||
<p class="is-6 has-text-grey">
|
<p class="is-6 has-text-grey-dark">
|
||||||
<span v-if="member.parent.domain">{{
|
<span>{{ `@${usernameWithDomain(member.parent)}` }}</span>
|
||||||
`@${member.parent.preferredUsername}@${member.parent.domain}`
|
|
||||||
}}</span>
|
|
||||||
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
|
|
||||||
<b-taglist>
|
<b-taglist>
|
||||||
<b-tag
|
<b-tag
|
||||||
type="is-info"
|
type="is-info"
|
||||||
|
@ -47,7 +45,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content" v-if="member.parent.summary">
|
<div class="content" v-if="member.parent.summary">
|
||||||
<p>{{ member.parent.summary }}</p>
|
<p v-html="member.parent.summary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -85,6 +83,7 @@ export default class GroupMemberCard extends Vue {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
.card {
|
.card {
|
||||||
.card-content {
|
.card-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -110,8 +109,9 @@ export default class GroupMemberCard extends Vue {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|
||||||
figure {
|
figure,
|
||||||
padding-right: 3px;
|
span.icon {
|
||||||
|
@include padding-right(3px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ export default class GroupSection extends Vue {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
section {
|
section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -44,7 +45,7 @@ section {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
padding-right: 0.5rem;
|
@include padding-right(0.5rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-slot {
|
.main-slot {
|
||||||
|
@ -68,7 +69,7 @@ div.group-section-title {
|
||||||
|
|
||||||
::v-deep & > a {
|
::v-deep & > a {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin-right: 5px;
|
@include margin-right(5px);
|
||||||
color: var(--title-color);
|
color: var(--title-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,57 +1,65 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="media">
|
<div class="card">
|
||||||
<div class="media-content">
|
<div class="card-content media">
|
||||||
<div class="content">
|
<div class="media-content">
|
||||||
<i18n
|
<div class="content">
|
||||||
tag="p"
|
<i18n
|
||||||
path="You have been invited by {invitedBy} to the following group:"
|
tag="p"
|
||||||
>
|
path="You have been invited by {invitedBy} to the following group:"
|
||||||
<b slot="invitedBy">{{ member.invitedBy.name }}</b>
|
>
|
||||||
</i18n>
|
<b slot="invitedBy">{{ member.invitedBy.name }}</b>
|
||||||
</div>
|
</i18n>
|
||||||
<div class="media subfield">
|
|
||||||
<div class="media-left">
|
|
||||||
<figure class="image is-48x48" v-if="member.parent.avatar">
|
|
||||||
<img class="is-rounded" :src="member.parent.avatar.url" alt="" />
|
|
||||||
</figure>
|
|
||||||
<b-icon v-else size="is-large" icon="account-group" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media subfield">
|
||||||
<div class="level">
|
<div class="media-left">
|
||||||
<div class="level-left">
|
<figure class="image is-48x48" v-if="member.parent.avatar">
|
||||||
<div class="level-item">
|
<img class="is-rounded" :src="member.parent.avatar.url" alt="" />
|
||||||
<router-link
|
</figure>
|
||||||
:to="{
|
<b-icon v-else size="is-large" icon="account-group" />
|
||||||
name: RouteName.GROUP,
|
</div>
|
||||||
params: {
|
<div class="media-content">
|
||||||
preferredUsername: usernameWithDomain(member.parent),
|
<div class="level">
|
||||||
},
|
<div class="level-left">
|
||||||
}"
|
<div class="level-item mr-3">
|
||||||
>
|
<router-link
|
||||||
<h3>{{ member.parent.name }}</h3>
|
:to="{
|
||||||
<p class="is-6 has-text-grey">
|
name: RouteName.GROUP,
|
||||||
<span v-if="member.parent.domain">
|
params: {
|
||||||
{{
|
preferredUsername: usernameWithDomain(member.parent),
|
||||||
`@${member.parent.preferredUsername}@${member.parent.domain}`
|
},
|
||||||
}}
|
}"
|
||||||
</span>
|
>
|
||||||
<span v-else>{{
|
<h3 class="is-size-5">{{ member.parent.name }}</h3>
|
||||||
`@${member.parent.preferredUsername}`
|
<p class="is-size-7 has-text-grey-dark">
|
||||||
}}</span>
|
<span v-if="member.parent.domain">
|
||||||
</p>
|
{{
|
||||||
</router-link>
|
`@${member.parent.preferredUsername}@${member.parent.domain}`
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{
|
||||||
|
`@${member.parent.preferredUsername}`
|
||||||
|
}}</span>
|
||||||
|
</p>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="level-right">
|
||||||
<div class="level-right">
|
<div class="level-item">
|
||||||
<div class="level-item">
|
<b-button
|
||||||
<b-button type="is-success" @click="$emit('accept', member.id)">
|
type="is-success"
|
||||||
{{ $t("Accept") }}
|
@click="$emit('accept', member.id)"
|
||||||
</b-button>
|
>
|
||||||
</div>
|
{{ $t("Accept") }}
|
||||||
<div class="level-item">
|
</b-button>
|
||||||
<b-button type="is-danger" @click="$emit('reject', member.id)">
|
</div>
|
||||||
{{ $t("Decline") }}
|
<div class="level-item">
|
||||||
</b-button>
|
<b-button
|
||||||
|
type="is-danger"
|
||||||
|
@click="$emit('reject', member.id)"
|
||||||
|
>
|
||||||
|
{{ $t("Decline") }}
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -82,4 +90,7 @@ export default class InvitationCard extends Vue {
|
||||||
background: lighten($primary, 40%);
|
background: lighten($primary, 40%);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
h3 {
|
||||||
|
color: $violet-3;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<section v-if="invitations && invitations.length > 0">
|
<section class="card my-3" v-if="invitations && invitations.length > 0">
|
||||||
<InvitationCard
|
<InvitationCard
|
||||||
v-for="member in invitations"
|
v-for="member in invitations"
|
||||||
:key="member.id"
|
:key="member.id"
|
||||||
|
@ -13,8 +13,9 @@
|
||||||
import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
|
import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import InvitationCard from "@/components/Group/InvitationCard.vue";
|
import InvitationCard from "@/components/Group/InvitationCard.vue";
|
||||||
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
|
import { PERSON_STATUS_GROUP } from "@/graphql/actor";
|
||||||
import { IMember } from "@/types/actor/member.model";
|
import { IMember } from "@/types/actor/member.model";
|
||||||
|
import { IGroup, IPerson, usernameWithDomain } from "@/types/actor";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
@ -26,19 +27,26 @@ export default class Invitations extends Vue {
|
||||||
|
|
||||||
async acceptInvitation(id: string): Promise<void> {
|
async acceptInvitation(id: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>(
|
await this.$apollo.mutate<{ acceptInvitation: IMember }>({
|
||||||
{
|
mutation: ACCEPT_INVITATION,
|
||||||
mutation: ACCEPT_INVITATION,
|
variables: {
|
||||||
variables: {
|
id,
|
||||||
id,
|
},
|
||||||
},
|
refetchQueries({ data }) {
|
||||||
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
|
const profile = data?.acceptInvitation?.actor as IPerson;
|
||||||
}
|
const group = data?.acceptInvitation?.parent as IGroup;
|
||||||
);
|
if (profile && group) {
|
||||||
if (data) {
|
return [
|
||||||
this.$emit("accept-invitation", data.acceptInvitation);
|
{
|
||||||
}
|
query: PERSON_STATUS_GROUP,
|
||||||
} catch (error) {
|
variables: { id: profile.id, group: usernameWithDomain(group) },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||||
this.$notifier.error(error.graphQLErrors[0].message);
|
this.$notifier.error(error.graphQLErrors[0].message);
|
||||||
|
@ -48,19 +56,26 @@ export default class Invitations extends Vue {
|
||||||
|
|
||||||
async rejectInvitation(id: string): Promise<void> {
|
async rejectInvitation(id: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>(
|
await this.$apollo.mutate<{ rejectInvitation: IMember }>({
|
||||||
{
|
mutation: REJECT_INVITATION,
|
||||||
mutation: REJECT_INVITATION,
|
variables: {
|
||||||
variables: {
|
id,
|
||||||
id,
|
},
|
||||||
},
|
refetchQueries({ data }) {
|
||||||
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
|
const profile = data?.rejectInvitation?.actor as IPerson;
|
||||||
}
|
const group = data?.rejectInvitation?.parent as IGroup;
|
||||||
);
|
if (profile && group) {
|
||||||
if (data) {
|
return [
|
||||||
this.$emit("reject-invitation", data.rejectInvitation);
|
{
|
||||||
}
|
query: PERSON_STATUS_GROUP,
|
||||||
} catch (error) {
|
variables: { id: profile.id, group: usernameWithDomain(group) },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||||
this.$notifier.error(error.graphQLErrors[0].message);
|
this.$notifier.error(error.graphQLErrors[0].message);
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import RedirectWithAccount from "@/components/Utils/RedirectWithAccount.vue";
|
import RedirectWithAccount from "@/components/Utils/RedirectWithAccount.vue";
|
||||||
import { FETCH_GROUP } from "@/graphql/group";
|
import { FETCH_GROUP } from "@/graphql/group";
|
||||||
import { IGroup } from "@/types/actor";
|
import { displayName, IGroup } from "@/types/actor";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { RedirectWithAccount },
|
components: { RedirectWithAccount },
|
||||||
|
@ -52,7 +52,7 @@ export default class JoinGroupWithAccount extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
get groupTitle(): undefined | string {
|
get groupTitle(): undefined | string {
|
||||||
return this.group?.name || this.group?.preferredUsername;
|
return this.group && displayName(this.group);
|
||||||
}
|
}
|
||||||
|
|
||||||
sentence = this.$t(
|
sentence = this.$t(
|
||||||
|
|
39
js/src/components/Group/MultiGroupCard.vue
Normal file
39
js/src/components/Group/MultiGroupCard.vue
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<div class="multi-card-group">
|
||||||
|
<group-card
|
||||||
|
class="group-card"
|
||||||
|
v-for="group in groups"
|
||||||
|
:group="group"
|
||||||
|
:key="group.id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { IGroup } from "@/types/actor";
|
||||||
|
import { PropType } from "vue";
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import GroupCard from "./GroupCard.vue";
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
GroupCard,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class MultiGroupCard extends Vue {
|
||||||
|
@Prop({ type: Array as PropType<IGroup[]>, required: true })
|
||||||
|
groups!: IGroup[];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.multi-card-group {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-rows: 1fr;
|
||||||
|
grid-column-gap: 30px;
|
||||||
|
grid-row-gap: 30px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
.group-card {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -83,11 +83,13 @@
|
||||||
/></a>
|
/></a>
|
||||||
<a
|
<a
|
||||||
:href="telegramShareUrl"
|
:href="telegramShareUrl"
|
||||||
|
class="telegram"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="nofollow noopener"
|
rel="nofollow noopener"
|
||||||
title="Telegram"
|
title="Telegram"
|
||||||
><b-icon icon="telegram" size="is-large" type="is-primary"
|
>
|
||||||
/></a>
|
<telegram-logo />
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
title="Diaspora"
|
title="Diaspora"
|
||||||
:href="diasporaShareUrl"
|
:href="diasporaShareUrl"
|
||||||
|
@ -115,12 +117,14 @@ import { Component, Prop, Vue, Ref } from "vue-property-decorator";
|
||||||
import { GroupVisibility } from "@/types/enums";
|
import { GroupVisibility } from "@/types/enums";
|
||||||
import DiasporaLogo from "../Share/DiasporaLogo.vue";
|
import DiasporaLogo from "../Share/DiasporaLogo.vue";
|
||||||
import MastodonLogo from "../Share/MastodonLogo.vue";
|
import MastodonLogo from "../Share/MastodonLogo.vue";
|
||||||
|
import TelegramLogo from "../Share/TelegramLogo.vue";
|
||||||
import { displayName, IGroup } from "@/types/actor";
|
import { displayName, IGroup } from "@/types/actor";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
DiasporaLogo,
|
DiasporaLogo,
|
||||||
MastodonLogo,
|
MastodonLogo,
|
||||||
|
TelegramLogo,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class ShareGroupModal extends Vue {
|
export default class ShareGroupModal extends Vue {
|
||||||
|
@ -173,7 +177,7 @@ export default class ShareGroupModal extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
get mastodonShareUrl(): string {
|
get mastodonShareUrl(): string {
|
||||||
return `https://toot.karamoff.dev/?text=${encodeURIComponent(
|
return `https://toot.kytta.dev/?text=${encodeURIComponent(
|
||||||
this.basicTextToEncode
|
this.basicTextToEncode
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
@ -194,7 +198,8 @@ export default class ShareGroupModal extends Vue {
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.diaspora,
|
.diaspora,
|
||||||
.mastodon {
|
.mastodon,
|
||||||
|
.telegram {
|
||||||
::v-deep span svg {
|
::v-deep span svg {
|
||||||
width: 2.25rem;
|
width: 2.25rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,9 @@
|
||||||
:width="width"
|
:width="width"
|
||||||
:height="height"
|
:height="height"
|
||||||
class="absolute top-0 left-0 transition-opacity duration-500"
|
class="absolute top-0 left-0 transition-opacity duration-500"
|
||||||
:class="isLoaded ? 'opacity-100' : 'opacity-0'"
|
:class="{ isLoaded: isLoaded ? 'opacity-100' : 'opacity-0', rounded }"
|
||||||
alt=""
|
alt=""
|
||||||
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,6 +38,7 @@ export default class LazyImage extends Vue {
|
||||||
@Prop({ type: String, required: false, default: null }) blurhash!: string;
|
@Prop({ type: String, required: false, default: null }) blurhash!: string;
|
||||||
@Prop({ type: Number, default: 1 }) width!: number;
|
@Prop({ type: Number, default: 1 }) width!: number;
|
||||||
@Prop({ type: Number, default: 1 }) height!: number;
|
@Prop({ type: Number, default: 1 }) height!: number;
|
||||||
|
@Prop({ type: Boolean, default: false }) rounded!: boolean;
|
||||||
|
|
||||||
inheritAttrs = false;
|
inheritAttrs = false;
|
||||||
isLoaded = false;
|
isLoaded = false;
|
||||||
|
@ -63,12 +65,14 @@ export default class LazyImage extends Vue {
|
||||||
onEnter(): void {
|
onEnter(): void {
|
||||||
// Image is visible (means: has entered the viewport),
|
// Image is visible (means: has entered the viewport),
|
||||||
// so start loading by setting the src attribute
|
// so start loading by setting the src attribute
|
||||||
this.image.src = this.src;
|
if (this.image) {
|
||||||
|
this.image.src = this.src;
|
||||||
|
|
||||||
this.image.onload = () => {
|
this.image.onload = () => {
|
||||||
// Image is loaded, so start fading in
|
// Image is loaded, so start fading in
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch("src")
|
@Watch("src")
|
||||||
|
@ -113,5 +117,8 @@ img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: 50% 50%;
|
object-position: 50% 50%;
|
||||||
|
&.rounded {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
:width="pictureOrDefault.metadata.width"
|
:width="pictureOrDefault.metadata.width"
|
||||||
:height="pictureOrDefault.metadata.height"
|
:height="pictureOrDefault.metadata.height"
|
||||||
:blurhash="pictureOrDefault.metadata.blurhash"
|
:blurhash="pictureOrDefault.metadata.blurhash"
|
||||||
|
:rounded="rounded"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -34,6 +35,7 @@ const DEFAULT_PICTURE = {
|
||||||
export default class LazyImageWrapper extends Vue {
|
export default class LazyImageWrapper extends Vue {
|
||||||
@Prop({ required: false, type: Object as PropType<IMedia | null> })
|
@Prop({ required: false, type: Object as PropType<IMedia | null> })
|
||||||
picture!: IMedia | null;
|
picture!: IMedia | null;
|
||||||
|
@Prop({ required: false, type: Boolean, default: false }) rounded!: boolean;
|
||||||
|
|
||||||
get pictureOrDefault(): Partial<IMedia> {
|
get pictureOrDefault(): Partial<IMedia> {
|
||||||
if (this.picture === null) {
|
if (this.picture === null) {
|
||||||
|
|
|
@ -19,7 +19,10 @@
|
||||||
:zoomInTitle="$t('Zoom in')"
|
:zoomInTitle="$t('Zoom in')"
|
||||||
:zoomOutTitle="$t('Zoom out')"
|
:zoomOutTitle="$t('Zoom out')"
|
||||||
></l-control-zoom>
|
></l-control-zoom>
|
||||||
<v-locatecontrol :options="{ icon: 'mdi mdi-map-marker' }" />
|
<v-locatecontrol
|
||||||
|
v-if="canDoGeoLocation"
|
||||||
|
:options="{ icon: 'mdi mdi-map-marker' }"
|
||||||
|
/>
|
||||||
<l-marker
|
<l-marker
|
||||||
:lat-lng="[lat, lon]"
|
:lat-lng="[lat, lon]"
|
||||||
@add="openPopup"
|
@add="openPopup"
|
||||||
|
@ -139,7 +142,9 @@ export default class Map extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDraggableMarkerPosition(e: LatLng): void {
|
updateDraggableMarkerPosition(e: LatLng): void {
|
||||||
this.updateDraggableMarkerCallback(e, this.zoom);
|
if (this.updateDraggableMarkerCallback) {
|
||||||
|
this.updateDraggableMarkerCallback(e, this.zoom);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateZoom(zoom: number): void {
|
updateZoom(zoom: number): void {
|
||||||
|
@ -152,6 +157,10 @@ export default class Map extends Vue {
|
||||||
(this.$t("© The OpenStreetMap Contributors") as string)
|
(this.$t("© The OpenStreetMap Contributors") as string)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canDoGeoLocation(): boolean {
|
||||||
|
return window.isSecureContext;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default class Vue2LeafletLocateControl extends Vue {
|
||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@Prop({ type: Boolean, default: true }) visible = true;
|
@Prop({ type: Boolean, default: true }) visible!: boolean;
|
||||||
|
|
||||||
ready = false;
|
ready = false;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<b-navbar
|
<b-navbar
|
||||||
|
id="navbar"
|
||||||
type="is-secondary"
|
type="is-secondary"
|
||||||
wrapper-class="container"
|
wrapper-class="container"
|
||||||
:active.sync="mobileNavbarActive"
|
:active.sync="mobileNavbarActive"
|
||||||
|
@ -48,26 +49,13 @@
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<b-button
|
<b-button
|
||||||
|
v-if="!hideCreateEventsButton"
|
||||||
tag="router-link"
|
tag="router-link"
|
||||||
:to="{ name: RouteName.CREATE_EVENT }"
|
:to="{ name: RouteName.CREATE_EVENT }"
|
||||||
type="is-primary"
|
type="is-primary"
|
||||||
>{{ $t("Create") }}</b-button
|
>{{ $t("Create") }}</b-button
|
||||||
>
|
>
|
||||||
</b-navbar-item>
|
</b-navbar-item>
|
||||||
<b-navbar-item
|
|
||||||
v-if="config && config.features.koenaConnect"
|
|
||||||
class="koena"
|
|
||||||
tag="a"
|
|
||||||
href="https://mediation.koena.net/framasoft/mobilizon/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/img/koena-a11y.svg"
|
|
||||||
width="150"
|
|
||||||
alt="Contact accessibilité"
|
|
||||||
/>
|
|
||||||
</b-navbar-item>
|
|
||||||
</template>
|
</template>
|
||||||
<template slot="end">
|
<template slot="end">
|
||||||
<b-navbar-item
|
<b-navbar-item
|
||||||
|
@ -87,12 +75,12 @@
|
||||||
v-if="currentActor.id && currentUser.isLoggedIn"
|
v-if="currentActor.id && currentUser.isLoggedIn"
|
||||||
right
|
right
|
||||||
collapsible
|
collapsible
|
||||||
|
ref="user-dropdown"
|
||||||
|
tabindex="0"
|
||||||
|
tag="span"
|
||||||
|
@keyup.enter="toggleMenu"
|
||||||
>
|
>
|
||||||
<template
|
<template slot="label" v-if="currentActor">
|
||||||
slot="label"
|
|
||||||
v-if="currentActor"
|
|
||||||
class="navbar-dropdown-profile"
|
|
||||||
>
|
|
||||||
<div class="identity-wrapper">
|
<div class="identity-wrapper">
|
||||||
<div>
|
<div>
|
||||||
<figure class="image is-32x32" v-if="currentActor.avatar">
|
<figure class="image is-32x32" v-if="currentActor.avatar">
|
||||||
|
@ -121,8 +109,11 @@
|
||||||
v-else
|
v-else
|
||||||
:active="identity.id === currentActor.id"
|
:active="identity.id === currentActor.id"
|
||||||
:key="identity.id"
|
:key="identity.id"
|
||||||
|
tabindex="0"
|
||||||
|
@click="setIdentity(identity)"
|
||||||
|
@keyup.enter="setIdentity(identity)"
|
||||||
>
|
>
|
||||||
<span @click="setIdentity(identity)">
|
<span>
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-32x32" v-if="identity.avatar">
|
<figure class="image is-32x32" v-if="identity.avatar">
|
||||||
<img
|
<img
|
||||||
|
@ -143,7 +134,7 @@
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<hr class="navbar-divider" />
|
<hr class="navbar-divider" role="presentation" />
|
||||||
</b-navbar-item>
|
</b-navbar-item>
|
||||||
|
|
||||||
<b-navbar-item
|
<b-navbar-item
|
||||||
|
@ -158,8 +149,13 @@
|
||||||
>{{ $t("Administration") }}
|
>{{ $t("Administration") }}
|
||||||
</b-navbar-item>
|
</b-navbar-item>
|
||||||
|
|
||||||
<b-navbar-item tag="span">
|
<b-navbar-item
|
||||||
<span @click="logout">{{ $t("Log out") }}</span>
|
tag="span"
|
||||||
|
tabindex="0"
|
||||||
|
@click="logout"
|
||||||
|
@keyup.enter="logout"
|
||||||
|
>
|
||||||
|
<span>{{ $t("Log out") }}</span>
|
||||||
</b-navbar-item>
|
</b-navbar-item>
|
||||||
</b-navbar-dropdown>
|
</b-navbar-dropdown>
|
||||||
|
|
||||||
|
@ -185,7 +181,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
|
||||||
import Logo from "@/components/Logo.vue";
|
import Logo from "@/components/Logo.vue";
|
||||||
import { GraphQLError } from "graphql";
|
import { GraphQLError } from "graphql";
|
||||||
import { loadLanguageAsync } from "@/utils/i18n";
|
import { loadLanguageAsync } from "@/utils/i18n";
|
||||||
|
@ -206,12 +202,8 @@ import RouteName from "../router/name";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
apollo: {
|
apollo: {
|
||||||
currentUser: {
|
currentUser: CURRENT_USER_CLIENT,
|
||||||
query: CURRENT_USER_CLIENT,
|
currentActor: CURRENT_ACTOR_CLIENT,
|
||||||
},
|
|
||||||
currentActor: {
|
|
||||||
query: CURRENT_ACTOR_CLIENT,
|
|
||||||
},
|
|
||||||
identities: {
|
identities: {
|
||||||
query: IDENTITIES,
|
query: IDENTITIES,
|
||||||
update: ({ identities }) =>
|
update: ({ identities }) =>
|
||||||
|
@ -257,6 +249,13 @@ export default class NavBar extends Vue {
|
||||||
|
|
||||||
displayName = displayName;
|
displayName = displayName;
|
||||||
|
|
||||||
|
@Ref("user-dropdown") userDropDown!: any;
|
||||||
|
|
||||||
|
toggleMenu(): void {
|
||||||
|
console.debug("called toggleMenu");
|
||||||
|
this.userDropDown.showMenu();
|
||||||
|
}
|
||||||
|
|
||||||
@Watch("currentActor")
|
@Watch("currentActor")
|
||||||
async initializeListOfIdentities(): Promise<void> {
|
async initializeListOfIdentities(): Promise<void> {
|
||||||
if (!this.currentUser.isLoggedIn) return;
|
if (!this.currentUser.isLoggedIn) return;
|
||||||
|
@ -271,6 +270,11 @@ export default class NavBar extends Vue {
|
||||||
// If we don't have any identities, the user has validated their account,
|
// If we don't have any identities, the user has validated their account,
|
||||||
// is logging for the first time but didn't create an identity somehow
|
// is logging for the first time but didn't create an identity somehow
|
||||||
if (this.identities.length === 0) {
|
if (this.identities.length === 0) {
|
||||||
|
console.debug(
|
||||||
|
"We have no identities listed for current user",
|
||||||
|
this.identities
|
||||||
|
);
|
||||||
|
console.debug("Pushing route to REGISTER_PROFILE");
|
||||||
try {
|
try {
|
||||||
await this.$router.push({
|
await this.$router.push({
|
||||||
name: RouteName.REGISTER_PROFILE,
|
name: RouteName.REGISTER_PROFILE,
|
||||||
|
@ -326,9 +330,14 @@ export default class NavBar extends Vue {
|
||||||
});
|
});
|
||||||
return changeIdentity(this.$apollo.provider.defaultClient, identity);
|
return changeIdentity(this.$apollo.provider.defaultClient, identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hideCreateEventsButton(): boolean {
|
||||||
|
return !!this.config?.restrictions?.onlyGroupsCanCreateEvents;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/_mixins" as *;
|
||||||
nav {
|
nav {
|
||||||
.navbar-item {
|
.navbar-item {
|
||||||
a.button {
|
a.button {
|
||||||
|
@ -361,7 +370,7 @@ nav {
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-item.has-dropdown a.navbar-link figure {
|
.navbar-item.has-dropdown a.navbar-link figure {
|
||||||
margin-right: 0.75rem;
|
@include margin-right(0.75rem);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
@ -370,15 +379,6 @@ nav {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.koena {
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
& > img {
|
|
||||||
max-height: 4rem;
|
|
||||||
padding-top: 0.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.identity-wrapper {
|
.identity-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,13 @@ import { IParticipant } from "../../types/participant.model";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
import { CONFIRM_PARTICIPATION } from "../../graphql/event";
|
import { CONFIRM_PARTICIPATION } from "../../graphql/event";
|
||||||
|
|
||||||
@Component
|
@Component({
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Confirm participation") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
export default class ConfirmParticipation extends Vue {
|
export default class ConfirmParticipation extends Vue {
|
||||||
@Prop({ type: String, required: true }) token!: string;
|
@Prop({ type: String, required: true }) token!: string;
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,7 @@
|
||||||
<b-modal
|
<b-modal
|
||||||
:active.sync="isAnonymousParticipationModalOpen"
|
:active.sync="isAnonymousParticipationModalOpen"
|
||||||
has-modal-card
|
has-modal-card
|
||||||
|
:close-button-aria-label="$t('Close')"
|
||||||
ref="anonymous-participation-modal"
|
ref="anonymous-participation-modal"
|
||||||
>
|
>
|
||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user