Compare commits

...

219 Commits

Author SHA1 Message Date
247c76e038 Merge branch 'upstream_1.0.3' into chapril 2021-01-10 10:36:16 +01:00
Thomas Citharel
a4b41efe49 Merge branch '1.0.3' into 'master'
1.0.3

See merge request framasoft/mobilizon!768
2020-12-18 17:13:58 +01:00
Thomas Citharel
425b28a426
Release 1.0.3
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-18 16:48:06 +01:00
Thomas Citharel
53ed6b43e2
Upgrade deps
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-18 16:48:05 +01:00
Thomas Citharel
fba16502cf
Remove leftover
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-18 16:48:05 +01:00
Thomas Citharel
766b452640
Filter out cancelled events on homepage
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-18 16:48:04 +01:00
Thomas Citharel
ad0086032b
Fix tests with events listing
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-18 16:48:04 +01:00
Thomas Citharel
d0ec74645f Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!769
2020-12-18 11:45:22 +01:00
Berto Te
19a61ddd12 Translated using Weblate (Spanish)
Currently translated at 100.0% (825 of 825 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/es/
2020-12-18 11:43:03 +01:00
Thomas Citharel
69355b4d2e Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!767
2020-12-17 17:53:55 +01:00
Taru Luojola
eb7ae2f4a4 Translated using Weblate (Finnish)
Currently translated at 100.0% (825 of 825 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/fi/
2020-12-17 17:33:23 +01:00
Thomas Citharel
3635967439
Fix posts AP endpoint
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-17 17:32:59 +01:00
Thomas Citharel
f7d064c022
Handle Hubzilla posts better
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-17 17:32:12 +01:00
Thomas Citharel
5e7bcc44df
Fix events query
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-17 16:57:55 +01:00
Thomas Citharel
ac8a487327
Fix an error message
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-17 16:53:08 +01:00
Thomas Citharel
f9e14c3a93
Fix events being not distinct
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-17 16:52:57 +01:00
Thomas Citharel
4f7faf4f4c
Fix leftover
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-17 16:01:18 +01:00
Thomas Citharel
4e9e658001 Merge branch 'fix-group-being-available-in-search' into 'master'
Fix group being available in search

See merge request framasoft/mobilizon!765
2020-12-17 15:57:14 +01:00
Thomas Citharel
a646d4a40a
Fix unlisted groups being available in search
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-17 15:38:03 +01:00
Thomas Citharel
da564078b3
Add a missing option to a command comment
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-17 15:25:43 +01:00
Thomas Citharel
3e489cfdcf Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!764
2020-12-17 11:49:37 +01:00
Kate
80a5086628 Translated using Weblate (German)
Currently translated at 97.0% (163 of 168 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/de/
2020-12-17 11:26:32 +01:00
Kate
bfc383177b Translated using Weblate (German)
Currently translated at 100.0% (238 of 238 strings)

Translation: Mobilizon/Backend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend/de/
2020-12-17 11:26:32 +01:00
Kate
51168fd82d Translated using Weblate (German)
Currently translated at 99.5% (818 of 822 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/de/
2020-12-17 11:26:32 +01:00
Thomas Citharel
b0d831e4e5 Merge branch 'routing-link-openstreetmap' into 'master'
Routing link openstreetmap

See merge request framasoft/mobilizon!757
2020-12-17 11:26:26 +01:00
ty kayn
5c57f1ce3c if an event has geo coordinates, add links to routing on OSM, with correct place and zoom of 14, 3 buttons to get routig as car, bike, and by feet.
Signed-off-by: Baptiste Lemoine <contact@cipherbliss.com>
2020-12-17 11:26:25 +01:00
Thomas Citharel
c8fb5bb80e Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!762
2020-12-16 14:51:19 +01:00
Kate
165ca65015 Translated using Weblate (German)
Currently translated at 51.7% (87 of 168 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/de/
2020-12-16 14:29:40 +01:00
Kate
ce541bf72b Translated using Weblate (German)
Currently translated at 100.0% (238 of 238 strings)

Translation: Mobilizon/Backend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend/de/
2020-12-16 14:29:40 +01:00
Kate
278beb83b9 Translated using Weblate (German)
Currently translated at 97.4% (801 of 822 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/de/
2020-12-16 14:29:40 +01:00
Kate
c6d7654f63 Translated using Weblate (German)
Currently translated at 7.1% (12 of 168 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/de/
2020-12-16 14:29:40 +01:00
Kate
1cc65ec298 Translated using Weblate (German)
Currently translated at 76.4% (182 of 238 strings)

Translation: Mobilizon/Backend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend/de/
2020-12-16 14:29:40 +01:00
Thomas Citharel
6833d79611 Merge branch 'save-remote-pics' into 'master'
Save remote pics

See merge request framasoft/mobilizon!763
2020-12-16 14:29:32 +01:00
Thomas Citharel
273c98cfdf
Fix tests
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-16 14:03:33 +01:00
Thomas Citharel
d1472d94de
Expose posts media through AP
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-16 09:54:56 +01:00
Thomas Citharel
4e7ab231ad
Allow data-media-id attribute in img tags
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-16 09:54:09 +01:00
Thomas Citharel
af98045d14
Fix post edit dropping pictures
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-16 09:53:43 +01:00
Thomas Citharel
9b27e70eb0
Save remote profiles avatars & banners locally
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-15 17:17:42 +01:00
Thomas Citharel
23124aa381 Merge branch 'deps' into 'master'
Deps and lint stuff

See merge request framasoft/mobilizon!761
2020-12-14 10:42:46 +01:00
Thomas Citharel
ae03f84950
Add yarn lint back to CI
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-14 10:21:33 +01:00
Thomas Citharel
8dc5b8a4b0
Fix lint issues
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-14 10:21:04 +01:00
Thomas Citharel
594d5a91ec
Upgrade deps
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-14 09:23:29 +01:00
Thomas Citharel
a9f47d4c45 Merge branch 'fix-docker-build' into 'master'
Fix Docker build

See merge request framasoft/mobilizon!760
2020-12-13 14:57:38 +01:00
Thomas Citharel
8c161c01af Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!759
2020-12-13 14:39:29 +01:00
Thomas Citharel
5d30ba9380
Fix Docker build
Add webp support to the Docker build and remove scripty

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-13 14:38:04 +01:00
Balázs Úr
08f0aadd69 Translated using Weblate (Hungarian)
Currently translated at 100.0% (822 of 822 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/hu/
2020-12-12 00:52:17 +01:00
josé m
95cfc1658b Translated using Weblate (Galician)
Currently translated at 99.8% (821 of 822 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/gl/
2020-12-12 00:52:16 +01:00
Taru Luojola
1b5845dd24 Translated using Weblate (Finnish)
Currently translated at 100.0% (822 of 822 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/fi/
2020-12-12 00:52:15 +01:00
Berto Te
e54be5f4a7 Translated using Weblate (Spanish)
Currently translated at 100.0% (822 of 822 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/es/
2020-12-12 00:52:14 +01:00
Thomas Citharel
6509ed44fa Merge branch 'front-improvements' into 'master'
Front improvements

Closes #497

See merge request framasoft/mobilizon!758
2020-12-11 16:05:25 +01:00
Thomas Citharel
c43aeb8a3e
Update CI
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-11 15:44:48 +01:00
Thomas Citharel
71f1701ce8
Add back pwa support
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-11 15:27:31 +01:00
Thomas Citharel
6a52ca0d91
Produce and use webp pictures with different sizes
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-11 15:27:04 +01:00
Thomas Citharel
c8987df7af Merge branch 'fix-edit-event-time-reset' into 'master'
Use direct datetime picker from Buefy

Closes #207, #459 et #494

See merge request framasoft/mobilizon!756
2020-12-10 12:33:28 +01:00
Thomas Citharel
736020392b
Use direct datetime picker from Buefy
Closes #494
Closes #459
Closes #207

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-10 12:16:35 +01:00
Thomas Citharel
9d6c1867af Merge branch 'luc/mobilizon-iamdoubz-apache2-reverse-proxy' into 'master'
Luc/mobilizon iamdoubz apache2 reverse proxy

See merge request framasoft/mobilizon!755
2020-12-10 11:35:31 +01:00
Thomas Citharel
1d9bf70e5e
Add websocket support to Apache vhost template
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-10 11:34:42 +01:00
iamdoubz
8b95a9a0b1
Add apache2 reverse proxy configuration file 2020-12-10 11:34:33 +01:00
Thomas Citharel
118aabf544
Merge branch 'LeoMouyna/mobilizon-297-password-reset-form' 2020-12-10 09:42:49 +01:00
Leo Mouyna
5e3eb00708
fix: improve reset password form.
See issue #297
2020-12-10 09:42:27 +01:00
Thomas Citharel
080432dbe5 Merge branch 'trim-federation-add-instance' into 'master'
 trim new relay address string to fix copy and paste domain name spaces and tabs #537

Closes #537

See merge request framasoft/mobilizon!752
2020-12-10 09:05:08 +01:00
Thomas Citharel
5f3531cc18 Merge branch 'fix-nav-search-field' into 'master'
Fix nav search field

Closes #450

See merge request framasoft/mobilizon!753
2020-12-09 20:08:39 +01:00
Thomas Citharel
86f0409e5d
Merge branch 'chagai95-master' 2020-12-09 20:04:30 +01:00
chagai95
76f438ced7
Riot is the old name... 2020-12-09 20:04:19 +01:00
Thomas Citharel
5f936c62a1 Merge branch 'suggest-creation-when-no-event' into 'master'
Suggest creation when no event

Closes #290

See merge request framasoft/mobilizon!754
2020-12-09 20:02:13 +01:00
Thomas Citharel
fff94580c8
Improve search view
Closes #450

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-09 19:55:46 +01:00
Thomas Citharel
848c18470c
Handle doing a paginated query on ordered_by results
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-09 19:55:45 +01:00
Thomas Citharel
8ebef3296b
Update schema.graphql
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-09 19:55:44 +01:00
Thomas Citharel
8e722032fb
[GraphQL] Move events endpoint to paginated event list
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-09 19:55:44 +01:00
Thomas Citharel
71854ec7b7
Improve my events / my groups when there's no content
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-09 19:09:31 +01:00
9de04695d9
add suggest create button when there is not any one found on 'my event' page 2020-12-09 18:09:57 +01:00
44d597a118 trim new relay address string to fix copy and paste domain name spaces and tabs #537 2020-12-09 15:34:49 +01:00
Thomas Citharel
cae4062b4e Merge branch 'update-deps' into 'master'
Update deps

See merge request framasoft/mobilizon!751
2020-12-09 11:02:15 +01:00
Thomas Citharel
79b52c1f10
Update deps
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-09 10:40:40 +01:00
Thomas Citharel
a0db8aedfb Merge branch 'fix-group-events-past' into 'master'
Fix group events past

Closes #492

See merge request framasoft/mobilizon!750
2020-12-09 10:24:45 +01:00
Thomas Citharel
6c0ee2446a
Group enhancements
And fixes #492

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-09 10:01:37 +01:00
Thomas Citharel
cadc741d99
Add tests for GroupSection
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-09 10:00:10 +01:00
Thomas Citharel
16fd2d0a2c
Add Vetur file configuration
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-09 10:00:09 +01:00
Thomas Citharel
fd9878955a Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!749
2020-12-09 09:59:46 +01:00
Eivind Ødegård
1c90b4706b Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (168 of 168 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/nn/
2020-12-09 09:52:17 +01:00
Eivind Ødegård
c41037b046 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (812 of 812 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/nn/
2020-12-09 09:52:16 +01:00
josé m
0983a51f3f Translated using Weblate (Galician)
Currently translated at 99.8% (811 of 812 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/gl/
2020-12-09 09:52:15 +01:00
gohoso9454
076dd5b83b Translated using Weblate (Swedish)
Currently translated at 31.9% (76 of 238 strings)

Translation: Mobilizon/Backend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend/sv/
2020-12-09 09:52:14 +01:00
Taru Luojola
e645bc0417 Translated using Weblate (Finnish)
Currently translated at 100.0% (812 of 812 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/fi/
2020-12-09 09:52:14 +01:00
Berto Te
622bbd29e2 Translated using Weblate (Spanish)
Currently translated at 100.0% (812 of 812 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/es/
2020-12-09 09:52:13 +01:00
Thomas Citharel
95efa43325 Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!745
2020-12-07 16:28:31 +01:00
diorama
8e12a1fc98 Translated using Weblate (Italian)
Currently translated at 100.0% (168 of 168 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/it/
2020-12-07 16:13:09 +01:00
diorama
75af571399 Translated using Weblate (Italian)
Currently translated at 100.0% (812 of 812 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/it/
2020-12-07 16:13:09 +01:00
Thomas Citharel
fe81771658 Merge branch 'add-tests-for-participation-without-account' into 'master'
Add tests for participation without account

Closes #533

See merge request framasoft/mobilizon!744
2020-12-07 16:13:02 +01:00
Thomas Citharel
d35ccff5a1
Add tests for participation without account
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-07 15:55:49 +01:00
Thomas Citharel
c0a367a014 Merge branch 'master' into 'master'
Accord du COD

See merge request framasoft/mobilizon!743
2020-12-06 15:49:33 +01:00
vincent debierre
11825b612c Accord du COD 2020-12-06 12:56:30 +01:00
Thomas Citharel
87effa0e9f Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!742
2020-12-06 09:49:24 +01:00
josé m
42d0817252 Translated using Weblate (Galician)
Currently translated at 99.8% (811 of 812 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/gl/
2020-12-06 07:52:14 +01:00
Taru Luojola
fdaecbd4ae Translated using Weblate (Finnish)
Currently translated at 100.0% (812 of 812 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/fi/
2020-12-06 07:52:13 +01:00
Berto Te
b8dabf6d6f Translated using Weblate (Spanish)
Currently translated at 100.0% (812 of 812 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/es/
2020-12-06 07:52:12 +01:00
Thomas Citharel
39cf73dba8 Merge branch 'improve-participation-button' into 'master'
Improve participation button

Closes #470

See merge request framasoft/mobilizon!741
2020-12-04 17:04:04 +01:00
Thomas Citharel
4ea484ea14
Update locale files
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-04 16:53:55 +01:00
Thomas Citharel
ee849e55c1
Improve participation section and test
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-04 16:44:37 +01:00
Thomas Citharel
96938a5511
Refactor the participation section for an event
And add a test for this new section

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-04 15:12:41 +01:00
Thomas Citharel
c94e431618
Improve "AboutInstance" page load
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-04 15:12:40 +01:00
Thomas Citharel
b4229e2e09
Fix logo component
Directly inline the svg source

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-04 15:12:39 +01:00
Thomas Citharel
d9cc9f5842
Improve the message when loading comments below event
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-04 15:07:27 +01:00
Thomas Citharel
10826650e7
Fix participation without account
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-04 09:40:57 +01:00
Thomas Citharel
88509f0ac1 Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!740
2020-12-04 09:06:39 +01:00
Quentin PAGÈS
3611014721 Translated using Weblate (Occitan)
Currently translated at 100.0% (802 of 802 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/oc/
2020-12-03 18:52:11 +01:00
Thomas Citharel
b0ab0af9ea Merge branch 'new-vue-components-tested' into 'master'
Added new vue components tested

See merge request framasoft/mobilizon!739
2020-12-03 17:39:57 +01:00
Thomas Citharel
d88427f816
Added new vue components tested
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-03 17:22:05 +01:00
Thomas Citharel
c1c9f421e0 Merge branch 'js-unit-tests' into 'master'
Introduce basic js unit tests

See merge request framasoft/mobilizon!738
2020-12-02 15:51:46 +01:00
Thomas Citharel
2f25fa0ca6
Introduce basic js unit tests
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-02 12:08:32 +01:00
Thomas Citharel
88cba1629d Merge branch 'show-identity-on-group-card' into 'master'
Show identity on group card

Closes #473 et #415

See merge request framasoft/mobilizon!737
2020-12-01 17:57:10 +01:00
Thomas Citharel
4e27d04569
Handle tags on eventlistcard properly
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-01 17:42:09 +01:00
Thomas Citharel
cb4b251c77
Simplify event list card with a dropdown, load group attribution and
show identity for participation

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-01 17:42:08 +01:00
Thomas Citharel
0541fb0ada
Allow event's attributedTo to use the Dataloader
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-01 17:42:07 +01:00
Thomas Citharel
06050c9cd6
Fix leftover
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-01 17:42:07 +01:00
Thomas Citharel
f642113070
Show identity for each membership in MyGroups
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-01 16:14:53 +01:00
Thomas Citharel
01c86a1cb9 Merge branch 'show-cancelled-status' into 'master'
Show cancelled status

Closes #478

See merge request framasoft/mobilizon!736
2020-12-01 12:49:54 +01:00
Thomas Citharel
4b021f50e5
Clearer logic whether to show or not the participation button
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-01 12:35:05 +01:00
Thomas Citharel
a76917b8e1
Fix participation
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-01 12:35:03 +01:00
Thomas Citharel
9683df9040
Show cancelled status on cancelled events
Closes #478

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-01 12:35:02 +01:00
Thomas Citharel
2a9e95bf7f Merge branch 'fix-member-notification-language' into 'master'
Send the membership emails in the correct language

Closes #472

See merge request framasoft/mobilizon!735
2020-12-01 12:29:20 +01:00
Thomas Citharel
6d8710f0fe
Send the membership emails in the correct language
And send them as well if the member is on the same instance 🙈

Close #472

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-01 10:21:07 +01:00
Thomas Citharel
1ed6fbae4e Merge branch 'show-draft-status-in-admin' into 'master'
Show draft status in admin

Closes #413

See merge request framasoft/mobilizon!734
2020-12-01 10:16:02 +01:00
Thomas Citharel
8140f5e227
Fix mediaSize being requested for every group
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-01 09:56:14 +01:00
Thomas Citharel
68d4dc3301
Remove leftovers
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-01 09:56:05 +01:00
Thomas Citharel
ac8856f08c
Show draft status on events and posts in group admin
Closes #413

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-01 09:55:24 +01:00
Thomas Citharel
def6df690d Merge branch 'ldap-full-admin-dn' into 'master'
Ldap full admin dn

Closes #528

See merge request framasoft/mobilizon!733
2020-12-01 09:32:18 +01:00
Thomas Citharel
d6d9309784
[LDAP] Allow to filter users by memberOf
Closes #528

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-01 09:16:41 +01:00
Thomas Citharel
0f5941a046
[LDAP] Allow to bind to an admin with a different FQDN
By directly providing the full DN

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-12-01 09:16:28 +01:00
Thomas Citharel
bc8ea10bb0 Merge branch 'add-language-unlogged' into 'master'
Add language unlogged

Closes #479 et #375

See merge request framasoft/mobilizon!732
2020-11-30 19:25:06 +01:00
Thomas Citharel
c39a771fd5 Merge branch 'upgrade-deps' into 'master'
Upgrade deps

See merge request framasoft/mobilizon!731
2020-11-30 18:34:45 +01:00
Thomas Citharel
005fb90556
Allow to pick language unlogged and format fallback messages
Closes #479

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-30 18:14:50 +01:00
Thomas Citharel
2141f92a30
Fix tests
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-30 16:46:26 +01:00
Thomas Citharel
207d5c0eb0
Use better upstream deps
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-30 12:48:23 +01:00
Thomas Citharel
b05f0fe3e6
simplify user resolver errors
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-30 12:48:22 +01:00
Thomas Citharel
2d541f2e32
Fix lint issues
And disable eslint when building in prod mode

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-30 12:48:21 +01:00
Thomas Citharel
da42522073
Fix eslint warnings
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-30 12:48:21 +01:00
Thomas Citharel
487ac56b4c
Upgrade deps
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-30 12:48:20 +01:00
Thomas Citharel
ac112b64c2 Merge branch 'fix-tests' into 'master'
Fix tests

Closes #390

See merge request framasoft/mobilizon!730
2020-11-30 12:46:18 +01:00
Thomas Citharel
10eb64720e
Fix tests with scheduler notifications
Close #390

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-30 12:23:40 +01:00
Thomas Citharel
74e59b2398
Fix tests failure with Group test find_group/3
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-30 12:23:39 +01:00
Thomas Citharel
662541c312
Update GraphQL schema file
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-27 17:25:41 +01:00
Thomas Citharel
d041d274e0
Fix leftover from Picture -> Media rename
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-27 17:20:21 +01:00
Thomas Citharel
f4d0e6d4b3 Merge branch 'clean-unused-users' into 'master'
Clean unconfirmed users

Closes #51

See merge request framasoft/mobilizon!729
2020-11-27 11:25:42 +01:00
Thomas Citharel
0e1dc0df8d
Clean unconfirmed users
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-27 11:10:12 +01:00
Thomas Citharel
86c6c19023 Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!728
2020-11-27 09:32:52 +01:00
Balázs Úr
6186c1b970 Translated using Weblate (Hungarian)
Currently translated at 100.0% (168 of 168 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/hu/
2020-11-27 07:34:57 +01:00
Balázs Úr
f6c78ea57e Translated using Weblate (Hungarian)
Currently translated at 100.0% (802 of 802 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/hu/
2020-11-27 07:34:56 +01:00
Thomas Citharel
0ec5cf3669 Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!726
2020-11-26 18:50:53 +01:00
Eivind Ødegård
649a561ec3 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 74.4% (125 of 168 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/nn/
2020-11-26 18:10:10 +01:00
Vodoyo Kamal
a8802b5990 Added translation using Weblate (Bengali) 2020-11-26 18:10:10 +01:00
Thomas Citharel
620187a056 Merge branch 'detect-images-in-body' into 'master'
Track usage of media files and add a job to clean them

See merge request framasoft/mobilizon!727
2020-11-26 18:10:04 +01:00
Thomas Citharel
40b9841c08
Add Changelog for orphan media files
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-26 17:53:34 +01:00
Thomas Citharel
c9457fe0d3
Track usage of media files and add a job to clean them
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-26 17:53:33 +01:00
Thomas Citharel
c19e326bd8 Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!725
2020-11-26 08:33:35 +01:00
josé m
201b402db4 Translated using Weblate (Galician)
Currently translated at 100.0% (238 of 238 strings)

Translation: Mobilizon/Backend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend/gl/
2020-11-26 07:52:11 +01:00
josé m
994d430076 Translated using Weblate (Galician)
Currently translated at 100.0% (802 of 802 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/gl/
2020-11-26 07:52:10 +01:00
frama late
5ee77fd575 Translated using Weblate (Italian)
Currently translated at 100.0% (168 of 168 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/it/
2020-11-26 07:52:10 +01:00
vancha march
985021c926 Translated using Weblate (Dutch)
Currently translated at 32.9% (264 of 802 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/nl/
2020-11-26 07:52:09 +01:00
x
c575e5b166 Translated using Weblate (Italian)
Currently translated at 100.0% (802 of 802 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/it/
2020-11-26 07:52:09 +01:00
Thomas Citharel
7d08945062 Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!724
2020-11-25 17:39:07 +01:00
x
2adc234a28 Translated using Weblate (Italian)
Currently translated at 100.0% (168 of 168 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/it/
2020-11-25 16:53:36 +01:00
Thomas Citharel
213b4e2aaa Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!723
2020-11-25 08:06:43 +01:00
josé m
91b5035a70 Translated using Weblate (Galician)
Currently translated at 100.0% (802 of 802 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/gl/
2020-11-24 22:52:11 +01:00
Joan Luci Labòrda
694971a0d7 Translated using Weblate (Occitan)
Currently translated at 51.1% (86 of 168 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/oc/
2020-11-24 22:52:10 +01:00
Taru Luojola
a0fbdff566 Translated using Weblate (Finnish)
Currently translated at 100.0% (802 of 802 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/fi/
2020-11-24 22:52:09 +01:00
Joan Luci Labòrda
058775eaba Translated using Weblate (Occitan)
Currently translated at 100.0% (802 of 802 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/oc/
2020-11-24 22:52:09 +01:00
Berto Te
0db9603ad4 Translated using Weblate (Spanish)
Currently translated at 100.0% (802 of 802 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/es/
2020-11-24 22:52:08 +01:00
Thomas Citharel
a368c9542b Merge branch 'allow-to-remove-pictures' into 'master'
Allow to remove pictures and show user media size usage

Closes #281

See merge request framasoft/mobilizon!721
2020-11-23 17:19:22 +01:00
Thomas Citharel
2ef973000e
Show user and actors media usage in admin
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-23 17:03:27 +01:00
Thomas Citharel
b11d35cbec
Backend support to get used media size for users and actors
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-23 17:00:42 +01:00
Thomas Citharel
6a1cd42d2c
Add backend to list an user's pictures
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-23 17:00:42 +01:00
Thomas Citharel
846f7b71f3
Update some outdated dev config
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-23 17:00:41 +01:00
Thomas Citharel
01b1176838
Fix some bad french translations
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-23 17:00:40 +01:00
Thomas Citharel
605239130e
Refactor Picture upload
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-23 17:00:39 +01:00
Thomas Citharel
7a731f1ef8
Fix pictures being deleting cascading to events & posts
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-23 17:00:38 +01:00
Thomas Citharel
1cd680526a
Add backend to remove pictures
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-23 17:00:36 +01:00
Thomas Citharel
03e4916ebf Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!722
2020-11-23 17:00:21 +01:00
flemlyn
6bb18a0855 Translated using Weblate (German)
Currently translated at 36.5% (87 of 238 strings)

Translation: Mobilizon/Backend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend/de/
2020-11-23 16:59:38 +01:00
Thomas Citharel
c195a73aa7 Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!719
2020-11-22 10:55:27 +01:00
josé m
0551737c55 Translated using Weblate (Galician)
Currently translated at 100.0% (168 of 168 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/gl/
2020-11-22 00:52:08 +01:00
Berto Te
80225c21ab Translated using Weblate (Spanish)
Currently translated at 100.0% (168 of 168 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/es/
2020-11-22 00:52:07 +01:00
fadelkon
5a80a66759 Translated using Weblate (Catalan)
Currently translated at 100.0% (801 of 801 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/ca/
2020-11-22 00:52:06 +01:00
Thomas Citharel
0e5d1027c9 Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!717
2020-11-20 15:30:01 +01:00
Taru Luojola
c2f88bf89f Translated using Weblate (Finnish)
Currently translated at 100.0% (168 of 168 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/fi/
2020-11-20 15:29:34 +01:00
frama late
7f9f426b0f Translated using Weblate (Italian)
Currently translated at 100.0% (801 of 801 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/it/
2020-11-20 15:29:33 +01:00
Thomas Citharel
68a4222a18
Add missing leftover documentation for GraphQL schema
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-20 15:24:04 +01:00
Thomas Citharel
86cda335dc
Merge i18n
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-20 12:23:25 +01:00
Thomas Citharel
7198a3f5cc Merge branch 'improve-graphql-docs' into 'master'
Improve graphql docs

See merge request framasoft/mobilizon!718
2020-11-20 12:10:28 +01:00
Thomas Citharel
19c9cf5e16
Fix refreshing groups
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-20 11:44:00 +01:00
Thomas Citharel
3eacbb2ca3
Improve GraphQL documentation and cleanup API
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-20 11:44:00 +01:00
Thomas Citharel
8db8314528 Merge branch 'dockerfile-volume-fix' into 'master'
fix: fix permissions for /app/uploads for volume mount

See merge request framasoft/mobilizon!711
2020-11-18 12:03:44 +01:00
Thomas Citharel
e8a3b6aa94 Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!716
2020-11-18 09:48:41 +01:00
Joan Luci Labòrda
1c6e53ab10 Translated using Weblate (Occitan)
Currently translated at 35.0% (60 of 171 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/oc/
2020-11-18 09:13:49 +01:00
Joan Luci Labòrda
c85b06ba09 Translated using Weblate (Occitan)
Currently translated at 34.7% (59 of 170 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/oc/
2020-11-18 08:52:19 +01:00
Ville Ranki
af581f3ba7 Translated using Weblate (Finnish)
Currently translated at 100.0% (801 of 801 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/fi/
2020-11-18 08:52:19 +01:00
Thomas Citharel
fd87704f43 Merge branch 'fix-creating-usernames-with-uppercase' into 'master'
Forbid creating usernames with uppercase characters

See merge request framasoft/mobilizon!715
2020-11-18 08:52:13 +01:00
Thomas Citharel
e6077d0dc3
Forbid creating usernames with uppercase characters
We don't actually enforce anything on the ActivityPub level, only
user-facing interfaces

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-17 19:54:40 +01:00
Thomas Citharel
c28dae45bb Merge branch 'fix-opengraph-actor-preview' into 'master'
Fix opengraph actor preview

See merge request framasoft/mobilizon!714
2020-11-17 16:03:13 +01:00
Thomas Citharel
72cd3e688d
Add tests for metadata
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-17 15:45:42 +01:00
Thomas Citharel
15a82c7bce
[Metadata] Fix actors not sanitizing their description and refactor
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-17 15:45:08 +01:00
Thomas Citharel
a115b49b4c
Only load all locales in prod mode
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-17 15:42:03 +01:00
Thomas Citharel
885b61dfd6 Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!713
2020-11-17 11:32:06 +01:00
Eivind Ødegård
cf413baae6 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 61.1% (104 of 170 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/nn/
2020-11-17 11:13:49 +01:00
Quentin PAGÈS
0a8e41451b Translated using Weblate (Occitan)
Currently translated at 32.9% (56 of 170 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/oc/
2020-11-17 11:13:49 +01:00
Joan Luci Labòrda
a75084ac9c Translated using Weblate (Occitan)
Currently translated at 32.9% (56 of 170 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/oc/
2020-11-17 11:13:49 +01:00
Thomas Citharel
1970061a33 Merge branch 'fix-register-on-strings' into 'master'
Fix register sentense string

See merge request framasoft/mobilizon!712
2020-11-17 11:13:43 +01:00
Thomas Citharel
5d9a36917d
Fix register sentense string
See https://framacolibri.org/t/sinscrire-sur-mobilizon-affiche-au-lieu-du-nom-de-linstance/9838

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-17 10:57:04 +01:00
Thomas Citharel
15fe86b176 Merge branch 'use-user-timezone-in-emails' into 'master'
Use user timezone in emails

See merge request framasoft/mobilizon!710
2020-11-17 09:58:21 +01:00
Thomas Citharel
fdc8536c6f
Use user timezone in emails
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-17 09:36:32 +01:00
Ivan Vandot
13fb242c82 set permissions for /app/uploads for volume mount 2020-11-16 23:28:29 +01:00
Thomas Citharel
06a1233fc2 Merge branch 'weblate-mobilizon-frontend' into 'master'
Translations update from Weblate

See merge request framasoft/mobilizon!709
2020-11-16 16:08:50 +01:00
Eivind Ødegård
c06fe5e686 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 31.1% (53 of 170 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/nn/
2020-11-16 15:47:25 +01:00
Balázs Úr
ad2577e988 Translated using Weblate (Hungarian)
Currently translated at 100.0% (170 of 170 strings)

Translation: Mobilizon/Backend errors
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend-errors/hu/
2020-11-16 15:47:24 +01:00
Balázs Úr
e503330d67 Translated using Weblate (Hungarian)
Currently translated at 100.0% (802 of 802 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/hu/
2020-11-16 15:47:24 +01:00
Eivind Ødegård
f598ab2000 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (802 of 802 strings)

Translation: Mobilizon/Frontend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/frontend/nn/
2020-11-16 15:47:23 +01:00
Marcin Mikołajczak
a7d78da3f8 Translated using Weblate (Polish)
Currently translated at 69.7% (166 of 238 strings)

Translation: Mobilizon/Backend
Translate-URL: https://weblate.framasoft.org/projects/mobilizon/backend/pl/
2020-11-16 15:47:22 +01:00
Thomas Citharel
12f6837fa2 Merge branch 'security-hide-tokens-in-logs' into 'master'
Hide tokens inside logs

See merge request framasoft/mobilizon!708
2020-11-16 15:31:11 +01:00
Thomas Citharel
eafc9ab658
Hide tokens inside logs
Especially from Websockets logs which contains auth token

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2020-11-16 12:30:06 +01:00
489 changed files with 28317 additions and 15550 deletions

View File

@ -39,9 +39,9 @@ lint:
- mix format --check-formatted --dry-run || export EXITVALUE=1
- cd js
- yarn install
#- yarn run lint || export EXITVALUE=1
- yarn run prettier --ignore-path="src/i18n/*" -c . || export EXITVALUE=1
- yarn run build
- yarn run lint || export EXITVALUE=1
- yarn run prettier -c . || export EXITVALUE=1
- yarn run build:assets
- cd ../
- exit $EXITVALUE
artifacts:
@ -69,7 +69,7 @@ exunit:
before_script:
- cd js
- yarn install
- yarn run build
- yarn run build:assets
- cd ../
- mix deps.get
- MIX_ENV=test mix ecto.create
@ -78,6 +78,21 @@ exunit:
- lint
script:
- mix coveralls
jest:
stage: test
before_script:
- cd js
- yarn install
dependencies:
- lint
script:
- yarn run test:unit --no-color
artifacts:
when: always
paths:
- js/coverage
expire_in: 30 days
# cypress:
# stage: test
# services:

View File

@ -1,8 +0,0 @@
projects:
Mobilizon:
schemaPath: schema.graphql
extensions:
endpoints:
dev:
url: 'http://localhost:4000/api'
introspect: true

View File

@ -4,7 +4,96 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
## 1.0.3 - 18-12-2020
**This release adds new migrations, be sure to run them before restarting Mobilizon**
**This release has repair steps, be sure to execute them right after restarting Mobilizon**
### Special operations
* **Reattach media files to their entity.**
When media files were uploaded and added in events and posts bodies, they were only attached to the profile that uploaded them, not to the event or post. This task attaches them back to their entity so that the command to clean orphan media files doesn't remove them.
* Source install
`MIX_ENV=prod mix mobilizon.maintenance.fix_unattached_media_in_body`
* Docker
`docker-compose exec mobilizon mobilizon_ctl maintenance.fix_unattached_media_in_body`
* **Refresh remote profiles to save avatars locally**
Profile avatars and banners were previously only proxified and cached. Now we save them locally. Refreshing all remote actors will save profile media locally instead.
* Source install
`MIX_ENV=prod mix mobilizon.actors.refresh --all`
* Docker
`docker-compose exec mobilizon mobilizon_ctl actors.refresh --all`
* **imagemagick and webp are now a required dependency** to build Mobilizon.
Optimized versions of Mobilizon's pictures are now produced during front-end build.
See [the documentation](https://docs.joinmobilizon.org/administration/dependencies/#misc) to make sure these dependencies are installed.
### Added
- **Add a command to clean orphan media files**. There's a `--dry-run` option to see what files would have been deleted.
**Make sure all media files have been reattached properly (see above) before running this command.**
In 1.1.0 a scheduled job will be enabled to clear orphan media files automatically after a while.
- Added user and actors media usage information in administration
- Added a scheduled job to clean unconfirmed users (and their eventual initial profile) after a 48 hour grace period
- Added a mix task to manually clean unconfirmed users
- Added OpenStreetMap (OSRM) or GoogleMaps routing pages on the event map modal
- Added PWA support, Mobilizon can now be installed on Android (Firefox and Chrome), iOS (Safari) and desktop (Chrome)
- Added possibility to pick language through a setting on the footer for unlogged users
### Changed
- Save remote avatars and banners instead of proxifying them
- Forbid creating usernames with uppercase characters
- Allow LDAP admin to use a fully qualified DN (different than the one for the users)
- Allow LDAP users to be filtered by LDAP attribute `memberOf`.
- Improve the "My events" and "My groups" page when there's nothing here yet
- Show identity concerned when listing event participations (in "My events") and group membership (in "My groups")
- The datetime picker on the event's edition page has been changed and allows directly editing the text
- Allow to clear and remove pictures from events and posts
### Fixed
- Fixed inline media that weren't being tracked, so that they are not considered orphans media files.
- Fixed permissions on the Docker volume
- Fixed emails not using user timezone
- Fixed draft status not being shown on group events & posts inside admin section
- Fixed cancelled status not being shown on cancelled events cards
- Fixed membership notification emails not being sent with the user's language
- Fixed group posts ActivityPub endpoint
- Fixed unlisted groups being available in search
- Fixed inline media pictures being unattached when editing an event or a post
- Fixed adding an instance to follow with spaces
- Fixed past groups showing up on group's page
- Fixed error message not showing up when you are already an anonymous participant for an event
- Fixed error message not showing up when you pick an username already in user for a new profile or a group
- Fixed translations not fallbacking properly to english when not found
-
### Security
- Stop logging user JWT tokens in Websocket Mobilizon logs
### Translations
Updated translations:
- Catalan
- Dutch
- English
- Finnish
- French
- Galician
- German
- Hungarian
- Italian
- Norwegian
- Occitan
- Polish
- Spanish
- Swedish
## 1.0.2 - 2020-11-15

View File

@ -52,7 +52,7 @@ We appreciate any contribution to Mobilizon. Check our [CONTRIBUTING](CONTRIBUTI
* 📜 Documentation [https://docs.joinmobilizon.org](https://docs.joinmobilizon.org)
### Discuss
* 💬 Riot/Matrix: [https://riot.im/app/#/room/#Mobilizon:matrix.org](https://riot.im/app/#/room/#Mobilizon:matrix.org)
* 💬 Element/Matrix: [https://matrix.to/#/#Mobilizon:matrix.org](https://matrix.to/#/#Mobilizon:matrix.org)
* 🗣️ Forum: [https://framacolibri.org/c/mobilizon](https://framacolibri.org/c/mobilizon)
### Follow

View File

@ -28,6 +28,10 @@ config :mobilizon, :instance,
upload_limit: 10_000_000,
avatar_upload_limit: 2_000_000,
banner_upload_limit: 4_000_000,
remove_orphan_uploads: true,
orphan_upload_grace_period_hours: 48,
remove_unconfirmed_users: true,
unconfirmed_user_grace_period_hours: 48,
email_from: "noreply@localhost",
email_reply_to: "noreply@localhost"
@ -77,17 +81,6 @@ config :mobilizon, Mobilizon.Web.Upload,
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, uploads: "uploads"
config :mobilizon, :media_proxy,
enabled: true,
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :media
]
]
config :mobilizon, Mobilizon.Web.Email.Mailer,
adapter: Bamboo.SMTPAdapter,
server: "localhost",
@ -142,6 +135,10 @@ config :mobilizon, :ldap,
base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
uid: System.get_env("LDAP_UID") || "cn",
require_bind_for_search: !(System.get_env("LDAP_REQUIRE_BIND_FOR_SEARCH") == "false"),
# The full CN to filter by `memberOf`, or `false` if disabled
group: false,
# Either the admin UID matching the field in `uid`,
# Either a tuple with the fully qualified DN: {:full, uid=admin,dc=example.com,dc=local}
bind_uid: System.get_env("LDAP_BIND_UID"),
bind_password: System.get_env("LDAP_BIND_PASSWORD")
@ -154,22 +151,20 @@ config :geolix,
}
]
config :auto_linker,
opts: [
scheme: true,
extra: true,
# TODO: Set to :no_scheme when it works properly
validate_tld: true,
class: false,
strip_prefix: false,
new_window: true,
rel: "noopener noreferrer ugc"
]
config :mobilizon, Mobilizon.Service.Formatter,
class: false,
rel: "noopener noreferrer ugc",
new_window: true,
truncate: false,
strip_prefix: false,
extra: true,
validate_tld: :no_scheme
config :tesla, adapter: Tesla.Adapter.Hackney
config :phoenix, :format_encoders, json: Jason, "activity-json": Jason
config :phoenix, :json_library, Jason
config :phoenix, :filter_parameters, ["password", "token"]
config :ex_cldr,
default_locale: "en",
@ -180,26 +175,8 @@ config :http_signatures,
config :mobilizon, :cldr,
locales: [
"ar",
"be",
"ca",
"cs",
"de",
"en",
"es",
"fi",
"fr",
"gl",
"hu",
"it",
"ja",
"nl",
"nn",
"oc",
"pl",
"pt",
"ru",
"sv"
"en"
]
config :mobilizon, :activitypub,
@ -233,6 +210,9 @@ config :mobilizon, :maps,
tiles: [
endpoint: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution: "© The OpenStreetMap Contributors"
],
routing: [
type: :openstreetmap
]
config :mobilizon, :anonymous,
@ -266,7 +246,10 @@ config :mobilizon, Oban,
queues: [default: 10, search: 5, mailers: 10, background: 5],
crontab: [
{"@hourly", Mobilizon.Service.Workers.BuildSiteMap, queue: :background},
{"17 * * * *", Mobilizon.Service.Workers.RefreshGroups, queue: :background}
{"17 * * * *", Mobilizon.Service.Workers.RefreshGroups, queue: :background},
# To be activated in Mobilizon 1.2
# {"@hourly", Mobilizon.Service.Workers.CleanOrphanMediaWorker, queue: :background},
{"@hourly", Mobilizon.Service.Workers.CleanUnconfirmedUsersWorker, queue: :background}
]
config :mobilizon, :rich_media,

View File

@ -19,7 +19,6 @@ config :mobilizon, Mobilizon.Web.Endpoint,
code_reloader: true,
check_origin: false,
watchers: [
# yarn: ["run", "dev", cd: Path.expand("../js", __DIR__)]
node: [
"node_modules/webpack/bin/webpack.js",
"--mode",
@ -53,8 +52,8 @@ config :mobilizon, Mobilizon.Web.Endpoint,
patterns: [
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
~r{priv/gettext/.*(po)$},
~r{lib/mobilizon_web/views/.*(ex)$},
~r{lib/mobilizon_web/templates/.*(eex)$}
~r{lib/web/(live|views)/.*(ex)$},
~r{lib/web/templates/.*(eex)$}
]
]
@ -92,13 +91,6 @@ config :mobilizon, :instance,
# config :mobilizon, :activitypub, sign_object_fetches: false
# No need to compile every locale in development environment
config :mobilizon, :cldr,
locales: [
"fr",
"en"
]
config :mobilizon, :anonymous,
reports: [
allowed: true

View File

@ -13,6 +13,31 @@ config :mobilizon, Mobilizon.Web.Endpoint,
# Do not print debug messages in production
config :logger, level: :info
# Load all locales in production
config :mobilizon, :cldr,
locales: [
"ar",
"be",
"ca",
"cs",
"de",
"en",
"es",
"fi",
"fr",
"gl",
"hu",
"it",
"ja",
"nl",
"nn",
"oc",
"pl",
"pt",
"ru",
"sv"
]
cond do
System.get_env("INSTANCE_CONFIG") &&
File.exists?("./config/#{System.get_env("INSTANCE_CONFIG")}") ->

View File

@ -1,7 +1,7 @@
# First build the application assets
FROM node:alpine as assets
RUN apk add --no-cache python build-base
RUN apk add --no-cache python build-base libwebp-tools bash imagemagick ncurses
COPY js .
RUN yarn install \
@ -33,6 +33,8 @@ FROM alpine
RUN apk add --no-cache openssl ncurses-libs file postgresql-client
RUN mkdir -p /app/uploads && chown nobody:nobody /app/uploads
USER nobody
EXPOSE 4000

View File

@ -4,4 +4,4 @@ indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
max_line_length = 80

View File

@ -7,13 +7,9 @@ module.exports = {
extends: [
"plugin:vue/essential",
"@vue/airbnb",
"@vue/typescript/recommended",
"plugin:cypress/recommended",
"plugin:prettier/recommended",
"prettier",
"eslint:recommended",
"@vue/prettier",
"@vue/typescript/recommended",
"@vue/prettier/@typescript-eslint",
],
@ -21,6 +17,7 @@ module.exports = {
parserOptions: {
ecmaVersion: 2020,
parser: "@typescript-eslint/parser",
},
rules: {
@ -35,29 +32,24 @@ module.exports = {
"@typescript-eslint/no-explicit-any": "off",
"cypress/no-unnecessary-waiting": "off",
"vue/max-len": [
"error",
"off",
{
ignoreStrings: true,
ignoreHTMLTextContents: true,
ignoreTemplateLiterals: true,
ignoreComments: true,
template: 170,
code: 100,
code: 80,
},
],
"prettier/prettier": "error",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/no-use-before-define": "off",
"import/prefer-default-export": "off",
"import/extensions": "off",
"import/no-unresolved": "off",
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
},
ignorePatterns: ["src/typings/*.d.ts", "vue.config.js"],
overrides: [
{
files: ["**/__tests__/*.{j,t}s?(x)", "**/tests/unit/**/*.spec.{j,t}s?(x)"],
env: {
mocha: true,
},
},
],
};

1
js/.gitignore vendored
View File

@ -4,6 +4,7 @@ node_modules
/tests/e2e/videos/
/tests/e2e/screenshots/
/coverage
# local env files
.env.local

View File

@ -1 +1,2 @@
src/i18n/*.json
src/i18n/*.json
coverage/

View File

@ -24,7 +24,9 @@ fetch(`http://localhost:4000/api`, {
.then((result) => result.json())
.then((result) => {
// here we're filtering out any type information unrelated to unions or interfaces
const filteredData = result.data.__schema.types.filter((type) => type.possibleTypes !== null);
const filteredData = result.data.__schema.types.filter(
(type) => type.possibleTypes !== null
);
result.data.__schema.types = filteredData;
fs.writeFile("./fragmentTypes.json", JSON.stringify(result.data), (err) => {
if (err) {

19
js/jest.config.js Normal file
View File

@ -0,0 +1,19 @@
module.exports = {
preset: "@vue/cli-plugin-unit-jest/presets/typescript-and-babel",
collectCoverage: true,
collectCoverageFrom: [
"**/*.{vue,ts}",
"!**/node_modules/**",
"!get_union_json.ts",
],
coverageReporters: ["html", "text", "text-summary"],
// The following should fix the issue with svgs and ?inline loader (see Logo.vue), but doesn't work
//
// transform: {
// "^.+\\.svg$": "<rootDir>/tests/unit/svgTransform.js",
// },
// moduleNameMapper: {
// "^@/(.*svg)(\\?inline)$": "<rootDir>/src/$1",
// "^@/(.*)$": "<rootDir>/src/$1",
// },
};

View File

@ -1,11 +1,13 @@
{
"name": "mobilizon",
"version": "1.0.2",
"version": "1.0.3",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --modern",
"test:unit": "vue-cli-service test:unit",
"build:assets": "vue-cli-service build --modern",
"build:pictures": "bash ./scripts/build/pictures.sh",
"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:e2e": "vue-cli-service test:e2e",
"lint": "vue-cli-service lint"
},
@ -29,7 +31,7 @@
"eslint-plugin-cypress": "^2.10.3",
"graphql": "^15.0.0",
"graphql-tag": "^2.10.3",
"intersection-observer": "^0.11.0",
"intersection-observer": "^0.12.0",
"leaflet": "^1.4.0",
"leaflet.locatecontrol": "^0.72.0",
"lodash": "^4.17.11",
@ -39,6 +41,7 @@
"tippy.js": "^6.2.3",
"tiptap": "^1.26.0",
"tiptap-extensions": "^1.29.1",
"unfetch": "^4.2.0",
"v-tooltip": "2.0.2",
"vue": "^2.6.11",
"vue-apollo": "^3.0.3",
@ -52,6 +55,7 @@
"vuedraggable": "2.23.2"
},
"devDependencies": {
"@types/jest": "^26.0.18",
"@types/leaflet": "^1.5.2",
"@types/leaflet.locatecontrol": "^0.60.7",
"@types/lodash": "^4.14.141",
@ -63,27 +67,27 @@
"@types/vuedraggable": "^2.23.0",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"@vue/cli-plugin-babel": "~4.5.8",
"@vue/cli-plugin-e2e-cypress": "~4.5.8",
"@vue/cli-plugin-eslint": "~4.5.8",
"@vue/cli-plugin-pwa": "~4.5.8",
"@vue/cli-plugin-router": "~4.5.8",
"@vue/cli-plugin-typescript": "~4.5.8",
"@vue/cli-service": "~4.5.8",
"@vue/eslint-config-airbnb": "^5.0.2",
"@vue/cli-plugin-babel": "~4.5.9",
"@vue/cli-plugin-e2e-cypress": "~4.5.9",
"@vue/cli-plugin-eslint": "~4.5.9",
"@vue/cli-plugin-pwa": "~4.5.9",
"@vue/cli-plugin-router": "~4.5.9",
"@vue/cli-plugin-typescript": "~4.5.9",
"@vue/cli-plugin-unit-jest": "~4.5.0",
"@vue/cli-service": "~4.5.9",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"@vue/test-utils": "^1.1.0",
"eslint": "^7.7.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.20.2",
"eslint-config-prettier": "^7.0.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^7.0.0",
"prettier": "2.1.2",
"prettier-eslint": "^11.0.0",
"mock-apollo-client": "^0.4",
"prettier": "2.2.1",
"prettier-eslint": "^12.0.0",
"sass": "^1.29.0",
"sass-loader": "^10.0.1",
"typescript": "~4.0.2",
"typescript": "~4.1.2",
"vue-cli-plugin-svg": "~0.1.3",
"vue-i18n-extract": "^1.0.2",
"vue-template-compiler": "^2.6.11",

View File

Before

Width:  |  Height:  |  Size: 725 KiB

After

Width:  |  Height:  |  Size: 725 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

3291
js/schema.graphql Normal file

File diff suppressed because it is too large Load Diff

90
js/scripts/build/pictures.sh Executable file
View File

@ -0,0 +1,90 @@
#!/bin/bash
set -eu
output_dir="../priv/static/img/pics"
resolutions=(
480
1024
1920
)
ignore=(
homepage_background.png
)
file_extension () {
filename=$(basename -- "$file")
echo "${filename##*.}"
}
file_name () {
filename=$(basename -- "$file")
echo "${filename%.*}"
}
convert_image () {
name=$(file_name)
extension=$(file_extension)
res="$1w"
output="$output_dir/$name-$res.$extension"
convert -geometry "$resolution"x $file $output
}
produce_webp () {
name=$(file_name)
output="$output_dir/$name.webp"
cwebp $file -quiet -o $output
}
progress() {
local w=80 p=$1; shift
# create a string of spaces, then change them to dots
printf -v dots "%*s" "$(( $p*$w/100 ))" ""; dots=${dots// /.};
# print those dots on a fixed-width space plus the percentage etc.
printf "\r\e[K|%-*s| %3d %% %s" "$w" "$dots" "$p" "$*";
}
echo "Generating responsive versions of the pictures…"
if ! command -v convert &> /dev/null
then
echo "$(tput setaf 1)ERROR: The convert command could not be found. You need to install ImageMagick.$(tput sgr 0)"
exit 1
fi
nb_files=$( shopt -s nullglob ; set -- $output_dir/* ; echo $#)
tasks=$((${#resolutions[@]}*$nb_files))
i=1
for file in $output_dir/*
do
if [[ -f $file ]]; then
for resolution in "${resolutions[@]}"; do
convert_image $resolution
progress $(($i*100/$tasks)) still working...
i=$((i+1))
done
fi
done
echo -e "\nDone!"
echo "Generating optimized versions of the pictures…"
if ! command -v cwebp &> /dev/null
then
echo "$(tput setaf 1)ERROR: The cwebp command could not be found. You need to install webp.$(tput sgr 0)"
exit 1
fi
nb_files=$( shopt -s nullglob ; set -- $output_dir/* ; echo $#)
i=1
for file in $output_dir/*
do
if [[ -f $file ]]; then
produce_webp
progress $(($i*100/$nb_files)) still working...
i=$((i+1))
fi
done
echo -e "\nDone!"

18
js/src/@types/dom.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
declare global {
interface GeolocationCoordinates {
readonly accuracy: number;
readonly altitude: number | null;
readonly altitudeAccuracy: number | null;
readonly heading: number | null;
readonly latitude: number;
readonly longitude: number;
readonly speed: number | null;
}
interface GeolocationPosition {
readonly coords: GeolocationCoordinates;
readonly timestamp: number;
}
}
export {};

View File

@ -32,8 +32,16 @@
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import NavBar from "./components/NavBar.vue";
import { AUTH_ACCESS_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID, AUTH_USER_ROLE } from "./constants";
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from "./graphql/user";
import {
AUTH_ACCESS_TOKEN,
AUTH_USER_EMAIL,
AUTH_USER_ID,
AUTH_USER_ROLE,
} from "./constants";
import {
CURRENT_USER_CLIENT,
UPDATE_CURRENT_USER_CLIENT,
} from "./graphql/user";
import Footer from "./components/Footer.vue";
import Logo from "./components/Logo.vue";
import { initializeCurrentActor } from "./utils/auth";

View File

@ -1,8 +1,11 @@
import { ICurrentUserRole } from "@/types/enums";
import { ApolloCache } from "apollo-cache";
import { NormalizedCacheObject } from "apollo-cache-inmemory";
import { ICurrentUserRole } from "@/types/current-user.model";
import { Resolvers } from "apollo-client/core/types";
export default function buildCurrentUserResolver(cache: ApolloCache<NormalizedCacheObject>) {
export default function buildCurrentUserResolver(
cache: ApolloCache<NormalizedCacheObject>
): Resolvers {
cache.writeData({
data: {
currentUser: {
@ -53,7 +56,12 @@ export default function buildCurrentUserResolver(cache: ApolloCache<NormalizedCa
preferredUsername,
avatar,
name,
}: { id: string; preferredUsername: string; avatar: string; name: string },
}: {
id: string;
preferredUsername: string;
avatar: string;
name: string;
},
{ cache: localCache }: { cache: ApolloCache<NormalizedCacheObject> }
) => {
const data = {

View File

@ -1,4 +1,7 @@
import { IntrospectionFragmentMatcher, NormalizedCacheObject } from "apollo-cache-inmemory";
import {
IntrospectionFragmentMatcher,
NormalizedCacheObject,
} from "apollo-cache-inmemory";
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from "@/constants";
import { REFRESH_TOKEN } from "@/graphql/auth";
import { saveTokenData } from "@/utils/auth";
@ -11,7 +14,11 @@ export const fragmentMatcher = new IntrospectionFragmentMatcher({
{
kind: "UNION",
name: "SearchResult",
possibleTypes: [{ name: "Event" }, { name: "Person" }, { name: "Group" }],
possibleTypes: [
{ name: "Event" },
{ name: "Person" },
{ name: "Group" },
],
},
{
kind: "INTERFACE",

View File

@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 248.16 46.78">
<title>Mobilizon Logo</title>
<g data-name="header">
<path d="M0 45.82l3.18-40.8a29.88 29.88 0 015.07-.36 27.74 27.74 0 014.95.36l4.86 17.16a92.19 92.19 0 012.34 10.08h.36a92.19 92.19 0 012.34-10.08L28 5.02a29.23 29.23 0 015-.36 29.23 29.23 0 015 .36l3.18 40.8a13.61 13.61 0 01-3.63.42 23.41 23.41 0 01-3.63-.24l-1.2-19.92q-.36-5.52-.48-12.84h-.44l-7.32 26.51a25.62 25.62 0 01-4 .3 23.36 23.36 0 01-3.84-.3L9.36 13.24H9q-.3 8.94-.48 12.84L7.26 46a22.47 22.47 0 01-3.6.24A13.75 13.75 0 010 45.82zM74 31.06q0 8-4.26 12.3a12.21 12.21 0 01-9 3.42 12.21 12.21 0 01-9-3.42q-4.26-4.26-4.26-12.3t4.24-12.31a12.21 12.21 0 019-3.42 12.21 12.21 0 019 3.42Q74 23.02 74 31.06zM60.75 20.98q-5.67 0-5.67 10.08t5.67 10.08q5.67 0 5.67-10.08t-5.67-10.08zM103.2 19.75q2.7 4.11 2.7 11.28T102 42.31a13.18 13.18 0 01-10 4.11 31.41 31.41 0 01-11.34-2V2.2l.4-.45h2.76A4 4 0 0187 2.83a5.38 5.38 0 01.93 3.57v11.94a12.08 12.08 0 017.56-2.7 8.71 8.71 0 017.71 4.11zm-9.72 2a7.28 7.28 0 00-5.58 2.82v16a15 15 0 004.08.54 5.25 5.25 0 004.68-2.67q1.68-2.67 1.68-7.59 0-9.03-4.86-9.1zM121 22v23.94a20.85 20.85 0 01-3.66.3 23 23 0 01-3.78-.3V24.75q0-3.24-2.7-3.24h-.72a9.32 9.32 0 01-.3-2.58 10.7 10.7 0 01.3-2.7 39.63 39.63 0 014.38-.24h1a5.19 5.19 0 014 1.62A6.27 6.27 0 01121 22z" />
<path d="M119.82.84a7.37 7.37 0 01.6 3 7.37 7.37 0 01-.6 3 7.46 7.46 0 01-3.87.84 6.49 6.49 0 01-3.69-.93 7.37 7.37 0 01-.6-3 7.37 7.37 0 01.6-3 8.09 8.09 0 013.87-.84 7.05 7.05 0 013.69.93z" fill="#fff" />
<path d="M139.08 40.42h2a10.23 10.23 0 01.6 3.18 9.24 9.24 0 01-.18 2.1 38.47 38.47 0 01-5.64.54q-6.48 0-6.48-7v-37l.36-.42h2.88a3.94 3.94 0 013.12 1.05 5.52 5.52 0 01.9 3.57v31.31q-.02 2.67 2.44 2.67zM155.94 22v23.94a20.85 20.85 0 01-3.66.3 23 23 0 01-3.78-.3V24.75q0-3.24-2.7-3.24h-.72a9.32 9.32 0 01-.3-2.58 10.7 10.7 0 01.3-2.7 39.63 39.63 0 014.38-.24h1a5.19 5.19 0 014.05 1.62 6.27 6.27 0 011.43 4.39z" />
<path d="M154.8 2.84a7.37 7.37 0 01.6 3 7.37 7.37 0 01-.6 3 7.46 7.46 0 01-3.87.84 6.49 6.49 0 01-3.69-.93 7.37 7.37 0 01-.6-3 7.37 7.37 0 01.6-3 8.09 8.09 0 013.87-.84 7.05 7.05 0 013.69.93z" fill="#fff" />
<path d="M163.08 39.22l8.76-11.82q1.32-1.8 4.8-5.7l-.18-.3a63.09 63.09 0 01-7.74.42H163a9.79 9.79 0 01-.24-2.34 15.8 15.8 0 01.42-3.3h20.4a16.31 16.31 0 011 4.26 4.1 4.1 0 01-.78 2.34L175 34.66a64.65 64.65 0 01-4.56 5.7l.18.24q3.12-.3 5.22-.3h2.58a15.35 15.35 0 006.12-.9 9.4 9.4 0 01.72 3.12q0 3.42-4.32 3.42h-18a14.27 14.27 0 01-.9-3.93 5.08 5.08 0 011.04-2.79zM215.88 31.06q0 8-4.26 12.3a13.63 13.63 0 01-18.06 0q-4.26-4.26-4.26-12.3t4.26-12.31a13.63 13.63 0 0118.06 0q4.26 4.27 4.26 12.31zm-13.29-10.08q-5.67 0-5.67 10.08t5.67 10.08q5.67 0 5.67-10.08t-5.67-10.08zM247 25.84v13.32a11 11 0 001.2 5.64 7 7 0 01-4.41 1.56q-2.43 0-3.33-1.14a5.69 5.69 0 01-.9-3.54V27.4a7.74 7.74 0 00-.72-3.87 2.78 2.78 0 00-2.58-1.17 8.62 8.62 0 00-6.3 3v20.58a20.85 20.85 0 01-3.66.3 23 23 0 01-3.78-.3v-29.7l.42-.36h2.76q3.42 0 4.08 3.6 4.38-3.84 8.73-3.84t6.42 2.82a12.17 12.17 0 012.07 7.38z" />
<path d="M57.26 10.75a7.37 7.37 0 01-.6-3 7.37 7.37 0 01.6-3 8.09 8.09 0 013.87-.84 7.05 7.05 0 013.69.84 7.37 7.37 0 01.6 3 7.37 7.37 0 01-.6 3 7.46 7.46 0 01-3.87.84 6.49 6.49 0 01-3.69-.84zM198.26 10.75a7.37 7.37 0 01-.6-3 7.37 7.37 0 01.6-3 8.09 8.09 0 013.87-.84 7.05 7.05 0 013.69.84 7.37 7.37 0 01.6 3 7.37 7.37 0 01-.6 3 7.46 7.46 0 01-3.87.84 6.49 6.49 0 01-3.69-.84z" fill="#fff" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -13,7 +13,12 @@
<template slot-scope="props">
<div class="media">
<div class="media-left">
<img width="32" :src="props.option.avatar.url" v-if="props.option.avatar" alt="" />
<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">
@ -21,7 +26,9 @@
{{ props.option.name }}
<br />
<small>{{ `@${props.option.preferredUsername}` }}</small>
<small v-if="props.option.domain">{{ `@${props.option.domain}` }}</small>
<small v-if="props.option.domain">{{
`@${props.option.domain}`
}}</small>
</span>
<span v-else>
{{ `@${props.option.preferredUsername}` }}
@ -53,7 +60,9 @@ export default class ActorAutoComplete extends Vue {
selected: IPerson | null = this.defaultSelected;
name: string = this.defaultSelected ? this.defaultSelected.preferredUsername : "";
name: string = this.defaultSelected
? this.defaultSelected.preferredUsername
: "";
page = 1;

View File

@ -12,8 +12,15 @@
<p>
{{ actor.name || `@${usernameWithDomain(actor)}` }}
</p>
<p class="has-text-grey" v-if="actor.name">@{{ usernameWithDomain(actor) }}</p>
<div v-if="full" class="summary" :class="{ limit: limit }" v-html="actor.summary" />
<p class="has-text-grey" v-if="actor.name">
@{{ usernameWithDomain(actor) }}
</p>
<div
v-if="full"
class="summary"
:class="{ limit: limit }"
v-html="actor.summary"
/>
</div>
</div>
</div>

View File

@ -7,7 +7,10 @@
<ul class="identities">
<li v-for="identity in identities" :key="identity.id">
<router-link
:to="{ name: 'UpdateIdentity', params: { identityName: identity.preferredUsername } }"
:to="{
name: 'UpdateIdentity',
params: { identityName: identity.preferredUsername },
}"
class="media identity"
v-bind:class="{ 'is-current-identity': isCurrentIdentity(identity) }"
>
@ -24,7 +27,10 @@
</li>
</ul>
<router-link :to="{ name: 'CreateIdentity' }" class="button create-identity is-primary">
<router-link
:to="{ name: 'CreateIdentity' }"
class="button create-identity is-primary"
>
{{ $t("Create a new identity") }}
</router-link>
</section>
@ -53,7 +59,7 @@ export default class Identities extends Vue {
errors: string[] = [];
isCurrentIdentity(identity: IPerson) {
isCurrentIdentity(identity: IPerson): boolean {
return identity.preferredUsername === this.currentIdentityName;
}
}

View File

@ -1,94 +0,0 @@
<docs>
```vue
<participant-card :participant="{ actor: { preferredUsername: 'user1', name: 'someoneIDontLike' }, role: 'REJECTED' }" />
```
```vue
<participant-card :participant="{ actor: { preferredUsername: 'user2', name: 'someoneWhoWillWait' }, role: 'NOT_APPROVED' }" />
```
```vue
<participant-card :participant="{ actor: { preferredUsername: 'user3', name: 'a_participant' }, role: 'PARTICIPANT' }" />
```
```vue
<participant-card :participant="{ actor: { preferredUsername: 'me', name: 'myself' }, role: 'CREATOR' }" />
```
</docs>
<template>
<article class="card">
<div class="card-content">
<div class="media">
<div class="media-left" v-if="participant.actor.avatar">
<figure class="image is-48x48">
<img :src="participant.actor.avatar.url" />
</figure>
</div>
<div class="media-content">
<span ref="title">{{ actorDisplayName }}</span
><br />
<small class="has-text-grey" v-if="participant.actor.domain"
>@{{ participant.actor.preferredUsername }}@{{ participant.actor.domain }}</small
>
<small class="has-text-grey" v-else>@{{ participant.actor.preferredUsername }}</small>
</div>
</div>
</div>
<footer class="card-footer">
<b-button
v-if="[ParticipantRole.NOT_APPROVED, ParticipantRole.REJECTED].includes(participant.role)"
@click="accept(participant)"
type="is-success"
class="card-footer-item"
>{{ $t("Approve") }}</b-button
>
<b-button
v-if="participant.role === ParticipantRole.NOT_APPROVED"
@click="reject(participant)"
type="is-danger"
class="card-footer-item"
>{{ $t("Reject") }}</b-button
>
<b-button
v-if="participant.role === ParticipantRole.PARTICIPANT"
@click="exclude(participant)"
type="is-danger"
class="card-footer-item"
>{{ $t("Exclude") }}</b-button
>
<span v-if="participant.role === ParticipantRole.CREATOR" class="card-footer-item">{{
$t("Creator")
}}</span>
</footer>
</article>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IParticipant, ParticipantRole } from "../../types/participant.model";
import { IPerson, Person } from "../../types/actor";
@Component
export default class ParticipantCard extends Vue {
@Prop({ required: true }) participant!: IParticipant;
@Prop({ type: Function }) accept!: Function;
@Prop({ type: Function }) reject!: Function;
@Prop({ type: Function }) exclude!: Function;
ParticipantRole = ParticipantRole;
get actorDisplayName(): string {
const actor = new Person(this.participant.actor as IPerson);
return actor.displayName();
}
}
</script>
<style lang="scss">
.card-footer-item {
height: $control-height;
}
</style>

View File

@ -12,8 +12,9 @@
</v-popover>
</template>
<script lang="ts">
import { ActorType } from "@/types/enums";
import { Component, Vue, Prop } from "vue-property-decorator";
import { IActor, ActorType } from "../../types/actor";
import { IActor } from "../../types/actor";
import ActorCard from "./ActorCard.vue";
@Component({

View File

@ -16,11 +16,21 @@
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.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-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>
@ -33,26 +43,39 @@
centered
v-slot="props"
>
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`">{{
props.row.approved ? $t("Accepted") : $t("Pending")
}}</span>
<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-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
<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 slot="detail" slot-scope="props">
@ -143,7 +166,11 @@ export default class Followers extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowers.refetch();
this.checkedRows = [];
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}
@ -158,7 +185,11 @@ export default class Followers extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowers.refetch();
this.checkedRows = [];
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}

View File

@ -1,13 +1,22 @@
<template>
<div>
<form @submit="followRelay">
<b-field :label="$t('Add an instance')" custom-class="add-relay" horizontal>
<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')" />
<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>
<b-button type="is-primary" native-type="submit">{{
$t("Add an instance")
}}</b-button>
</p>
</b-field>
</b-field>
@ -29,12 +38,25 @@
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.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-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>
@ -46,26 +68,39 @@
centered
v-slot="props"
>
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`">{{
props.row.approved ? $t("Accepted") : $t("Pending")
}}</span>
<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-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
<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 slot="detail" slot-scope="props">
@ -103,7 +138,6 @@ 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 { Paginate } from "../../types/paginate";
import RelayMixin from "../../mixins/relay";
@Component({
@ -127,19 +161,25 @@ export default class Followings extends Mixins(RelayMixin) {
await this.$apollo.mutate({
mutation: ADD_RELAY,
variables: {
address: this.newRelayAddress,
address: this.newRelayAddress.trim(), // trim to fix copy and paste domain name spaces and tabs
},
});
await this.$apollo.queries.relayFollowings.refetch();
this.newRelayAddress = "";
} catch (err) {
Snackbar.open({ message: err.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: err.message,
type: "is-danger",
position: "is-bottom",
});
}
}
async removeRelays(): Promise<void> {
await this.checkedRows.forEach((row: IFollower) => {
this.removeRelay(`${row.targetActor.preferredUsername}@${row.targetActor.domain}`);
this.removeRelay(
`${row.targetActor.preferredUsername}@${row.targetActor.domain}`
);
});
}
@ -154,7 +194,11 @@ export default class Followings extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowings.refetch();
this.checkedRows = [];
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}
}

View File

@ -1,19 +1,34 @@
<template>
<li :class="{ reply: comment.inReplyToComment }">
<article class="media" :class="{ selected: commentSelected }" :id="commentId">
<article
class="media"
:class="{ selected: commentSelected }"
:id="commentId"
>
<popover-actor-card
class="media-left"
:actor="comment.actor"
:inline="true"
v-if="comment.actor"
>
<figure class="image is-48x48" v-if="!comment.deletedAt && comment.actor.avatar">
<figure
class="image is-48x48"
v-if="!comment.deletedAt && comment.actor.avatar"
>
<img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
</figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
</popover-actor-card>
<div v-else class="media-left">
<figure class="image is-48x48" v-if="!comment.deletedAt && comment.actor.avatar">
<figure
class="image is-48x48"
v-if="!comment.deletedAt && comment.actor.avatar"
>
<img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
</figure>
<b-icon v-else size="is-large" icon="account-circle" />
@ -21,7 +36,9 @@
<div class="media-content">
<div class="content">
<span class="first-line" v-if="!comment.deletedAt">
<strong :class="{ organizer: commentFromOrganizer }">{{ comment.actor.name }}</strong>
<strong :class="{ organizer: commentFromOrganizer }">{{
comment.actor.name
}}</strong>
<small>@{{ usernameWithDomain(comment.actor) }}</small>
<a class="comment-link has-text-grey" :href="commentURL">
<small>{{
@ -54,10 +71,15 @@
<div class="load-replies" v-if="comment.totalReplies">
<p v-if="!showReplies" @click="fetchReplies">
<b-icon icon="chevron-down" /><span>{{
$tc("View a reply", comment.totalReplies, { totalReplies: comment.totalReplies })
$tc("View a reply", comment.totalReplies, {
totalReplies: comment.totalReplies,
})
}}</span>
</p>
<p v-else-if="comment.totalReplies && showReplies" @click="showReplies = false">
<p
v-else-if="comment.totalReplies && showReplies"
@click="showReplies = false"
>
<b-icon icon="chevron-up" />
<span>{{ $t("Hide replies") }}</span>
</p>
@ -86,14 +108,24 @@
</nav>
</div>
</article>
<form class="reply" @submit.prevent="replyToComment" v-if="currentActor.id" v-show="replyTo">
<form
class="reply"
@submit.prevent="replyToComment"
v-if="currentActor.id"
v-show="replyTo"
>
<article class="media reply">
<figure class="media-left" v-if="currentActor.avatar">
<p class="image is-48x48">
<img :src="currentActor.avatar.url" alt="" />
</p>
</figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
<div class="media-content">
<div class="content">
<span class="first-line">
@ -102,7 +134,12 @@
</span>
<br />
<span class="editor-line">
<editor class="editor" ref="commentEditor" v-model="newComment.text" mode="comment" />
<editor
class="editor"
ref="commentEditor"
v-model="newComment.text"
mode="comment"
/>
<b-button
:disabled="newComment.text.trim().length === 0"
native-type="submit"
@ -118,7 +155,12 @@
<div class="left">
<div class="vertical-border" @click="showReplies = false" />
</div>
<transition-group name="comment-replies" v-if="showReplies" class="comment-replies" tag="ul">
<transition-group
name="comment-replies"
v-if="showReplies"
class="comment-replies"
tag="ul"
>
<comment
class="reply"
v-for="reply in comment.replies"
@ -137,7 +179,7 @@ import { Component, Prop, Vue, Ref } from "vue-property-decorator";
import EditorComponent from "@/components/Editor.vue";
import { SnackbarProgrammatic as Snackbar } from "buefy";
import { formatDistanceToNow } from "date-fns";
import { CommentModeration } from "../../types/event-options.model";
import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import { IPerson, usernameWithDomain } from "../../types/actor";
@ -155,7 +197,8 @@ import PopoverActorCard from "../Account/PopoverActorCard.vue";
},
},
components: {
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
editor: () =>
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
comment: () => import(/* webpackChunkName: "comment" */ "./Comment.vue"),
PopoverActorCard,
},
@ -167,7 +210,9 @@ export default class Comment extends Vue {
// Hack because Vue only exports it's own interface.
// See https://github.com/kaorun343/vue-property-decorator/issues/257
@Ref() readonly commentEditor!: EditorComponent & { replyToComment: (comment: IComment) => void };
@Ref() readonly commentEditor!: EditorComponent & {
replyToComment: (comment: IComment) => void;
};
currentActor!: IPerson;
@ -231,7 +276,9 @@ export default class Comment extends Vue {
if (!eventData) return;
const { event } = eventData;
const { comments } = event;
const parentCommentIndex = comments.findIndex((oldComment) => oldComment.id === parentId);
const parentCommentIndex = comments.findIndex(
(oldComment) => oldComment.id === parentId
);
const parentComment = comments[parentCommentIndex];
if (!parentComment) return;
parentComment.replies = thread;
@ -288,7 +335,6 @@ export default class Comment extends Vue {
mutation: CREATE_REPORT,
variables: {
eventId: this.event.id,
reporterId: this.currentActor.id,
reportedId: this.comment.actor.id,
commentsIds: [this.comment.id],
content,
@ -304,7 +350,11 @@ export default class Comment extends Vue {
duration: 5000,
});
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}
}

View File

@ -6,9 +6,11 @@
@submit.prevent="createCommentForEvent(newComment)"
@keyup.ctrl.enter="createCommentForEvent(newComment)"
>
<b-notification v-if="isEventOrganiser && !areCommentsClosed" :closable="false">{{
$t("Comments are closed for everybody else.")
}}</b-notification>
<b-notification
v-if="isEventOrganiser && !areCommentsClosed"
:closable="false"
>{{ $t("Comments are closed for everybody else.") }}</b-notification
>
<article class="media">
<figure class="media-left">
<identity-picker-wrapper :inline="false" v-model="newComment.actor" />
@ -16,11 +18,20 @@
<div class="media-content">
<div class="field">
<p class="control">
<editor ref="commenteditor" mode="comment" v-model="newComment.text" />
<editor
ref="commenteditor"
mode="comment"
v-model="newComment.text"
/>
</p>
</div>
<div class="send-comment">
<b-button native-type="submit" type="is-primary">{{ $t("Post a comment") }}</b-button>
<b-button
native-type="submit"
type="is-primary"
class="comment-button-submit"
>{{ $t("Post a comment") }}</b-button
>
</div>
</div>
</article>
@ -28,8 +39,19 @@
<b-notification v-else :closable="false">{{
$t("The organiser has chosen to close comments.")
}}</b-notification>
<transition name="comment-empty-list" mode="out-in">
<transition-group name="comment-list" v-if="comments.length" class="comment-list" tag="ul">
<p
v-if="$apollo.queries.comments.loading"
class="loading has-text-centered"
>
{{ $t("Loading comments…") }}
</p>
<transition name="comment-empty-list" mode="out-in" v-else>
<transition-group
name="comment-list"
v-if="comments.length"
class="comment-list"
tag="ul"
>
<comment
class="root-comment"
:comment="comment"
@ -51,7 +73,7 @@
import { Prop, Vue, Component, Watch } from "vue-property-decorator";
import Comment from "@/components/Comment/Comment.vue";
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
import { CommentModeration } from "../../types/event-options.model";
import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model";
import {
CREATE_COMMENT_FROM_EVENT,
@ -76,7 +98,9 @@ import { IEvent } from "../../types/event.model";
};
},
update(data) {
return data.event.comments.map((comment: IComment) => new CommentModel(comment));
return data.event.comments.map(
(comment: IComment) => new CommentModel(comment)
);
},
skip() {
return !this.event.uuid;
@ -86,7 +110,8 @@ import { IEvent } from "../../types/event.model";
components: {
Comment,
IdentityPickerWrapper,
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
editor: () =>
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
},
})
export default class CommentTree extends Vue {
@ -112,9 +137,10 @@ export default class CommentTree extends Vue {
mutation: CREATE_COMMENT_FROM_EVENT,
variables: {
eventId: this.event.id,
actorId: comment.actor.id,
text: comment.text,
inReplyToCommentId: comment.inReplyToComment ? comment.inReplyToComment.id : null,
inReplyToCommentId: comment.inReplyToComment
? comment.inReplyToComment.id
: null,
},
update: (store, { data }) => {
if (data == null) return;
@ -204,7 +230,6 @@ export default class CommentTree extends Vue {
mutation: DELETE_COMMENT,
variables: {
commentId: comment.id,
actorId: this.currentActor.id,
},
update: (store, { data }) => {
if (data == null) return;
@ -230,7 +255,9 @@ export default class CommentTree extends Vue {
});
if (!localData) return;
const { thread: oldReplyList } = localData;
const replies = oldReplyList.filter((reply) => reply.id !== deletedCommentId);
const replies = oldReplyList.filter(
(reply) => reply.id !== deletedCommentId
);
store.writeQuery({
query: FETCH_THREAD_REPLIES,
variables: {
@ -251,7 +278,9 @@ export default class CommentTree extends Vue {
event.comments = oldComments;
} else {
// we have deleted a thread itself
event.comments = oldComments.filter((reply) => reply.id !== deletedCommentId);
event.comments = oldComments.filter(
(reply) => reply.id !== deletedCommentId
);
}
store.writeQuery({
query: COMMENTS_THREADS,
@ -276,14 +305,18 @@ export default class CommentTree extends Vue {
.filter((comment) => comment.inReplyToComment == null)
.sort((a, b) => {
if (a.updatedAt && b.updatedAt) {
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
return (
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
);
}
return 0;
});
}
get filteredOrderedComments(): IComment[] {
return this.orderedComments.filter((comment) => !comment.deletedAt || comment.totalReplies > 0);
return this.orderedComments.filter(
(comment) => !comment.deletedAt || comment.totalReplies > 0
);
}
get isEventOrganiser(): boolean {
@ -302,7 +335,7 @@ export default class CommentTree extends Vue {
}
get isAbleToComment(): boolean {
if (this.currentActor.id) {
if (this.currentActor && this.currentActor.id) {
return this.areCommentsClosed || this.isEventOrganiser;
}
return false;

View File

@ -15,7 +15,10 @@
<span v-else class="name comment-link has-text-grey">
{{ $t("[deleted]") }}
</span>
<span class="icons" v-if="!comment.deletedAt && comment.actor.id === currentActor.id">
<span
class="icons"
v-if="!comment.deletedAt && comment.actor.id === currentActor.id"
>
<b-dropdown aria-role="list">
<b-icon slot="trigger" role="button" icon="dots-horizontal" />
@ -44,8 +47,9 @@
<div class="post-infos">
<span :title="comment.insertedAt | formatDateTimeString">
{{
formatDistanceToNow(new Date(comment.updatedAt), { locale: $dateFnsLocale }) ||
$t("Right now")
formatDistanceToNow(new Date(comment.updatedAt), {
locale: $dateFnsLocale,
}) || $t("Right now")
}}</span
>
</div>
@ -77,7 +81,9 @@
type="is-primary"
>{{ $t("Update") }}</b-button
>
<b-button native-type="button" @click="toggleEditMode">{{ $t("Cancel") }}</b-button>
<b-button native-type="button" @click="toggleEditMode">{{
$t("Cancel")
}}</b-button>
</div>
</form>
</div>
@ -95,7 +101,8 @@ import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
currentActor: CURRENT_ACTOR_CLIENT,
},
components: {
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
editor: () =>
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
},
})
export default class DiscussionComment extends Vue {

View File

@ -1,14 +1,23 @@
<template>
<router-link
class="discussion-minimalist-card-wrapper"
:to="{ name: RouteName.DISCUSSION, params: { slug: discussion.slug, id: discussion.id } }"
:to="{
name: RouteName.DISCUSSION,
params: { slug: discussion.slug, id: discussion.id },
}"
>
<div class="media-left">
<figure
class="image is-32x32"
v-if="discussion.lastComment.actor && discussion.lastComment.actor.avatar"
v-if="
discussion.lastComment.actor && discussion.lastComment.actor.avatar
"
>
<img class="is-rounded" :src="discussion.lastComment.actor.avatar.url" alt />
<img
class="is-rounded"
:src="discussion.lastComment.actor.avatar.url"
alt
/>
</figure>
<b-icon v-else size="is-medium" icon="account-circle" />
</div>
@ -17,15 +26,18 @@
<p class="discussion-minimalist-title">{{ discussion.title }}</p>
<span :title="actualDate | formatDateTimeString">
{{
formatDistanceToNowStrict(new Date(actualDate), { locale: $dateFnsLocale }) ||
$t("Right now")
formatDistanceToNowStrict(new Date(actualDate), {
locale: $dateFnsLocale,
}) || $t("Right now")
}}</span
>
</div>
<div class="has-text-grey" v-if="!discussion.lastComment.deletedAt">
{{ htmlTextEllipsis }}
</div>
<div v-else class="has-text-grey">{{ $t("[This comment has been deleted]") }}</div>
<div v-else class="has-text-grey">
{{ $t("[This comment has been deleted]") }}
</div>
</div>
</router-link>
</template>
@ -54,7 +66,10 @@ export default class DiscussionListItem extends Vue {
}
get actualDate(): string | Date | undefined {
if (this.discussion.updatedAt === this.discussion.insertedAt && this.discussion.lastComment) {
if (
this.discussion.updatedAt === this.discussion.insertedAt &&
this.discussion.lastComment
) {
return this.discussion.lastComment.publishedAt;
}
return this.discussion.updatedAt;
@ -83,7 +98,8 @@ export default class DiscussionListItem extends Vue {
.discussion-minimalist-title {
color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica,
Arial, serif;
font-size: 1.25rem;
font-weight: 700;
flex: 1;

View File

@ -117,11 +117,21 @@
<b-icon icon="format-quote-close" />
</button>
<button v-if="!isBasicMode" class="menubar__button" @click="commands.undo" type="button">
<button
v-if="!isBasicMode"
class="menubar__button"
@click="commands.undo"
type="button"
>
<b-icon icon="undo" />
</button>
<button v-if="!isBasicMode" class="menubar__button" @click="commands.redo" type="button">
<button
v-if="!isBasicMode"
class="menubar__button"
@click="commands.redo"
type="button"
>
<b-icon icon="redo" />
</button>
</div>
@ -181,7 +191,9 @@
</div>
</div>
</template>
<div v-else class="suggestion-list__item is-empty">{{ $t("No profiles found") }}</div>
<div v-else class="suggestion-list__item is-empty">
{{ $t("No profiles found") }}
</div>
</div>
</div>
</template>
@ -212,7 +224,7 @@ import { SEARCH_PERSONS } from "../graphql/search";
import { Actor, IActor, IPerson } from "../types/actor";
import Image from "./Editor/Image";
import MaxSize from "./Editor/MaxSize";
import { UPLOAD_PICTURE } from "../graphql/upload";
import { UPLOAD_MEDIA } from "../graphql/upload";
import { listenFileUpload } from "../utils/upload";
import { CURRENT_ACTOR_CLIENT } from "../graphql/actor";
import { IComment } from "../types/comment.model";
@ -372,7 +384,7 @@ export default class EditorComponent extends Vue {
searchText: query,
},
});
// TODO: TipTap doesn't handle async for onFilter, hence the following line.
// TipTap doesn't handle async for onFilter, hence the following line.
this.filteredActors = result.data.searchPersons.elements;
return this.filteredActors;
},
@ -395,6 +407,7 @@ export default class EditorComponent extends Vue {
new Image(),
new MaxSize({ maxSize: this.maxSize }),
],
// eslint-disable-next-line @typescript-eslint/ban-types
onUpdate: ({ getHTML }: { getHTML: Function }) => {
this.$emit("input", getHTML());
},
@ -410,6 +423,7 @@ export default class EditorComponent extends Vue {
}
}
// eslint-disable-next-line @typescript-eslint/ban-types
showLinkMenu(command: Function, active: boolean): Function | undefined {
if (!this.editor) return undefined;
if (active) return command({ href: null });
@ -430,7 +444,8 @@ export default class EditorComponent extends Vue {
upHandler(): void {
this.navigatedActorIndex =
(this.navigatedActorIndex + this.filteredActors.length - 1) % this.filteredActors.length;
(this.navigatedActorIndex + this.filteredActors.length - 1) %
this.filteredActors.length;
}
/**
@ -438,7 +453,8 @@ export default class EditorComponent extends Vue {
* if it's the last item, navigate to the first one
*/
downHandler(): void {
this.navigatedActorIndex = (this.navigatedActorIndex + 1) % this.filteredActors.length;
this.navigatedActorIndex =
(this.navigatedActorIndex + 1) % this.filteredActors.length;
}
enterHandler(): void {
@ -522,19 +538,22 @@ export default class EditorComponent extends Vue {
* Show a file prompt, upload picture and insert it into editor
* @param command
*/
// eslint-disable-next-line @typescript-eslint/ban-types
async showImagePrompt(command: Function): Promise<void> {
const image = await listenFileUpload();
try {
const { data } = await this.$apollo.mutate({
mutation: UPLOAD_PICTURE,
mutation: UPLOAD_MEDIA,
variables: {
file: image,
name: image.name,
actorId: this.currentActor.id,
},
});
if (data.uploadPicture && data.uploadPicture.url) {
command({ src: data.uploadPicture.url });
if (data.uploadMedia && data.uploadMedia.url) {
command({
src: data.uploadMedia.url,
"data-media-id": data.uploadMedia.id,
});
}
} catch (error) {
console.error(error);

View File

@ -1,6 +1,7 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { Node } from "tiptap";
import { UPLOAD_PICTURE } from "@/graphql/upload";
import { UPLOAD_MEDIA } from "@/graphql/upload";
import apolloProvider from "@/vue-apollo";
import ApolloClient from "apollo-client";
import { NormalizedCacheObject } from "apollo-cache-inmemory";
@ -12,6 +13,7 @@ import { EditorView } from "prosemirror-view";
/* eslint-disable class-methods-use-this */
export default class Image extends Node {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
get name() {
return "image";
}
@ -27,16 +29,18 @@ export default class Image extends Node {
title: {
default: null,
},
"data-media-id": {},
},
group: "inline",
draggable: true,
parseDOM: [
{
tag: "img[src]",
tag: "img",
getAttrs: (dom: any) => ({
src: dom.getAttribute("src"),
title: dom.getAttribute("title"),
alt: dom.getAttribute("alt"),
"data-media-id": dom.getAttribute("data-media-id"),
}),
},
],
@ -45,15 +49,21 @@ export default class Image extends Node {
}
commands({ type }: { type: NodeType }): any {
return (attrs: { [key: string]: string }) => (state: EditorState, dispatch: DispatchFn) => {
return (attrs: { [key: string]: string }) => (
state: EditorState,
dispatch: DispatchFn
) => {
const { selection }: { selection: TextSelection } = state;
const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos;
const position = selection.$cursor
? selection.$cursor.pos
: selection.$to.pos;
const node = type.create(attrs);
const transaction = state.tr.insert(position, node);
dispatch(transaction);
};
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
get plugins() {
return [
new Plugin({
@ -72,7 +82,8 @@ export default class Image extends Node {
}
const images = Array.from(realEvent.dataTransfer.files).filter(
(file: any) => /image/i.test(file.type) && !/svg/i.test(file.type)
(file: any) =>
/image/i.test(file.type) && !/svg/i.test(file.type)
);
if (images.length === 0) {
@ -88,21 +99,24 @@ export default class Image extends Node {
});
if (!coordinates) return false;
const client = apolloProvider.defaultClient as ApolloClient<NormalizedCacheObject>;
const editorElem = document.getElementById("tiptab-editor");
const actorId = editorElem && editorElem.dataset.actorId;
try {
images.forEach(async (image) => {
const { data } = await client.mutate({
mutation: UPLOAD_PICTURE,
mutation: UPLOAD_MEDIA,
variables: {
actorId,
file: image,
name: image.name,
},
});
const node = schema.nodes.image.create({ src: data.uploadPicture.url });
const transaction = view.state.tr.insert(coordinates.pos, node);
const node = schema.nodes.image.create({
src: data.uploadMedia.url,
"data-media-id": data.uploadMedia.id,
});
const transaction = view.state.tr.insert(
coordinates.pos,
node
);
view.dispatch(transaction);
});
return true;

View File

@ -1,17 +1,23 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { Extension, Plugin } from "tiptap";
export default class MaxSize extends Extension {
// eslint-disable-next-line class-methods-use-this
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
get name() {
return "maxSize";
}
// eslint-disable-next-line class-methods-use-this
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
get defaultOptions() {
return {
maxSize: null,
};
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
get plugins() {
return [
new Plugin({
@ -21,7 +27,7 @@ export default class MaxSize extends Extension {
const newLength = newState.doc.content.size;
if (newLength > max && newLength > oldLength) {
let newTr = newState.tr;
const newTr = newState.tr;
newTr.insertText("", max + 1, newLength);
return newTr;

View File

@ -21,9 +21,13 @@
</b-autocomplete>
</b-field>
<b-field v-if="isSecureContext()">
<b-button type="is-text" v-if="!gettingLocation" icon-right="target" @click="locateMe">{{
$t("Use my location")
}}</b-button>
<b-button
type="is-text"
v-if="!gettingLocation"
icon-right="target"
@click="locateMe"
>{{ $t("Use my location") }}</b-button
>
<span v-else>{{ $t("Getting location") }}</span>
</b-field>
<!--
@ -50,7 +54,7 @@
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { LatLng } from "leaflet";
import { debounce } from "lodash";
import { debounce, DebouncedFunc } from "lodash";
import { Address, IAddress } from "../../types/address.model";
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
import { CONFIG } from "../../graphql/config";
@ -58,7 +62,8 @@ import { IConfig } from "../../types/config.model";
@Component({
components: {
"map-leaflet": () => import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
"map-leaflet": () =>
import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
},
apollo: {
config: CONFIG,
@ -81,7 +86,8 @@ export default class AddressAutoComplete extends Vue {
private gettingLocation = false;
private location!: Position;
// eslint-disable-next-line no-undef
private location!: GeolocationPosition;
private gettingLocationError: any;
@ -89,7 +95,7 @@ export default class AddressAutoComplete extends Vue {
config!: IConfig;
fetchAsyncData!: Function;
fetchAsyncData!: DebouncedFunc<(query: string) => Promise<void>>;
// We put this in data because of issues like
// https://github.com/vuejs/vue-class-component/issues/263
@ -121,7 +127,9 @@ export default class AddressAutoComplete extends Vue {
},
});
this.addressData = result.data.searchAddress.map((address: IAddress) => new Address(address));
this.addressData = result.data.searchAddress.map(
(address: IAddress) => new Address(address)
);
this.isFetching = false;
}
@ -174,7 +182,9 @@ export default class AddressAutoComplete extends Vue {
},
});
this.addressData = result.data.reverseGeocode.map((address: IAddress) => new Address(address));
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;
@ -197,7 +207,10 @@ export default class AddressAutoComplete extends Vue {
this.location = await AddressAutoComplete.getLocation();
this.mapDefaultZoom = 12;
this.reverseGeoCode(
new LatLng(this.location.coords.latitude, this.location.coords.longitude),
new LatLng(
this.location.coords.latitude,
this.location.coords.longitude
),
12
);
} catch (e) {
@ -207,7 +220,8 @@ export default class AddressAutoComplete extends Vue {
this.gettingLocation = false;
}
static async getLocation(): Promise<Position> {
// 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."));

View File

@ -1,141 +0,0 @@
<docs>
### Datetime Picker
> We're wrapping the Buefy datepicker & an input
### Defaults
- step: 10
### Example
```vue
<DateTimePicker :value="new Date()" />
```
</docs>
<template>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">{{ label }}</label>
</div>
<div class="field-body">
<div class="field is-narrow is-grouped calendar-picker">
<b-datepicker
:day-names="localeShortWeekDayNamesProxy"
:month-names="localeMonthNamesProxy"
:first-day-of-week="parseInt($t('firstDayOfWeek'), 10)"
:min-date="minDatetime"
:max-date="maxDatetime"
v-model="dateWithTime"
:placeholder="$t('Click to select')"
:years-range="[-2, 10]"
icon="calendar"
class="is-narrow"
/>
<b-timepicker
placeholder="Type or select a time..."
icon="clock"
v-model="dateWithTime"
:min-time="minTime"
:max-time="maxTime"
size="is-small"
inline
>
</b-timepicker>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { localeMonthNames, localeShortWeekDayNames } from "@/utils/datetime";
@Component
export default class DateTimePicker extends Vue {
/**
* @model
* The DateTime value
*/
@Prop({ required: true, type: Date, default: () => new Date() }) value!: Date;
/**
* What's shown besides the picker
*/
@Prop({ required: false, type: String, default: "Datetime" }) label!: string;
/**
* The step for the time input
*/
@Prop({ required: false, type: Number, default: 1 }) step!: number;
/**
* Earliest date available for selection
*/
@Prop({ required: false, type: Date, default: null }) minDatetime!: Date;
/**
* Latest date available for selection
*/
@Prop({ required: false, type: Date, default: null }) maxDatetime!: Date;
dateWithTime: Date = this.value;
localeShortWeekDayNamesProxy = localeShortWeekDayNames();
localeMonthNamesProxy = localeMonthNames();
@Watch("value")
updateValue(): void {
this.dateWithTime = this.value;
}
@Watch("dateWithTime")
updateDateWithTimeWatcher(): void {
this.updateDateTime();
}
updateDateTime(): void {
/**
* Returns the updated date
*
* @type {Date}
*/
this.$emit("input", this.dateWithTime);
}
get minTime(): Date | null {
if (this.minDatetime && this.datesAreOnSameDay(this.dateWithTime, this.minDatetime)) {
return this.minDatetime;
}
return null;
}
get maxTime(): Date | null {
if (this.maxDatetime && this.datesAreOnSameDay(this.dateWithTime, this.maxDatetime)) {
return this.maxDatetime;
}
return null;
}
// eslint-disable-next-line class-methods-use-this
private datesAreOnSameDay(first: Date, second: Date): boolean {
return (
first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate()
);
}
}
</script>
<style lang="scss" scoped>
.timepicker {
::v-deep .dropdown-content {
padding: 0;
}
}
.calendar-picker {
::v-deep .dropdown-menu {
z-index: 200;
}
}
</style>

View File

@ -1,5 +1,8 @@
<template>
<router-link class="card" :to="{ name: 'Event', params: { uuid: event.uuid } }">
<router-link
class="card"
:to="{ name: 'Event', params: { uuid: event.uuid } }"
>
<div class="card-image">
<figure
class="image is-16by9"
@ -7,10 +10,19 @@
event.picture ? event.picture.url : '/img/mobilizon_default_card.png'
}')`"
>
<div class="tag-container" v-if="event.tags">
<div
class="tag-container"
v-if="event.tags || event.status !== EventStatus.CONFIRMED"
>
<b-tag type="is-info" v-if="event.status === EventStatus.TENTATIVE">
{{ $t("Tentative") }}
</b-tag>
<b-tag type="is-danger" v-if="event.status === EventStatus.CANCELLED">
{{ $t("Cancelled") }}
</b-tag>
<router-link
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
v-for="tag in event.tags.slice(0, 3)"
v-for="tag in (event.tags || []).slice(0, 3)"
:key="tag.slug"
>
<b-tag type="is-light">{{ tag.title }}</b-tag>
@ -21,14 +33,18 @@
<div class="card-content">
<div class="media">
<div class="media-left">
<date-calendar-icon v-if="!mergedOptions.hideDate" :date="event.beginsOn" />
<date-calendar-icon
v-if="!mergedOptions.hideDate"
:date="event.beginsOn"
/>
</div>
<div class="media-content">
<p class="event-title">{{ event.title }}</p>
<div class="event-subtitle" v-if="event.physicalAddress">
<!-- <p>{{ $t('By @{username}', { username: actor.preferredUsername }) }}</p>-->
<span>
{{ event.physicalAddress.description }}, {{ event.physicalAddress.locality }}
{{ event.physicalAddress.description }},
{{ event.physicalAddress.locality }}
</span>
</div>
</div>
@ -77,7 +93,7 @@ import { IEvent, IEventCardOptions } from "@/types/event.model";
import { Component, Prop, Vue } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { Actor, Person } from "@/types/actor";
import { ParticipantRole } from "../../types/participant.model";
import { EventStatus, ParticipantRole } from "@/types/enums";
import RouteName from "../../router/name";
@Component({
@ -92,6 +108,8 @@ export default class EventCard extends Vue {
ParticipantRole = ParticipantRole;
EventStatus = EventStatus;
RouteName = RouteName;
defaultOptions: IEventCardOptions = {

View File

@ -18,7 +18,9 @@
</docs>
<template>
<span v-if="!endsOn">{{ beginsOn | formatDateTimeString(showStartTime) }}</span>
<span v-if="!endsOn">{{
beginsOn | formatDateTimeString(showStartTime)
}}</span>
<span v-else-if="isSameDay() && showStartTime && showEndTime">
{{
$t("On {date} from {startTime} to {endTime}", {
@ -44,7 +46,9 @@
})
}}
</span>
<span v-else-if="isSameDay()">{{ $t("On {date}", { date: formatDate(beginsOn) }) }}</span>
<span v-else-if="isSameDay()">{{
$t("On {date}", { date: formatDate(beginsOn) })
}}</span>
<span v-else-if="endsOn && showStartTime && showEndTime">
{{
$t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
@ -97,7 +101,9 @@ export default class EventFullDate extends Vue {
}
isSameDay(): boolean {
const sameDay = new Date(this.beginsOn).toDateString() === new Date(this.endsOn).toDateString();
const sameDay =
new Date(this.beginsOn).toDateString() ===
new Date(this.endsOn).toDateString();
return this.endsOn !== undefined && sameDay;
}
}

View File

@ -1,38 +1,49 @@
<template>
<article class="box">
<div class="columns">
<div class="content column">
<div class="identity-header">
<figure class="image is-24x24" v-if="participation.actor.avatar">
<img class="is-rounded" :src="participation.actor.avatar.url" alt="" />
</figure>
{{ displayNameAndUsername(participation.actor) }}
</div>
<div class="list-card">
<div class="content">
<div class="title-wrapper">
<div class="columns">
<div class="column is-narrow">
<div class="date-component">
<date-calendar-icon :date="participation.event.beginsOn" />
</div>
</div>
<div class="column">
<router-link
:to="{ name: RouteName.EVENT, params: { uuid: participation.event.uuid } }"
>
<h3 class="title event-title-card">{{ participation.event.title }}</h3>
</router-link>
</div>
<div class="date-component">
<date-calendar-icon :date="participation.event.beginsOn" />
</div>
<router-link
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
>
<h3 class="title">{{ participation.event.title }}</h3>
</router-link>
</div>
<div class="participation-actor has-text-grey">
<span>
<b-icon icon="earth" v-if="participation.event.visibility === EventVisibility.PUBLIC" />
<b-icon
icon="earth"
v-if="participation.event.visibility === EventVisibility.PUBLIC"
/>
<b-icon
icon="link"
v-else-if="participation.event.visibility === EventVisibility.UNLISTED"
v-else-if="
participation.event.visibility === EventVisibility.UNLISTED
"
/>
<b-icon
icon="lock"
v-else-if="participation.event.visibility === EventVisibility.PRIVATE"
v-else-if="
participation.event.visibility === EventVisibility.PRIVATE
"
/>
</span>
<span
v-if="
participation.event.physicalAddress && participation.event.physicalAddress.locality
participation.event.physicalAddress &&
participation.event.physicalAddress.locality
"
>{{ participation.event.physicalAddress.locality }} -</span
>
@ -40,19 +51,10 @@
<i18n tag="span" path="Organized by {name}">
<popover-actor-card
slot="name"
:actor="participation.event.organizerActor"
:actor="organizerActor"
:inline="true"
>
{{ participation.event.organizerActor.displayName() }}
</popover-actor-card>
</i18n>
<i18n
v-if="participation.role === ParticipantRole.PARTICIPANT"
path="Going as {name}"
tag="span"
>
<popover-actor-card slot="name" :actor="participation.actor" :inline="true">
{{ participation.actor.displayName() }}
{{ organizerActor.displayName() }}
</popover-actor-card>
</i18n>
</span>
@ -61,12 +63,15 @@
<span
class="participant-stats"
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
<span v-if="participation.event.options.maximumAttendeeCapacity !== 0">
<span
v-if="participation.event.options.maximumAttendeeCapacity !== 0"
>
{{
$tc(
"{available}/{capacity} available places",
@ -76,16 +81,21 @@
available:
participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant,
capacity: participation.event.options.maximumAttendeeCapacity,
capacity:
participation.event.options.maximumAttendeeCapacity,
}
)
}}
</span>
<span v-else>
{{
$tc("{count} participants", participation.event.participantStats.participant, {
count: participation.event.participantStats.participant,
})
$tc(
"{count} participants",
participation.event.participantStats.participant,
{
count: participation.event.participantStats.participant,
}
)
}}
</span>
<span v-if="participation.event.participantStats.notApproved > 0">
@ -111,80 +121,90 @@
</span>
</div>
</div>
<div class="actions column is-narrow">
<ul>
<li
<div class="actions">
<b-dropdown aria-role="list" position="is-bottom-left">
<b-button slot="trigger" role="button" icon-right="dots-horizontal">
{{ $t("Actions") }}
</b-button>
<b-dropdown-item
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.EDIT_EVENT,
params: { eventId: participation.event.uuid },
})
"
>
<b-button
type="is-text"
@click="
gotToWithCheck(participation, {
name: RouteName.EDIT_EVENT,
params: { eventId: participation.event.uuid },
})
"
icon-left="pencil"
>{{ $t("Edit") }}
</b-button>
</li>
<li v-if="participation.role === ParticipantRole.CREATOR">
<b-button
type="is-text"
@click="
gotToWithCheck(participation, {
name: RouteName.DUPLICATE_EVENT,
params: { eventId: participation.event.uuid },
})
"
icon-left="content-duplicate"
>
{{ $t("Duplicate") }}
</b-button>
</li>
<li
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
<b-icon icon="pencil" />
{{ $t("Edit") }}
</b-dropdown-item>
<b-dropdown-item
v-if="participation.role === ParticipantRole.CREATOR"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.DUPLICATE_EVENT,
params: { eventId: participation.event.uuid },
})
"
>
<b-icon icon="content-duplicate" />
{{ $t("Duplicate") }}
</b-dropdown-item>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="openDeleteEventModalWrapper"
>
<b-button type="is-text" icon-left="delete">{{ $t("Delete") }} </b-button>
</li>
<li
<b-icon icon="delete" />
{{ $t("Delete") }}
</b-dropdown-item>
<b-dropdown-item
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS,
params: { eventId: participation.event.uuid },
})
"
>
<b-button
type="is-text"
@click="
gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS,
params: { eventId: participation.event.uuid },
})
"
icon-left="account-multiple-plus"
>{{ $t("Manage participations") }}
</b-button>
</li>
<li>
<b-button
tag="router-link"
icon-left="view-compact"
type="is-text"
:to="{ name: RouteName.EVENT, params: { uuid: participation.event.uuid } }"
>{{ $t("View event page") }}
</b-button>
</li>
</ul>
<b-icon icon="account-multiple-plus" />
{{ $t("Manage participations") }}
</b-dropdown-item>
<b-dropdown-item aria-role="listitem" has-link>
<router-link
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
>
<b-icon icon="view-compact" />
{{ $t("View event page") }}
</router-link>
</b-dropdown-item>
</b-dropdown>
</div>
</div>
</article>
@ -195,9 +215,10 @@ import { Component, Prop } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { mixins } from "vue-class-component";
import { RawLocation, Route } from "vue-router";
import { IParticipant, ParticipantRole } from "../../types/participant.model";
import { EventVisibility, IEventCardOptions } from "../../types/event.model";
import { IPerson } from "../../types/actor";
import { EventVisibility, ParticipantRole } from "@/types/enums";
import { IParticipant } from "../../types/participant.model";
import { IEventCardOptions } from "../../types/event.model";
import { displayNameAndUsername, IActor, IPerson } from "../../types/actor";
import ActorMixin from "../../mixins/actor";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import EventMixin from "../../mixins/event";
@ -242,6 +263,8 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
EventVisibility = EventVisibility;
displayNameAndUsername = displayNameAndUsername;
RouteName = RouteName;
get mergedOptions(): IEventCardOptions {
@ -254,11 +277,17 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
async openDeleteEventModalWrapper(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await this.openDeleteEventModal(this.participation.event, this.currentActor);
await this.openDeleteEventModal(this.participation.event);
}
async gotToWithCheck(participation: IParticipant, route: RawLocation): Promise<Route> {
if (participation.actor.id !== this.currentActor.id && participation.event.organizerActor) {
async gotToWithCheck(
participation: IParticipant,
route: RawLocation
): Promise<Route> {
if (
participation.actor.id !== this.currentActor.id &&
participation.event.organizerActor
) {
const organizer = participation.event.organizerActor as IPerson;
await changeIdentity(this.$apollo.provider.defaultClient, organizer);
this.$buefy.notification.open({
@ -275,6 +304,16 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
}
return this.$router.push(route);
}
get organizerActor(): IActor | undefined {
if (
this.participation.event.attributedTo &&
this.participation.event.attributedTo.id
) {
return this.participation.event.attributedTo;
}
return this.participation.event.organizerActor;
}
}
</script>
@ -310,6 +349,34 @@ article.box {
.actions {
padding-right: 7.5px;
cursor: pointer;
ul li {
margin: 0 auto;
.is-link {
cursor: pointer;
}
.button.is-text {
text-decoration: none;
::v-deep span:first-child i.mdi::before {
font-size: 24px !important;
}
::v-deep span:last-child {
padding-left: 4px;
}
&:hover {
background: #f5f5f5;
}
}
* {
font-size: 0.8rem;
color: $background-color;
}
}
}
div.content {
@ -352,38 +419,22 @@ article.box {
}
}
}
}
.actions {
ul li {
margin: 0 auto;
.identity-header {
background: $yellow-2;
display: flex;
padding: 5px;
.is-link {
cursor: pointer;
}
.button.is-text {
text-decoration: none;
::v-deep span:first-child i.mdi::before {
font-size: 24px !important;
}
::v-deep span:last-child {
padding-left: 4px;
}
&:hover {
background: #f5f5f5;
}
}
* {
font-size: 0.8rem;
color: $background-color;
}
}
figure {
padding-right: 3px;
}
}
& > .columns {
padding: 1.25rem;
}
padding: 0;
}
.content h3.event-title-card {
line-height: 1em;

View File

@ -6,7 +6,9 @@
<div class="date-component">
<date-calendar-icon :date="event.beginsOn" />
</div>
<router-link :to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }">
<router-link
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
>
<h2 class="title">{{ event.title }}</h2>
</router-link>
</div>
@ -15,17 +17,34 @@
{{ event.physicalAddress.locality }}
</span>
<span v-if="event.attributedTo && options.memberofGroup">
{{ $t("Created by {name}", { name: usernameWithDomain(event.organizerActor) }) }}
{{
$t("Created by {name}", {
name: usernameWithDomain(event.organizerActor),
})
}}
</span>
<span v-else-if="options.memberofGroup">
{{ $t("Organized by {name}", { name: usernameWithDomain(event.organizerActor) }) }}
{{
$t("Organized by {name}", {
name: usernameWithDomain(event.organizerActor),
})
}}
</span>
</div>
<div class="columns">
<span class="column is-narrow">
<b-icon icon="earth" v-if="event.visibility === EventVisibility.PUBLIC" />
<b-icon icon="link" v-if="event.visibility === EventVisibility.UNLISTED" />
<b-icon icon="lock" v-if="event.visibility === EventVisibility.PRIVATE" />
<b-icon
icon="earth"
v-if="event.visibility === EventVisibility.PUBLIC"
/>
<b-icon
icon="link"
v-if="event.visibility === EventVisibility.UNLISTED"
/>
<b-icon
icon="lock"
v-if="event.visibility === EventVisibility.PRIVATE"
/>
</span>
<span class="column is-narrow participant-stats">
<span v-if="event.options.maximumAttendeeCapacity !== 0">
@ -38,9 +57,13 @@
</span>
<span v-else>
{{
$tc("{count} participants", event.participantStats.participant, {
count: event.participantStats.participant,
})
$tc(
"{count} participants",
event.participantStats.participant,
{
count: event.participantStats.participant,
}
)
}}
</span>
</span>
@ -51,7 +74,7 @@
</template>
<script lang="ts">
import { EventVisibility, IEventCardOptions, IEvent } from "@/types/event.model";
import { IEventCardOptions, IEvent } from "@/types/event.model";
import { Component, Prop } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { IPerson, usernameWithDomain } from "@/types/actor";
@ -59,7 +82,7 @@ import { mixins } from "vue-class-component";
import ActorMixin from "@/mixins/actor";
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import EventMixin from "@/mixins/event";
import { ParticipantRole } from "../../types/participant.model";
import { EventVisibility, ParticipantRole } from "@/types/enums";
import RouteName from "../../router/name";
const defaultOptions: IEventCardOptions = {

View File

@ -14,10 +14,12 @@
{{
$tc(
"{available}/{capacity} available places",
event.options.maximumAttendeeCapacity - event.participantStats.participant,
event.options.maximumAttendeeCapacity -
event.participantStats.participant,
{
available:
event.options.maximumAttendeeCapacity - event.participantStats.participant,
event.options.maximumAttendeeCapacity -
event.participantStats.participant,
capacity: event.options.maximumAttendeeCapacity,
}
)
@ -42,9 +44,13 @@
"
>
{{
$tc("{count} requests waiting", event.participantStats.notApproved, {
count: event.participantStats.notApproved,
})
$tc(
"{count} requests waiting",
event.participantStats.notApproved,
{
count: event.participantStats.notApproved,
}
)
}}
</b-button>
</span>
@ -56,7 +62,7 @@
import { Component, Prop, Vue } from "vue-property-decorator";
import { IEvent } from "@/types/event.model";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { ParticipantRole } from "../../types/participant.model";
import { ParticipantRole } from "@/types/enums";
import RouteName from "../../router/name";
@Component({
@ -88,7 +94,8 @@ export default class EventMinimalistCard extends Vue {
.event-minimalist-title {
color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-size: 1.25rem;
font-weight: 700;
}

View File

@ -33,9 +33,12 @@
<div v-else-if="queryText.length >= 3" class="is-enabled">
<span>{{ $t('No results for "{queryText}"') }}</span>
<span>{{
$t("You can try another search term or drag and drop the marker on the map", {
queryText,
})
$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>-->
@ -102,7 +105,7 @@
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { LatLng } from "leaflet";
import { debounce } from "lodash";
import { debounce, DebouncedFunc } from "lodash";
import { Address, IAddress } from "../../types/address.model";
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
import { CONFIG } from "../../graphql/config";
@ -110,7 +113,8 @@ import { IConfig } from "../../types/config.model";
@Component({
components: {
"map-leaflet": () => import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
"map-leaflet": () =>
import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
},
apollo: {
config: CONFIG,
@ -133,7 +137,8 @@ export default class FullAddressAutoComplete extends Vue {
private gettingLocation = false;
private location!: Position;
// eslint-disable-next-line no-undef
private location!: GeolocationPosition;
private gettingLocationError: any;
@ -141,11 +146,11 @@ export default class FullAddressAutoComplete extends Vue {
config!: IConfig;
fetchAsyncData!: Function;
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() {
data(): Record<string, unknown> {
return {
fetchAsyncData: debounce(this.asyncData, 200),
};
@ -173,7 +178,9 @@ export default class FullAddressAutoComplete extends Vue {
},
});
this.addressData = result.data.searchAddress.map((address: IAddress) => new Address(address));
this.addressData = result.data.searchAddress.map(
(address: IAddress) => new Address(address)
);
this.isFetching = false;
}
@ -224,7 +231,9 @@ export default class FullAddressAutoComplete extends Vue {
},
});
this.addressData = result.data.reverseGeocode.map((address: IAddress) => new Address(address));
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;
@ -248,7 +257,10 @@ export default class FullAddressAutoComplete extends Vue {
this.location = await FullAddressAutoComplete.getLocation();
this.mapDefaultZoom = 12;
this.reverseGeoCode(
new LatLng(this.location.coords.latitude, this.location.coords.longitude),
new LatLng(
this.location.coords.latitude,
this.location.coords.longitude
),
12
);
} catch (e) {
@ -266,7 +278,8 @@ export default class FullAddressAutoComplete extends Vue {
return window.isSecureContext;
}
static async getLocation(): Promise<Position> {
// 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."));

View File

@ -10,9 +10,18 @@
>
<div class="media">
<figure class="image is-48x48" v-if="availableActor.avatar">
<img class="media-left is-rounded" :src="availableActor.avatar.url" alt="" />
<img
class="media-left is-rounded"
:src="availableActor.avatar.url"
alt=""
/>
</figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
<div class="media-content">
<h3>{{ availableActor.name }}</h3>
<small>{{ `@${availableActor.preferredUsername}` }}</small>
@ -23,9 +32,11 @@
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { IMember, IPerson, MemberRole, IActor, Actor } from "@/types/actor";
import { IPerson, IActor, Actor } from "@/types/actor";
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import { Paginate } from "@/types/paginate";
import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums";
@Component({
apollo: {
@ -59,16 +70,21 @@ export default class OrganizerPicker extends Vue {
get actualMemberships(): IMember[] {
if (this.restrictModeratorLevel) {
return this.groupMemberships.elements.filter((membership: IMember) =>
[MemberRole.ADMINISTRATOR, MemberRole.MODERATOR, MemberRole.CREATOR].includes(
membership.role
)
[
MemberRole.ADMINISTRATOR,
MemberRole.MODERATOR,
MemberRole.CREATOR,
].includes(membership.role)
);
}
return this.groupMemberships.elements;
}
get actualAvailableActors(): IActor[] {
return [this.identity, ...this.actualMemberships.map((member) => member.parent)];
return [
this.identity,
...this.actualMemberships.map((member) => member.parent),
];
}
@Watch("currentActor")

View File

@ -1,7 +1,11 @@
<template>
<div class="organizer-picker">
<!-- If we have a current actor (inline) -->
<div v-if="inline && currentActor.id" class="inline box" @click="isComponentModalActive = true">
<div
v-if="inline && currentActor.id"
class="inline box"
@click="isComponentModalActive = true"
>
<div class="media">
<div class="media-left">
<figure class="image is-48x48" v-if="currentActor.avatar">
@ -15,7 +19,9 @@
</div>
<div class="media-content" v-if="currentActor.name">
<p class="is-4">{{ currentActor.name }}</p>
<p class="is-6 has-text-grey">{{ `@${currentActor.preferredUsername}` }}</p>
<p class="is-6 has-text-grey">
{{ `@${currentActor.preferredUsername}` }}
</p>
</div>
<div class="media-content" v-else>
{{ `@${currentActor.preferredUsername}` }}
@ -26,7 +32,11 @@
</div>
</div>
<!-- If we have a current actor -->
<span v-else-if="currentActor.id" class="block" @click="isComponentModalActive = true">
<span
v-else-if="currentActor.id"
class="block"
@click="isComponentModalActive = true"
>
<img
class="image is-48x48"
v-if="currentActor.avatar"
@ -40,13 +50,19 @@
<div class="media">
<div class="media-left">
<figure class="image is-48x48" v-if="identity.avatar">
<img class="image is-rounded" :src="identity.avatar.url" :alt="identity.avatar.alt" />
<img
class="image is-rounded"
:src="identity.avatar.url"
:alt="identity.avatar.alt"
/>
</figure>
<b-icon v-else size="is-large" icon="account-circle" />
</div>
<div class="media-content" v-if="identity.name">
<p class="is-4">{{ identity.name }}</p>
<p class="is-6 has-text-grey">{{ `@${identity.preferredUsername}` }}</p>
<p class="is-6 has-text-grey">
{{ `@${identity.preferredUsername}` }}
</p>
</div>
<div class="media-content" v-else>
{{ `@${identity.preferredUsername}` }}
@ -74,7 +90,11 @@
<div class="column">
<div v-if="actorMembersForCurrentActor.length > 0">
<p>{{ $t("Add a contact") }}</p>
<p class="field" v-for="actor in actorMembersForCurrentActor" :key="actor.id">
<p
class="field"
v-for="actor in actorMembersForCurrentActor"
:key="actor.id"
>
<b-checkbox v-model="actualContacts" :native-value="actor.id">
<div class="media">
<div class="media-left">
@ -89,7 +109,9 @@
</div>
<div class="media-content" v-if="actor.name">
<p class="is-4">{{ actor.name }}</p>
<p class="is-6 has-text-grey">{{ `@${actor.preferredUsername}` }}</p>
<p class="is-6 has-text-grey">
{{ `@${actor.preferredUsername}` }}
</p>
</div>
<div class="media-content" v-else>
{{ `@${actor.preferredUsername}` }}
@ -115,7 +137,8 @@
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { IActor, IGroup, IMember, IPerson } from "../../types/actor";
import { IMember } from "@/types/actor/member.model";
import { IActor, IGroup, IPerson } from "../../types/actor";
import OrganizerPicker from "./OrganizerPicker.vue";
import { PERSON_MEMBERSHIPS_WITH_MEMBERS } from "../../graphql/actor";
import { Paginate } from "../../types/paginate";
@ -150,7 +173,8 @@ export default class OrganizerPickerWrapper extends Vue {
groupMemberships: Paginate<IMember> = { elements: [], total: 0 };
@Prop({ type: Array, required: false, default: () => [] }) contacts!: IActor[];
@Prop({ type: Array, required: false, default: () => [] })
contacts!: IActor[];
actualContacts: (string | undefined)[] = this.contacts.map(({ id }) => id);
@ -171,7 +195,9 @@ export default class OrganizerPickerWrapper extends Vue {
pickActor(): void {
this.$emit(
"update:contacts",
this.actorMembersForCurrentActor.filter(({ id }) => this.actualContacts.includes(id))
this.actorMembersForCurrentActor.filter(({ id }) =>
this.actualContacts.includes(id)
)
);
this.$emit("input", this.currentActor);
this.isComponentModalActive = false;
@ -182,7 +208,9 @@ export default class OrganizerPickerWrapper extends Vue {
({ parent: { id } }) => id === this.currentActor.id
);
if (currentMembership) {
return currentMembership.parent.members.elements.map(({ actor }) => actor);
return currentMembership.parent.members.elements.map(
({ actor }: { actor: IActor }) => actor
);
}
return [];
}

View File

@ -47,8 +47,16 @@ A button to set your participation
>
</b-dropdown>
<div v-else-if="participation && participation.role === ParticipantRole.NOT_APPROVED">
<b-dropdown aria-role="list" position="is-bottom-left" class="dropdown-disabled">
<div
v-else-if="
participation && participation.role === ParticipantRole.NOT_APPROVED
"
>
<b-dropdown
aria-role="list"
position="is-bottom-left"
class="dropdown-disabled"
>
<button class="button is-success is-large" type="button" slot="trigger">
<b-icon icon="timer-sand-empty" />
<template>
@ -74,9 +82,17 @@ A button to set your participation
<small>{{ $t("Waiting for organization team approval.") }}</small>
</div>
<div v-else-if="participation && participation.role === ParticipantRole.REJECTED">
<div
v-else-if="
participation && participation.role === ParticipantRole.REJECTED
"
>
<span>
{{ $t("Unfortunately, your participation request was rejected by the organizers.") }}
{{
$t(
"Unfortunately, your participation request was rejected by the organizers."
)
}}
</span>
</div>
@ -92,7 +108,11 @@ A button to set your participation
<b-icon icon="menu-down" />
</button>
<b-dropdown-item :value="true" aria-role="listitem" @click="joinEvent(currentActor)">
<b-dropdown-item
:value="true"
aria-role="listitem"
@click="joinEvent(currentActor)"
>
<div class="media">
<div class="media-left">
<figure class="image is-32x32" v-if="currentActor.avatar">
@ -103,7 +123,8 @@ A button to set your participation
<span>
{{
$t("as {identity}", {
identity: currentActor.name || `@${currentActor.preferredUsername}`,
identity:
currentActor.name || `@${currentActor.preferredUsername}`,
})
}}
</span>
@ -121,7 +142,10 @@ A button to set your participation
</b-dropdown>
<b-button
tag="router-link"
:to="{ name: RouteName.EVENT_PARTICIPATE_LOGGED_OUT, params: { uuid: event.uuid } }"
:to="{
name: RouteName.EVENT_PARTICIPATE_LOGGED_OUT,
params: { uuid: event.uuid },
}"
v-else-if="!participation && hasAnonymousParticipationMethods"
type="is-primary"
size="is-large"
@ -130,7 +154,10 @@ A button to set your participation
>
<b-button
tag="router-link"
:to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT, params: { uuid: event.uuid } }"
:to="{
name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT,
params: { uuid: event.uuid },
}"
v-else-if="!currentActor.id"
type="is-primary"
size="is-large"
@ -142,8 +169,9 @@ A button to set your participation
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IParticipant, ParticipantRole } from "../../types/participant.model";
import { EventJoinOptions, IEvent } from "../../types/event.model";
import { EventJoinOptions, ParticipantRole } from "@/types/enums";
import { IParticipant } from "../../types/participant.model";
import { IEvent } from "../../types/event.model";
import { IPerson, Person } from "../../types/actor";
import { CURRENT_ACTOR_CLIENT, IDENTITIES } from "../../graphql/actor";
import { CURRENT_USER_CLIENT } from "../../graphql/user";
@ -161,7 +189,9 @@ import RouteName from "../../router/name";
identities: {
query: IDENTITIES,
update: ({ identities }) =>
identities ? identities.map((identity: IPerson) => new Person(identity)) : [],
identities
? identities.map((identity: IPerson) => new Person(identity))
: [],
skip() {
return this.currentUser.isLoggedIn === false;
},

View File

@ -59,7 +59,12 @@
<a :href="linkedInShareUrl" target="_blank" rel="nofollow noopener"
><b-icon icon="linkedin" size="is-large" type="is-primary"
/></a>
<a :href="diasporaShareUrl" class="diaspora" target="_blank" rel="nofollow noopener">
<a
:href="diasporaShareUrl"
class="diaspora"
target="_blank"
rel="nofollow noopener"
>
<span data-v-5e15e80a="" class="icon has-text-primary is-large">
<DiasporaLogo alt="diaspora-logo" />
</span>
@ -67,7 +72,6 @@
<a :href="emailShareUrl" target="_blank" rel="nofollow noopener"
><b-icon icon="email" size="is-large" type="is-primary"
/></a>
<!-- TODO: mailto: links are not used anymore, we should provide a popup to redact a message instead -->
</div>
</div>
</section>
@ -76,7 +80,9 @@
<script lang="ts">
import { Component, Prop, Vue, Ref } from "vue-property-decorator";
import { IEvent, EventVisibility, EventStatus } from "../../types/event.model";
import { EventStatus, EventVisibility } from "@/types/enums";
import { IEvent } from "../../types/event.model";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import DiasporaLogo from "../../assets/diaspora-icon.svg?inline";
@ -88,7 +94,8 @@ import DiasporaLogo from "../../assets/diaspora-icon.svg?inline";
export default class ShareEventModal extends Vue {
@Prop({ type: Object, required: true }) event!: IEvent;
@Prop({ type: Boolean, required: false, default: true }) eventCapacityOK!: boolean;
@Prop({ type: Boolean, required: false, default: true })
eventCapacityOK!: boolean;
@Ref("eventURLInput") readonly eventURLInput!: any;
@ -99,13 +106,15 @@ export default class ShareEventModal extends Vue {
showCopiedTooltip = false;
get twitterShareUrl(): string {
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(this.event.url)}&text=${
this.event.title
}`;
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(
this.event.url
)}&text=${this.event.title}`;
}
get facebookShareUrl(): string {
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(this.event.url)}`;
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
this.event.url
)}`;
}
get linkedInShareUrl(): string {
@ -124,7 +133,7 @@ export default class ShareEventModal extends Vue {
)}&url=${encodeURIComponent(this.event.url)}`;
}
copyURL() {
copyURL(): void {
this.eventURLInput.$refs.input.select();
document.execCommand("copy");
this.showCopiedTooltip = true;

View File

@ -4,7 +4,9 @@
{{ $t("Add some tags") }}
<b-tooltip
type="is-dark"
:label="$t('You can add tags by hitting the Enter key or by adding a comma')"
:label="
$t('You can add tags by hitting the Enter key or by adding a comma')
"
>
<b-icon size="is-small" icon="help-circle-outline"></b-icon>
</b-tooltip>
@ -40,7 +42,6 @@ import { ITag } from "../../types/tag.model";
if (typeof tag !== "string") {
return tag;
}
// @ts-ignore
return { title: tag, slug: tag } as ITag;
});
this.$emit("input", tagEntities);
@ -57,14 +58,14 @@ export default class TagInput extends Vue {
filteredTags: ITag[] = [];
getFilteredTags(text: string) {
getFilteredTags(text: string): void {
this.filteredTags = differenceBy(this.data, this.value, "id").filter(
(option) => get(option, this.path).toString().toLowerCase().indexOf(text.toLowerCase()) >= 0
(option) =>
get(option, this.path)
.toString()
.toLowerCase()
.indexOf(text.toLowerCase()) >= 0
);
}
static isTag(x: any): x is ITag {
return x.slug !== undefined;
}
}
</script>

View File

@ -1,9 +1,38 @@
<template>
<footer class="footer" ref="footer">
<img :src="`/img/pics/footer_${random}.jpg`" alt="" />
<picture>
<source
:srcset="`/img/pics/footer_${random}-1024w.webp 1x, /img/pics/footer_${random}-1920w.webp 2x`"
type="image/webp"
/>
<source
:srcset="`/img/pics/footer_${random}-1024w.jpg 1x, /img/pics/footer_${random}-1920w.jpg 2x`"
type="image/jpeg"
/>
<img
:src="`/img/pics/footer_${random}-1024w.jpg`"
alt=""
width="5234"
height="2189"
loading="lazy"
/>
</picture>
<ul>
<li>
<router-link :to="{ name: RouteName.ABOUT }">{{ $t("About") }}</router-link>
<b-select
v-if="$i18n"
v-model="locale"
:placeholder="$t('Select a language')"
>
<option v-for="(language, lang) in langs" :value="lang" :key="lang">
{{ language }}
</option>
</b-select>
</li>
<li>
<router-link :to="{ name: RouteName.ABOUT }">{{
$t("About")
}}</router-link>
</li>
<li>
<a href='https://www.chapril.org/Mentions-legales.html' >
@ -14,7 +43,10 @@
<router-link :to="{ name: RouteName.TERMS }">{{ $t("Terms") }}</router-link>
</li>
<li>
<a hreflang="en" href="https://framagit.org/framasoft/mobilizon/blob/master/LICENSE">
<a
hreflang="en"
href="https://framagit.org/framasoft/mobilizon/blob/master/LICENSE"
>
{{ $t("License") }}
</a>
</li>
@ -24,7 +56,9 @@
tag="span"
path="Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}."
>
<a slot="mobilizon" href="https://joinmobilizon.org">{{ $t("Mobilizon") }}</a>
<a slot="mobilizon" href="https://joinmobilizon.org">{{
$t("Mobilizon")
}}</a>
<span slot="date">{{ new Date().getFullYear() }}</span>
<a href="https://joinmobilizon.org/hall-of-fame" slot="contributors">{{
$t("more than 1360 contributors")
@ -34,17 +68,40 @@
</footer>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { Component, Vue, Watch } from "vue-property-decorator";
import { saveLocaleData } from "@/utils/auth";
import { loadLanguageAsync } from "@/utils/i18n";
import RouteName from "../router/name";
import langs from "../i18n/langs.json";
@Component
export default class Footer extends Vue {
RouteName = RouteName;
locale: string | null = this.$i18n.locale;
langs: Record<string, string> = langs;
// eslint-disable-next-line class-methods-use-this
get random(): number {
return Math.floor(Math.random() * 4) + 1;
}
@Watch("locale")
// eslint-disable-next-line class-methods-use-this
async updateLocale(locale: string): Promise<void> {
if (locale) {
await loadLanguageAsync(locale);
saveLocaleData(locale);
}
}
@Watch("$i18n.locale", { deep: true })
updateLocaleFromI18n(locale: string): void {
if (locale) {
this.locale = locale;
}
}
}
</script>
<style lang="scss" scoped>
@ -88,5 +145,10 @@ footer.footer {
text-decoration: underline;
text-decoration-color: $secondary;
}
::v-deep span.select select {
background: $background-color;
color: $white;
}
}
</style>

View File

@ -17,7 +17,9 @@
>
<h3>{{ group.name }}</h3>
<p class="is-6 has-text-grey">
<span v-if="group.domain">{{ `@${group.preferredUsername}@${group.domain}` }}</span>
<span v-if="group.domain">{{
`@${group.preferredUsername}@${group.domain}`
}}</span>
<span v-else>{{ `@${group.preferredUsername}` }}</span>
</p>
</router-link>

View File

@ -1,5 +1,11 @@
<template>
<div class="card">
<div class="identity-header">
<figure class="image is-24x24" v-if="member.actor.avatar">
<img class="is-rounded" :src="member.actor.avatar.url" alt="" />
</figure>
{{ displayNameAndUsername(member.actor) }}
</div>
<div class="card-content">
<div>
<div class="media">
@ -13,7 +19,9 @@
<router-link
:to="{
name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(member.parent) },
params: {
preferredUsername: usernameWithDomain(member.parent),
},
}"
>
<h3>{{ member.parent.name }}</h3>
@ -23,12 +31,16 @@
}}</span>
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
<b-taglist>
<b-tag type="is-info" v-if="member.role === MemberRole.ADMINISTRATOR">{{
$t("Administrator")
}}</b-tag>
<b-tag type="is-info" v-else-if="member.role === MemberRole.MODERATOR">{{
$t("Moderator")
}}</b-tag>
<b-tag
type="is-info"
v-if="member.role === MemberRole.ADMINISTRATOR"
>{{ $t("Administrator") }}</b-tag
>
<b-tag
type="is-info"
v-else-if="member.role === MemberRole.MODERATOR"
>{{ $t("Moderator") }}</b-tag
>
</b-taglist>
</p>
</router-link>
@ -54,7 +66,9 @@
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IMember, MemberRole, usernameWithDomain } from "@/types/actor";
import { displayNameAndUsername, usernameWithDomain } from "@/types/actor";
import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums";
import RouteName from "../../router/name";
@Component
@ -65,20 +79,34 @@ export default class GroupMemberCard extends Vue {
usernameWithDomain = usernameWithDomain;
displayNameAndUsername = displayNameAndUsername;
MemberRole = MemberRole;
}
</script>
<style lang="scss" scoped>
.card-content {
display: flex;
align-items: center;
.card {
.card-content {
display: flex;
align-items: center;
& > div:first-child {
flex: 1;
& > div:first-child {
flex: 1;
}
& > div:last-child {
cursor: pointer;
}
}
& > div:last-child {
cursor: pointer;
.identity-header {
background: $yellow-2;
display: flex;
padding: 5px;
figure {
padding-right: 3px;
}
}
}
</style>

View File

@ -8,7 +8,9 @@
<a
class="list-item"
v-for="groupMembership in actualMemberships"
:class="{ 'is-active': groupMembership.parent.id === currentGroup.id }"
:class="{
'is-active': groupMembership.parent.id === currentGroup.id,
}"
@click="changeCurrentGroup(groupMembership.parent)"
:key="groupMembership.id"
>
@ -19,14 +21,25 @@
:src="groupMembership.parent.avatar.url"
alt=""
/>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
<div class="media-content">
<h3>@{{ groupMembership.parent.name }}</h3>
<small>{{ `@${groupMembership.parent.preferredUsername}` }}</small>
<small>{{
`@${groupMembership.parent.preferredUsername}`
}}</small>
</div>
</div>
</a>
<a class="list-item" @click="changeCurrentGroup(new Group())" v-if="currentGroup.id">
<a
class="list-item"
@click="changeCurrentGroup(new Group())"
v-if="currentGroup.id"
>
<h3>{{ $t("Unset group") }}</h3>
</a>
</div>
@ -36,9 +49,11 @@
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IGroup, IMember, IPerson, Group, MemberRole } from "@/types/actor";
import { IGroup, IPerson, Group } from "@/types/actor";
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import { Paginate } from "@/types/paginate";
import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums";
@Component({
apollo: {
@ -77,9 +92,11 @@ export default class GroupPicker extends Vue {
get actualMemberships(): IMember[] {
if (this.restrictModeratorLevel) {
return this.groupMemberships.elements.filter((membership: IMember) =>
[MemberRole.ADMINISTRATOR, MemberRole.MODERATOR, MemberRole.CREATOR].includes(
membership.role
)
[
MemberRole.ADMINISTRATOR,
MemberRole.MODERATOR,
MemberRole.CREATOR,
].includes(membership.role)
);
}
return this.groupMemberships.elements;

View File

@ -10,7 +10,11 @@
{{ $t("The event will show the group as organizer.") }}
</p>
</div>
<div v-if="inline && currentGroup.id" class="inline box" @click="isComponentModalActive = true">
<div
v-if="inline && currentGroup.id"
class="inline box"
@click="isComponentModalActive = true"
>
<div class="media">
<div class="media-left">
<figure class="image is-48x48" v-if="currentGroup.avatar">
@ -24,7 +28,9 @@
</div>
<div class="media-content" v-if="currentGroup.name">
<p class="is-4">{{ currentGroup.name }}</p>
<p class="is-6 has-text-grey">{{ `@${currentGroup.preferredUsername}` }}</p>
<p class="is-6 has-text-grey">
{{ `@${currentGroup.preferredUsername}` }}
</p>
</div>
<div class="media-content" v-else>
{{ `@${currentGroup.preferredUsername}` }}
@ -34,7 +40,11 @@
</b-button>
</div>
</div>
<span v-else-if="currentGroup.id" class="block" @click="isComponentModalActive = true">
<span
v-else-if="currentGroup.id"
class="block"
@click="isComponentModalActive = true"
>
<img
class="image is-48x48"
v-if="currentGroup.avatar"
@ -44,7 +54,9 @@
<b-icon v-else size="is-large" icon="account-circle" />
</span>
<div v-if="groupMemberships.total === 0" class="box">
<p class="is-4">{{ $t("This identity is not a member of any group.") }}</p>
<p class="is-4">
{{ $t("This identity is not a member of any group.") }}
</p>
<p class="is-6 is-size-6 has-text-grey">
{{ $t("You need to create the group before you create an event.") }}
</p>
@ -61,7 +73,8 @@
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { IGroup, IMember, IPerson } from "../../types/actor";
import { IMember } from "@/types/actor/member.model";
import { IGroup, IPerson } from "../../types/actor";
import GroupPicker from "./GroupPicker.vue";
import { PERSON_MEMBERSHIPS } from "../../graphql/actor";
import { Paginate } from "../../types/paginate";

View File

@ -26,7 +26,8 @@ export default class GroupSection extends Vue {
@Prop({ required: true, type: String }) icon!: string;
@Prop({ required: false, type: Boolean, default: true }) privateSection!: boolean;
@Prop({ required: false, type: Boolean, default: true })
privateSection!: boolean;
@Prop({ required: true, type: Object }) route!: Route;
}
@ -76,7 +77,8 @@ div.group-section-title {
::v-deep span {
display: inline;
padding: 3px 8px;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-weight: 500;
font-size: 30px;
flex: 1;

View File

@ -2,7 +2,10 @@
<div class="media">
<div class="media-content">
<div class="content">
<i18n tag="p" path="You have been invited by {invitedBy} to the following group:">
<i18n
tag="p"
path="You have been invited by {invitedBy} to the following group:"
>
<b slot="invitedBy">{{ member.invitedBy.name }}</b>
</i18n>
</div>
@ -20,15 +23,21 @@
<router-link
:to="{
name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(member.parent) },
params: {
preferredUsername: usernameWithDomain(member.parent),
},
}"
>
<h3>{{ member.parent.name }}</h3>
<p class="is-6 has-text-grey">
<span v-if="member.parent.domain">
{{ `@${member.parent.preferredUsername}@${member.parent.domain}` }}
{{
`@${member.parent.preferredUsername}@${member.parent.domain}`
}}
</span>
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
<span v-else>{{
`@${member.parent.preferredUsername}`
}}</span>
</p>
</router-link>
</div>
@ -54,7 +63,8 @@
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IMember, usernameWithDomain } from "@/types/actor";
import { usernameWithDomain } from "@/types/actor";
import { IMember } from "@/types/actor/member.model";
import RouteName from "../../router/name";
@Component

View File

@ -11,10 +11,10 @@
</template>
<script lang="ts">
import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
import { IMember } from "@/types/actor";
import { Component, Prop, Vue } from "vue-property-decorator";
import InvitationCard from "@/components/Group/InvitationCard.vue";
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
import { IMember } from "@/types/actor/member.model";
@Component({
components: {
@ -26,13 +26,15 @@ export default class Invitations extends Vue {
async acceptInvitation(id: string): Promise<void> {
try {
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>({
mutation: ACCEPT_INVITATION,
variables: {
id,
},
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
});
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>(
{
mutation: ACCEPT_INVITATION,
variables: {
id,
},
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
}
);
if (data) {
this.$emit("accept-invitation", data.acceptInvitation);
}
@ -46,13 +48,15 @@ export default class Invitations extends Vue {
async rejectInvitation(id: string): Promise<void> {
try {
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>({
mutation: REJECT_INVITATION,
variables: {
id,
},
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
});
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>(
{
mutation: REJECT_INVITATION,
variables: {
id,
},
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
}
);
if (data) {
this.$emit("reject-invitation", data.rejectInvitation);
}

View File

@ -25,6 +25,8 @@ export default class JoinGroupWithAccount extends Vue {
}`;
}
sentence = this.$t("We will redirect you to your instance in order to interact with this group");
sentence = this.$t(
"We will redirect you to your instance in order to interact with this group"
);
}
</script>

View File

@ -8,11 +8,7 @@ import { Component, Prop, Vue } from "vue-property-decorator";
// @ts-ignore
import MobilizonLogo from "../assets/logo_chapril_mobilizon.png";
@Component({
components: {
MobilizonLogo,
},
})
@Component
export default class Logo extends Vue {
@Prop({ type: Boolean, required: false, default: false }) invert!: boolean;
}

View File

@ -8,7 +8,11 @@
@click="clickMap"
@update:zoom="updateZoom"
>
<l-tile-layer :url="config.maps.tiles.endpoint" :attribution="attribution"> </l-tile-layer>
<l-tile-layer
:url="config.maps.tiles.endpoint"
:attribution="attribution"
>
</l-tile-layer>
<v-locatecontrol :options="{ icon: 'mdi mdi-map-marker' }" />
<l-marker
:lat-lng="[lat, lon]"
@ -17,7 +21,9 @@
:draggable="!readOnly"
>
<l-popup v-if="popupMultiLine">
<span v-for="line in popupMultiLine" :key="line">{{ line }}<br /></span>
<span v-for="line in popupMultiLine" :key="line"
>{{ line }}<br
/></span>
</l-popup>
</l-marker>
</l-map>
@ -51,12 +57,15 @@ export default class Map extends Vue {
@Prop({ type: String, required: true }) coords!: string;
@Prop({ type: Object, required: false }) marker!: { text: string | string[]; icon: string };
@Prop({ type: Object, required: false }) marker!: {
text: string | string[];
icon: string;
};
@Prop({ type: Object, required: false }) options!: object;
@Prop({ type: Object, required: false }) options!: Record<string, unknown>;
@Prop({ type: Function, required: false })
updateDraggableMarkerCallback!: Function;
updateDraggableMarkerCallback!: (latlng: LatLng, zoom: number) => void;
defaultOptions: {
zoom: number;
@ -86,45 +95,48 @@ export default class Map extends Vue {
}
/* eslint-enable */
openPopup(event: LeafletEvent) {
openPopup(event: LeafletEvent): void {
this.$nextTick(() => {
event.target.openPopup();
});
}
get mergedOptions(): object {
get mergedOptions(): Record<string, unknown> {
return { ...this.defaultOptions, ...this.options };
}
get lat() {
get lat(): number {
return this.$props.coords.split(";")[1];
}
get lon() {
get lon(): number {
return this.$props.coords.split(";")[0];
}
get popupMultiLine() {
get popupMultiLine(): Array<string> {
if (Array.isArray(this.marker.text)) {
return this.marker.text;
}
return [this.marker.text];
}
clickMap(event: LeafletMouseEvent) {
clickMap(event: LeafletMouseEvent): void {
this.updateDraggableMarkerPosition(event.latlng);
}
updateDraggableMarkerPosition(e: LatLng) {
updateDraggableMarkerPosition(e: LatLng): void {
this.updateDraggableMarkerCallback(e, this.zoom);
}
updateZoom(zoom: number) {
updateZoom(zoom: number): void {
this.zoom = zoom;
}
get attribution() {
return this.config.maps.tiles.attribution || this.$t("© The OpenStreetMap Contributors");
get attribution(): string {
return (
this.config.maps.tiles.attribution ||
(this.$t("© The OpenStreetMap Contributors") as string)
);
}
}
</script>

View File

@ -17,13 +17,16 @@ import { Component, Prop, Vue } from "vue-property-decorator";
@Component({
beforeDestroy() {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.parentContainer.removeLayer(this);
},
})
export default class Vue2LeafletLocateControl extends Vue {
@Prop({ type: Object, default: () => ({}) }) options!: object;
@Prop({ type: Object, default: () => ({}) }) options!: Record<
string,
unknown
>;
@Prop({ type: Boolean, default: true }) visible = true;
@ -33,7 +36,7 @@ export default class Vue2LeafletLocateControl extends Vue {
parentContainer: any;
mounted() {
mounted(): void {
this.mapObject = L.control.locate(this.options);
DomEvent.on(this.mapObject, this.$listeners as any);
propsBinder(this, this.mapObject, this.$props);
@ -42,7 +45,7 @@ export default class Vue2LeafletLocateControl extends Vue {
this.mapObject.addTo(this.parentContainer.mapObject, !this.visible);
}
public locate() {
public locate(): void {
this.mapObject.start();
}
}

View File

@ -1,9 +1,18 @@
<template>
<b-navbar type="is-secondary" wrapper-class="container" :active.sync="mobileNavbarActive">
<b-navbar
type="is-secondary"
wrapper-class="container"
:active.sync="mobileNavbarActive"
>
<template slot="brand">
<b-navbar-item tag="router-link" :to="{ name: RouteName.HOME }">
<img src="/img/long_logo_chapril_mobilizon.png" alt="logo mobilizon" style="width: 5em" />
<b-navbar-item tag="router-link"
:aria-label="$t('Home')"
:to="{ name: RouteName.HOME }">
<img
src="/img/long_logo_chapril_mobilizon.png"
alt="logo mobilizon"
style="width: 5em"
/>
</b-navbar-item>
</template>
<template slot="start">
@ -21,9 +30,12 @@
</b-navbar-item>
<b-navbar-item tag="span" v-if="config && config.features.eventCreation">
<b-button tag="router-link" :to="{ name: RouteName.CREATE_EVENT }" type="is-primary"
>{{ $t("Create") }}
</b-button>
<b-button
tag="router-link"
:to="{ name: RouteName.CREATE_EVENT }"
type="is-primary"
>{{ $t("Create") }}</b-button
>
</b-navbar-item>
</template>
<template slot="end">
@ -38,9 +50,17 @@
</b-navbar-item>
<b-navbar-dropdown v-if="currentActor.id && currentUser.isLoggedIn" right>
<template slot="label" v-if="currentActor" class="navbar-dropdown-profile">
<template
slot="label"
v-if="currentActor"
class="navbar-dropdown-profile"
>
<figure class="image is-32x32" v-if="currentActor.avatar">
<img class="is-rounded" alt="avatarUrl" :src="currentActor.avatar.url" />
<img
class="is-rounded"
alt="avatarUrl"
:src="currentActor.avatar.url"
/>
</figure>
<b-icon v-else icon="account-circle" />
</template>
@ -73,9 +93,11 @@
<hr class="navbar-divider" />
</b-navbar-item>
<b-navbar-item tag="router-link" :to="{ name: RouteName.UPDATE_IDENTITY }"
>{{ $t("My account") }}
</b-navbar-item>
<b-navbar-item
tag="router-link"
:to="{ name: RouteName.UPDATE_IDENTITY }"
>{{ $t("My account") }}</b-navbar-item
>
<!-- <b-navbar-item tag="router-link" :to="{ name: RouteName.CREATE_GROUP }">-->
<!-- {{ $t('Create group') }}-->
@ -103,9 +125,11 @@
<strong>{{ $t("Sign up") }}</strong>
</router-link>
<router-link class="button is-light" :to="{ name: RouteName.LOGIN }"
>{{ $t("Log in") }}
</router-link>
<router-link
class="button is-light"
:to="{ name: RouteName.LOGIN }"
>{{ $t("Log in") }}</router-link
>
</div>
</b-navbar-item>
</template>
@ -117,13 +141,18 @@ import { Component, Vue, Watch } from "vue-property-decorator";
import Logo from "@/components/Logo.vue";
import { GraphQLError } from "graphql";
import { loadLanguageAsync } from "@/utils/i18n";
import { ICurrentUserRole } from "@/types/enums";
import { CURRENT_USER_CLIENT, USER_SETTINGS } from "../graphql/user";
import { changeIdentity, logout } from "../utils/auth";
import { CURRENT_ACTOR_CLIENT, IDENTITIES, UPDATE_DEFAULT_ACTOR } from "../graphql/actor";
import {
CURRENT_ACTOR_CLIENT,
IDENTITIES,
UPDATE_DEFAULT_ACTOR,
} from "../graphql/actor";
import { IPerson, Person } from "../types/actor";
import { CONFIG } from "../graphql/config";
import { IConfig } from "../types/config.model";
import { ICurrentUser, ICurrentUserRole, IUser } from "../types/current-user.model";
import { ICurrentUser, IUser } from "../types/current-user.model";
import SearchField from "./SearchField.vue";
import RouteName from "../router/name";
@ -138,7 +167,9 @@ import RouteName from "../router/name";
identities: {
query: IDENTITIES,
update: ({ identities }) =>
identities ? identities.map((identity: IPerson) => new Person(identity)) : [],
identities
? identities.map((identity: IPerson) => new Person(identity))
: [],
skip() {
return this.currentUser.isLoggedIn === false;
},
@ -209,7 +240,8 @@ export default class NavBar extends Vue {
async handleErrors(errors: GraphQLError[]): Promise<void> {
if (
errors.length > 0 &&
errors[0].message === "You need to be logged-in to view your list of identities"
errors[0].message ===
"You need to be logged-in to view your list of identities"
) {
await this.logout();
}

View File

@ -1,9 +1,14 @@
<template>
<section class="section container">
<h1 class="title" v-if="loading">{{ $t("Your participation request is being validated") }}</h1>
<h1 class="title" v-if="loading">
{{ $t("Your participation request is being validated") }}
</h1>
<div v-else>
<div v-if="failed">
<b-message :title="$t('Error while validating participation request')" type="is-danger">
<b-message
:title="$t('Error while validating participation request')"
type="is-danger"
>
{{
$t(
"Either the participation request has already been validated, either the validation token is incorrect."
@ -12,9 +17,16 @@
</b-message>
</div>
<div v-else>
<h1 class="title">{{ $t("Your participation request has been validated") }}</h1>
<p class="content" v-if="participation.event.joinOptions == EventJoinOptions.RESTRICTED">
{{ $t("Your participation still has to be approved by the organisers.") }}
<h1 class="title">
{{ $t("Your participation request has been validated") }}
</h1>
<p
class="content"
v-if="participation.event.joinOptions == EventJoinOptions.RESTRICTED"
>
{{
$t("Your participation still has to be approved by the organisers.")
}}
</p>
<div class="columns has-text-centered">
<div class="column">
@ -38,9 +50,9 @@
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { confirmLocalAnonymousParticipation } from "@/services/AnonymousParticipationStorage";
import { EventJoinOptions } from "@/types/enums";
import { IParticipant } from "../../types/participant.model";
import RouteName from "../../router/name";
import { EventJoinOptions } from "../../types/event.model";
import { CONFIRM_PARTICIPATION } from "../../graphql/event";
@Component

View File

@ -0,0 +1,240 @@
<template>
<div>
<div
class="event-participation has-text-right"
v-if="isEventNotAlreadyPassed"
>
<participation-button
v-if="shouldShowParticipationButton"
:participation="participation"
:event="event"
:current-actor="currentActor"
@join-event="(actor) => $emit('join-event', actor)"
@join-modal="$emit('join-modal')"
@join-event-with-confirmation="
(actor) => $emit('join-event-with-confirmation', actor)
"
@confirm-leave="$emit('confirm-leave')"
/>
<b-button
type="is-text"
v-if="!actorIsParticipant && anonymousParticipation !== null"
@click="$emit('cancel-anonymous-participation')"
>{{ $t("Cancel anonymous participation") }}</b-button
>
<small v-if="!actorIsParticipant && anonymousParticipation">
{{ $t("You are participating in this event anonymously") }}
<b-tooltip :label="$t('Click for more information')">
<span
class="is-clickable"
@click="isAnonymousParticipationModalOpen = true"
>
<b-icon size="is-small" icon="information-outline" />
</span>
</b-tooltip>
</small>
<small
v-else-if="!actorIsParticipant && anonymousParticipation === false"
>
{{
$t(
"You are participating in this event anonymously but didn't confirm participation"
)
}}
<b-tooltip
:label="
$t(
'This information is saved only on your computer. Click for details'
)
"
>
<router-link :to="{ name: RouteName.TERMS }">
<b-icon size="is-small" icon="help-circle-outline" />
</router-link>
</b-tooltip>
</small>
</div>
<div v-else>
<button class="button is-primary" type="button" slot="trigger" disabled>
<template>
<span>{{ $t("Event already passed") }}</span>
</template>
<b-icon icon="menu-down" />
</button>
</div>
<b-modal
:active.sync="isAnonymousParticipationModalOpen"
has-modal-card
ref="anonymous-participation-modal"
>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">
{{ $t("About anonymous participation") }}
</p>
</header>
<section class="modal-card-body">
<b-notification
type="is-primary"
:closable="false"
v-if="event.joinOptions === EventJoinOptions.RESTRICTED"
>
{{
$t(
"As the event organizer has chosen to manually validate participation requests, your participation will be really confirmed only once you receive an email stating it's being accepted."
)
}}
</b-notification>
<p>
{{
$t(
"Your participation status is saved only on this device and will be deleted one month after the event's passed."
)
}}
</p>
<p v-if="isSecureContext">
{{
$t(
"You may clear all participation information for this device with the buttons below."
)
}}
</p>
<div class="buttons" v-if="isSecureContext">
<b-button
type="is-danger is-outlined"
@click="clearEventParticipationData"
>
{{ $t("Clear participation data for this event") }}
</b-button>
<b-button type="is-danger" @click="clearAllParticipationData">
{{ $t("Clear participation data for all events") }}
</b-button>
</div>
</section>
</div>
</b-modal>
</div>
</template>
<script lang="ts">
import { EventJoinOptions, EventStatus, ParticipantRole } from "@/types/enums";
import { IParticipant } from "@/types/participant.model";
import { Component, Prop, Vue } from "vue-property-decorator";
import RouteName from "@/router/name";
import { IEvent } from "@/types/event.model";
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { IPerson } from "@/types/actor";
import { IConfig } from "@/types/config.model";
import { CONFIG } from "@/graphql/config";
import {
removeAllAnonymousParticipations,
removeAnonymousParticipation,
} from "@/services/AnonymousParticipationStorage";
import ParticipationButton from "../Event/ParticipationButton.vue";
@Component({
apollo: {
currentActor: CURRENT_ACTOR_CLIENT,
config: CONFIG,
},
components: {
ParticipationButton,
},
})
export default class ParticipationSection extends Vue {
@Prop({ required: true }) participation!: IParticipant;
@Prop({ required: true }) event!: IEvent;
@Prop({ required: true, default: null }) anonymousParticipation!:
| boolean
| null;
currentActor!: IPerson;
config!: IConfig;
RouteName = RouteName;
EventJoinOptions = EventJoinOptions;
isAnonymousParticipationModalOpen = false;
get actorIsParticipant(): boolean {
if (this.actorIsOrganizer) return true;
return (
this.participation &&
this.participation.role === ParticipantRole.PARTICIPANT
);
}
get actorIsOrganizer(): boolean {
return (
this.participation && this.participation.role === ParticipantRole.CREATOR
);
}
get shouldShowParticipationButton(): boolean {
// If we have an anonymous participation, don't show the participation button
if (
this.config &&
this.config.anonymous.participation.allowed &&
this.anonymousParticipation
) {
return false;
}
// So that people can cancel their participation
if (this.actorIsParticipant) return true;
// You can participate to draft or cancelled events
if (this.event.draft || this.event.status === EventStatus.CANCELLED)
return false;
// Organizer can't participate
if (this.actorIsOrganizer) return false;
// If capacity is OK
if (this.eventCapacityOK) return true;
// Else
return false;
}
get eventCapacityOK(): boolean {
if (this.event.draft) return true;
if (!this.event.options.maximumAttendeeCapacity) return true;
return (
this.event.options.maximumAttendeeCapacity >
this.event.participantStats.participant
);
}
get isEventNotAlreadyPassed(): boolean {
return new Date(this.endDate) > new Date();
}
get endDate(): Date {
return this.event.endsOn !== null && this.event.endsOn > this.event.beginsOn
? this.event.endsOn
: this.event.beginsOn;
}
// eslint-disable-next-line class-methods-use-this
get isSecureContext(): boolean {
return window.isSecureContext;
}
async clearEventParticipationData(): Promise<void> {
await removeAnonymousParticipation(this.event.uuid);
window.location.reload();
}
// eslint-disable-next-line class-methods-use-this
clearAllParticipationData(): void {
removeAllAnonymousParticipations();
window.location.reload();
}
}
</script>

View File

@ -1,5 +1,9 @@
<template>
<redirect-with-account :uri="uri" :pathAfterLogin="`/events/${uuid}`" :sentence="sentence" />
<redirect-with-account
:uri="uri"
:pathAfterLogin="`/events/${uuid}`"
:sentence="sentence"
/>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
@ -21,6 +25,8 @@ export default class ParticipationWithAccount extends Vue {
}`;
}
sentence = this.$t("We will redirect you to your instance in order to interact with this event");
sentence = this.$t(
"We will redirect you to your instance in order to interact with this event"
);
}
</script>

View File

@ -33,7 +33,13 @@
)
}}
</p>
<p v-else>{{ $t("If you want, you may send a message to the event organizer here.") }}</p>
<p v-else>
{{
$t(
"If you want, you may send a message to the event organizer here."
)
}}
</p>
<b-field :label="$t('Message')">
<b-input
type="textarea"
@ -54,42 +60,93 @@
</p>
</b-checkbox>
</b-field>
<b-button :disabled="sendingForm" type="is-primary" native-type="submit">{{
$t("Send email")
}}</b-button>
<b-button
:disabled="sendingForm"
type="is-primary"
native-type="submit"
>{{ $t("Send email") }}</b-button
>
<div class="has-text-centered">
<b-button native-type="button" tag="a" type="is-text" @click="$router.go(-1)">{{
$t("Back to previous page")
}}</b-button>
<b-button
native-type="button"
tag="a"
type="is-text"
@click="$router.go(-1)"
>{{ $t("Back to previous page") }}</b-button
>
</div>
</form>
<div v-else>
<h1 class="title">{{ $t("Request for participation confirmation sent") }}</h1>
<p class="content">{{ $t("Check your inbox (and your junk mail folder).") }}</p>
<p class="content">{{ $t("You may now close this window.") }}</p>
<h1 class="title">
{{ $t("Request for participation confirmation sent") }}
</h1>
<p class="content">
<span>{{
$t("Check your inbox (and your junk mail folder).")
}}</span>
<span
class="details"
v-if="event.joinOptions === EventJoinOptions.RESTRICTED"
>
{{
$t(
"Your participation will be validated once you click the confirmation link into the email, and after the organizer manually validates your participation."
)
}} </span
><span class="details" v-else>{{
$t(
"Your participation will be validated once you click the confirmation link into the email."
)
}}</span>
</p>
<b-message type="is-warning" v-if="error">{{ error }}</b-message>
<p class="content">
<i18n path="You may now close this window, or {return_to_event}.">
<router-link
slot="return_to_event"
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
>{{ $t("return to the event's page") }}</router-link
>
</i18n>
</p>
</div>
</div>
</div>
<b-message type="is-danger" v-else-if="!$apollo.loading"
>{{
$t(
"Unable to load event for participation. The error details are provided below:"
)
}}
<details>
<pre>{{ error }}</pre>
</details>
</b-message>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { EventModel, IEvent, EventJoinOptions } from "@/types/event.model";
import { FETCH_EVENT, JOIN_EVENT } from "@/graphql/event";
import { EventModel, IEvent } from "@/types/event.model";
import { FETCH_EVENT_BASIC, JOIN_EVENT } from "@/graphql/event";
import { IConfig } from "@/types/config.model";
import { CONFIG } from "@/graphql/config";
import { addLocalUnconfirmedAnonymousParticipation } from "@/services/AnonymousParticipationStorage";
import { IParticipant, ParticipantRole } from "../../types/participant.model";
import { EventJoinOptions, ParticipantRole } from "@/types/enums";
import RouteName from "@/router/name";
import { IParticipant } from "../../types/participant.model";
@Component({
apollo: {
event: {
query: FETCH_EVENT,
query: FETCH_EVENT_BASIC,
variables() {
return {
uuid: this.uuid,
};
},
error(e) {
this.error = e;
},
skip() {
return !this.uuid;
},
@ -101,7 +158,11 @@ import { IParticipant, ParticipantRole } from "../../types/participant.model";
export default class ParticipationWithoutAccount extends Vue {
@Prop({ type: String, required: true }) uuid!: string;
anonymousParticipation: { email: string; message: string; saveParticipation: boolean } = {
anonymousParticipation: {
email: string;
message: string;
saveParticipation: boolean;
} = {
email: "",
message: "",
saveParticipation: true,
@ -119,6 +180,8 @@ export default class ParticipationWithoutAccount extends Vue {
EventJoinOptions = EventJoinOptions;
RouteName = RouteName;
async joinEvent(): Promise<void> {
this.error = false;
this.sendingForm = true;
@ -134,21 +197,27 @@ export default class ParticipationWithoutAccount extends Vue {
},
update: (store, { data: updateData }) => {
if (updateData == null) {
console.error("Cannot update event participant cache, because of data null value.");
console.error(
"Cannot update event participant cache, because of data null value."
);
return;
}
const cachedData = store.readQuery<{ event: IEvent }>({
query: FETCH_EVENT,
query: FETCH_EVENT_BASIC,
variables: { uuid: this.event.uuid },
});
if (cachedData == null) {
console.error("Cannot update event participant cache, because of cached null value.");
console.error(
"Cannot update event participant cache, because of cached null value."
);
return;
}
const { event } = cachedData;
if (event === null) {
console.error("Cannot update event participant cache, because of null value.");
console.error(
"Cannot update event participant cache, because of null value."
);
return;
}
@ -158,16 +227,15 @@ export default class ParticipationWithoutAccount extends Vue {
event.participantStats.going += 1;
event.participantStats.participant += 1;
}
console.log("just before writequery");
store.writeQuery({
query: FETCH_EVENT,
query: FETCH_EVENT_BASIC,
variables: { uuid: this.event.uuid },
data: { event },
});
},
});
console.log("finished with store", data);
this.formSent = true;
if (
data &&
data.joinEvent.metadata.cancellationToken &&
@ -177,13 +245,28 @@ export default class ParticipationWithoutAccount extends Vue {
this.event,
data.joinEvent.metadata.cancellationToken
);
console.log("done with crypto stuff");
}
} catch (e) {
this.error = e.message;
if (
["TextEncoder is not defined", "crypto.subtle is undefined"].includes(
e.message
)
) {
this.error = this.$t(
"Unable to save your participation in this browser."
) as string;
} else if (e.graphQLErrors && e.graphQLErrors.length > 0) {
this.error = e.graphQLErrors[0].message;
} else if (e.networkError) {
this.error = e.networkError.message;
}
}
this.sendingForm = false;
this.formSent = true;
}
}
</script>
<style lang="scss" scoped>
section.container.section {
background: $white;
}
</style>

View File

@ -2,22 +2,34 @@
<section class="section container hero">
<div class="hero-body" v-if="event">
<div class="container">
<subtitle>{{ $t("You wish to participate to the following event") }}</subtitle>
<subtitle>{{
$t("You wish to participate to the following event")
}}</subtitle>
<EventListViewCard v-if="event" :event="event" />
<div class="columns has-text-centered">
<div class="column">
<router-link :to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT }">
<router-link
:to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT }"
>
<figure class="image is-128x128">
<img src="../../assets/undraw_profile.svg" alt="Profile illustration" />
<img
src="../../assets/undraw_profile.svg"
alt="Profile illustration"
/>
</figure>
<b-button type="is-primary">{{ $t("I have a Mobilizon account") }}</b-button>
<b-button type="is-primary">{{
$t("I have a Mobilizon account")
}}</b-button>
</router-link>
<p>
<small>
{{
$t("Either on the {instance} instance or on another instance.", {
instance: host,
})
$t(
"Either on the {instance} instance or on another instance.",
{
instance: host,
}
)
}}
</small>
<b-tooltip
@ -32,25 +44,41 @@
</b-tooltip>
</p>
</div>
<vertical-divider :content="$t('Or')" v-if="anonymousParticipationAllowed" />
<vertical-divider
:content="$t('Or')"
v-if="anonymousParticipationAllowed"
/>
<div
class="column"
v-if="anonymousParticipationAllowed && hasAnonymousEmailParticipationMethod"
v-if="
anonymousParticipationAllowed &&
hasAnonymousEmailParticipationMethod
"
>
<router-link
:to="{ name: RouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT }"
v-if="event.local"
>
<figure class="image is-128x128">
<img src="../../assets/undraw_mail_2.svg" alt="Privacy illustration" />
<img
src="../../assets/undraw_mail_2.svg"
alt="Privacy illustration"
/>
</figure>
<b-button type="is-primary">{{ $t("I don't have a Mobilizon account") }}</b-button>
<b-button type="is-primary">{{
$t("I don't have a Mobilizon account")
}}</b-button>
</router-link>
<a :href="`${event.url}/participate/without-account`" v-else>
<figure class="image is-128x128">
<img src="../../assets/undraw_mail_2.svg" alt="Privacy illustration" />
<img
src="../../assets/undraw_mail_2.svg"
alt="Privacy illustration"
/>
</figure>
<b-button type="is-primary">{{ $t("I don't have a Mobilizon account") }}</b-button>
<b-button type="is-primary">{{
$t("I don't have a Mobilizon account")
}}</b-button>
</a>
<p>
<small>{{ $t("Participate using your email address") }}</small>

View File

@ -1,7 +1,7 @@
<template>
<div class="root">
<figure class="image" v-if="actualImageSrc">
<img :src="actualImageSrc" />
<figure class="image" v-if="imageSrc">
<img :src="imageSrc" />
</figure>
<figure class="image is-128x128" v-else>
<div class="image-placeholder">
@ -9,12 +9,19 @@
</div>
</figure>
<b-upload @input="onFileChanged" :accept="accept">
<a class="button is-primary">
<b-icon icon="upload"></b-icon>
<span>{{ $t("Click to upload") }}</span>
</a>
</b-upload>
<div class="action-buttons">
<b-field class="file is-primary">
<b-upload @input="onFileChanged" :accept="accept" class="file-label">
<span class="file-cta">
<b-icon class="file-icon" icon="upload" />
<span>{{ $t("Click to upload") }}</span>
</span>
</b-upload>
</b-field>
<b-button type="is-text" v-if="imageSrc" @click="removeOrClearPicture">
{{ $t("Clear") }}
</b-button>
</div>
</div>
</template>
@ -45,18 +52,28 @@ figure.image {
color: #eee;
}
}
.action-buttons {
display: flex;
flex-direction: column;
}
</style>
<script lang="ts">
import { IMedia } from "@/types/media.model";
import { Component, Model, Prop, Vue, Watch } from "vue-property-decorator";
@Component
export default class PictureUpload extends Vue {
@Model("change", { type: File }) readonly pictureFile!: File;
@Prop({ type: String, required: false }) defaultImageSrc!: string;
@Prop({ type: Object, required: false }) defaultImage!: IMedia;
@Prop({ type: String, required: false, default: "image/gif,image/png,image/jpeg,image/webp" })
@Prop({
type: String,
required: false,
default: "image/gif,image/png,image/jpeg,image/webp",
})
accept!: string;
@Prop({
@ -70,10 +87,14 @@ export default class PictureUpload extends Vue {
})
textFallback!: string;
imageSrc: string | null = null;
imageSrc: string | null = this.defaultImage ? this.defaultImage.url : null;
file!: File | null;
mounted(): void {
this.updatePreview(this.pictureFile);
if (this.pictureFile) {
this.updatePreview(this.pictureFile);
}
}
@Watch("pictureFile")
@ -81,13 +102,23 @@ export default class PictureUpload extends Vue {
this.updatePreview(val);
}
onFileChanged(file: File): void {
@Watch("defaultImage")
onDefaultImageChange(defaultImage: IMedia): void {
this.imageSrc = defaultImage ? defaultImage.url : null;
}
onFileChanged(file: File | null): void {
this.$emit("change", file);
this.updatePreview(file);
this.file = file;
}
private updatePreview(file?: File) {
async removeOrClearPicture(): Promise<void> {
this.onFileChanged(null);
}
private updatePreview(file?: File | null) {
if (file) {
this.imageSrc = URL.createObjectURL(file);
return;
@ -95,9 +126,5 @@ export default class PictureUpload extends Vue {
this.imageSrc = null;
}
get actualImageSrc(): string | null {
return this.imageSrc || this.defaultImageSrc;
}
}
</script>

View File

@ -14,26 +14,46 @@
<div class="media-content">
<p class="post-minimalist-title">{{ post.title }}</p>
<div class="metadata">
<b-tag type="is-warning" size="is-small" v-if="post.draft">{{ $t("Draft") }}</b-tag>
<b-tag type="is-warning" size="is-small" v-if="post.draft">{{
$t("Draft")
}}</b-tag>
<small
v-if="post.visibility === PostVisibility.PUBLIC && isCurrentActorMember"
v-if="
post.visibility === PostVisibility.PUBLIC &&
isCurrentActorMember
"
class="has-text-grey"
>
<b-icon icon="earth" size="is-small" />{{ $t("Public") }}</small
>
<small v-else-if="post.visibility === PostVisibility.UNLISTED" class="has-text-grey">
<b-icon icon="link" size="is-small" />{{ $t("Accessible through link") }}</small
<small
v-else-if="post.visibility === PostVisibility.UNLISTED"
class="has-text-grey"
>
<b-icon icon="link" size="is-small" />{{
$t("Accessible through link")
}}</small
>
<small
v-else-if="post.visibility === PostVisibility.PRIVATE"
class="has-text-grey"
>
<small v-else-if="post.visibility === PostVisibility.PRIVATE" class="has-text-grey">
<b-icon icon="lock" size="is-small" />{{
$t("Accessible only to members", { group: post.attributedTo.name })
$t("Accessible only to members", {
group: post.attributedTo.name,
})
}}</small
>
<small class="has-text-grey">{{
$options.filters.formatDateTimeString(new Date(post.insertedAt), false)
$options.filters.formatDateTimeString(
new Date(post.insertedAt),
false
)
}}</small>
<small class="has-text-grey" v-if="isCurrentActorMember">{{
$t("Created by {username}", { username: `@${usernameWithDomain(post.author)}` })
$t("Created by {username}", {
username: `@${usernameWithDomain(post.author)}`,
})
}}</small>
</div>
</div>
@ -43,15 +63,17 @@
</template>
<script lang="ts">
import { usernameWithDomain } from "@/types/actor";
import { PostVisibility } from "@/types/enums";
import { Component, Prop, Vue } from "vue-property-decorator";
import RouteName from "../../router/name";
import { IPost, PostVisibility } from "../../types/post.model";
import { IPost } from "../../types/post.model";
@Component
export default class PostElementItem extends Vue {
@Prop({ required: true, type: Object }) post!: IPost;
@Prop({ required: false, type: Boolean, default: false }) isCurrentActorMember!: boolean;
@Prop({ required: false, type: Boolean, default: false })
isCurrentActorMember!: boolean;
RouteName = RouteName;
@ -74,7 +96,8 @@ export default class PostElementItem extends Vue {
.post-minimalist-title {
color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-size: 1rem;
font-weight: 700;
overflow: hidden;

View File

@ -43,7 +43,8 @@ export default class PostListItem extends Vue {
.post-minimalist-title {
color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-size: 1rem;
font-weight: 700;
overflow: hidden;

View File

@ -22,10 +22,18 @@
<div class="content columns">
<div class="column is-one-quarter-desktop">
<span v-if="report.reporter.type === ActorType.APPLICATION">
{{ $t("Reported by someone on {domain}", { domain: report.reporter.domain }) }}
{{
$t("Reported by someone on {domain}", {
domain: report.reporter.domain,
})
}}
</span>
<span v-else>
{{ $t("Reported by {reporter}", { reporter: report.reporter.preferredUsername }) }}
{{
$t("Reported by {reporter}", {
reporter: report.reporter.preferredUsername,
})
}}
</span>
</div>
<div class="column" v-if="report.content" v-html="report.content" />
@ -36,7 +44,7 @@
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IReport } from "@/types/report.model";
import { ActorType } from "@/types/actor";
import { ActorType } from "@/types/enums";
@Component
export default class ReportCard extends Vue {

View File

@ -4,7 +4,10 @@
<p class="modal-card-title">{{ title }}</p>
</header>
<section class="modal-card-body is-flex" :class="{ 'is-titleless': !title }">
<section
class="modal-card-body is-flex"
:class="{ 'is-titleless': !title }"
>
<div class="media">
<div class="media-left">
<b-icon icon="alert" type="is-warning" size="is-large" />
@ -16,7 +19,12 @@
<figure class="image is-48x48" v-if="comment.actor.avatar">
<img :src="comment.actor.avatar.url" alt="Image" />
</figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
</div>
<div class="media-content">
<div class="content">
@ -82,7 +90,10 @@ import { IComment } from "../../types/comment.model";
},
})
export default class ReportModal extends Vue {
@Prop({ type: Function }) onConfirm!: Function;
@Prop({ type: Function }) onConfirm!: (
content: string,
forward: boolean
) => void;
@Prop({ type: String }) title!: string;

View File

@ -14,7 +14,9 @@
</div>
<div class="body">
<h3>{{ resource.title }}</h3>
<span class="host" v-if="inline">{{ resource.updatedAt | formatDateTimeString }}</span>
<span class="host" v-if="inline">{{
resource.updatedAt | formatDateTimeString
}}</span>
</div>
<draggable
v-if="!inline"
@ -93,21 +95,27 @@ export default class FolderItem extends Mixins(ResourceMixin) {
async moveResource(resource: IResource): Promise<IResource | undefined> {
try {
const { data } = await this.$apollo.mutate<{ updateResource: IResource }>({
mutation: UPDATE_RESOURCE,
variables: {
id: resource.id,
path: `${this.resource.path}/${resource.title}`,
parentId: this.resource.id,
},
});
const { data } = await this.$apollo.mutate<{ updateResource: IResource }>(
{
mutation: UPDATE_RESOURCE,
variables: {
id: resource.id,
path: `${this.resource.path}/${resource.title}`,
parentId: this.resource.id,
},
}
);
if (!data) {
console.error("Error while updating resource");
return undefined;
}
return data.updateResource;
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
return undefined;
}
}

View File

@ -17,7 +17,7 @@
</b-dropdown>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import { Component, Vue } from "vue-property-decorator";
@Component
export default class ResourceDropdown extends Vue {}

View File

@ -2,7 +2,12 @@
<div class="resource-wrapper">
<a :href="resource.resourceUrl" target="_blank">
<div class="preview">
<div v-if="resource.type && Object.keys(mapServiceTypeToIcon).includes(resource.type)">
<div
v-if="
resource.type &&
Object.keys(mapServiceTypeToIcon).includes(resource.type)
"
>
<b-icon :icon="mapServiceTypeToIcon[resource.type]" size="is-large" />
</div>
<div
@ -21,7 +26,9 @@
:src="resource.metadata.faviconUrl"
/>
<h3>{{ resource.title }}</h3>
<span class="host" v-if="inline">{{ resource.updatedAt | formatDateTimeString }}</span>
<span class="host" v-if="inline">{{
resource.updatedAt | formatDateTimeString
}}</span>
<span class="host" v-else>{{ urlHostname }}</span>
</div>
</a>

View File

@ -2,9 +2,15 @@
<div v-if="resource">
<article class="panel is-primary">
<p class="panel-heading">
{{ $t('Move "{resourceName}"', { resourceName: initialResource.title }) }}
{{
$t('Move "{resourceName}"', { resourceName: initialResource.title })
}}
</p>
<a class="panel-block clickable" @click="resource = resource.parent" v-if="resource.parent">
<a
class="panel-block clickable"
@click="resource = resource.parent"
v-if="resource.parent"
>
<span class="panel-icon">
<b-icon icon="chevron-up" size="is-small" />
</span>
@ -23,12 +29,19 @@
<a
class="panel-block"
v-for="element in resource.children.elements"
:class="{ clickable: element.type === 'folder' && element.id !== initialResource.id }"
:class="{
clickable:
element.type === 'folder' && element.id !== initialResource.id,
}"
:key="element.id"
@click="goDown(element)"
>
<span class="panel-icon">
<b-icon icon="folder" size="is-small" v-if="element.type === 'folder'" />
<b-icon
icon="folder"
size="is-small"
v-if="element.type === 'folder'"
/>
<b-icon icon="link" size="is-small" v-else />
</span>
{{ element.title }}
@ -44,10 +57,17 @@
{{ $t("No resources in this folder") }}
</p>
</article>
<b-button type="is-primary" @click="updateResource" :disabled="moveDisabled">{{
$t("Move resource to {folder}", { folder: resource.title })
<b-button
type="is-primary"
@click="updateResource"
:disabled="moveDisabled"
>{{
$t("Move resource to {folder}", { folder: resource.title })
}}</b-button
>
<b-button type="is-text" @click="$emit('close-move-modal')">{{
$t("Cancel")
}}</b-button>
<b-button type="is-text" @click="$emit('closeMoveModal')">{{ $t("Cancel") }}</b-button>
</div>
</template>
<script lang="ts">
@ -81,31 +101,34 @@ export default class ResourceSelector extends Vue {
resource: IResource | undefined = this.initialResource.parent;
goDown(element: IResource) {
goDown(element: IResource): void {
if (element.type === "folder" && element.id !== this.initialResource.id) {
this.resource = element;
}
}
updateResource() {
updateResource(): void {
this.$emit(
"updateResource",
"update-resource",
{
id: this.initialResource.id,
title: this.initialResource.title,
parent: this.resource && this.resource.path === "/" ? null : this.resource,
parent:
this.resource && this.resource.path === "/" ? null : this.resource,
path: this.initialResource.path,
},
this.initialResource.parent
);
}
get moveDisabled() {
get moveDisabled(): boolean | undefined {
return (
(this.initialResource.parent &&
this.resource &&
this.initialResource.parent.path === this.resource.path) ||
(this.initialResource.parent == undefined && this.resource && this.resource.path === "/")
(this.initialResource.parent === undefined &&
this.resource &&
this.resource.path === "/")
);
}
}

View File

@ -23,9 +23,9 @@ export default class SearchField extends Vue {
search = "";
enter() {
async enter(): Promise<void> {
this.$emit("navbar-search");
this.$router.push({
await this.$router.push({
name: RouteName.SEARCH,
query: { term: this.search },
});

View File

@ -15,16 +15,27 @@
</p>
</div>
<div class="field">
<b-checkbox v-model="notificationOnDay" @input="updateSetting({ notificationOnDay })">
<b-checkbox
v-model="notificationOnDay"
@input="updateSetting({ notificationOnDay })"
>
<strong>{{ $t("Notification on the day of the event") }}</strong>
<p>
{{
$t("We'll use your timezone settings to send a recap of the morning of the event.")
$t(
"We'll use your timezone settings to send a recap of the morning of the event."
)
}}
</p>
</b-checkbox>
</div>
<p>{{ $t("To activate more notifications, head over to the notification settings.") }}</p>
<p>
{{
$t(
"To activate more notifications, head over to the notification settings."
)
}}
</p>
</section>
</div>
</template>
@ -50,7 +61,11 @@ export default class NotificationsOnboarding extends mixins(Onboarding) {
try {
this.doUpdateSetting(variables);
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}
}

View File

@ -16,7 +16,7 @@ export default class SettingMenuItem extends Vue {
@Prop({ required: true, type: Object }) to!: Route;
get isActive() {
get isActive(): boolean {
if (!this.to) return false;
if (this.to.name === this.$route.name) {
if (this.to.params) {

View File

@ -20,11 +20,12 @@ export default class SettingMenuSection extends Vue {
@Prop({ required: true, type: Object }) to!: Route;
get sectionActive() {
get sectionActive(): boolean {
if (this.$slots.default) {
return this.$slots.default.some(
({
componentOptions: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
propsData: { to },
},

View File

@ -1,18 +1,27 @@
<template>
<aside>
<ul>
<SettingMenuSection :title="$t('Account')" :to="{ name: RouteName.ACCOUNT_SETTINGS }">
<SettingMenuSection
:title="$t('Account')"
:to="{ name: RouteName.ACCOUNT_SETTINGS }"
>
<SettingMenuItem
:title="this.$t('General')"
:to="{ name: RouteName.ACCOUNT_SETTINGS_GENERAL }"
/>
<SettingMenuItem :title="$t('Preferences')" :to="{ name: RouteName.PREFERENCES }" />
<SettingMenuItem
:title="$t('Preferences')"
:to="{ name: RouteName.PREFERENCES }"
/>
<SettingMenuItem
:title="this.$t('Email notifications')"
:to="{ name: RouteName.NOTIFICATIONS }"
/>
</SettingMenuSection>
<SettingMenuSection :title="$t('Profiles')" :to="{ name: RouteName.IDENTITIES }">
<SettingMenuSection
:title="$t('Profiles')"
:to="{ name: RouteName.IDENTITIES }"
>
<SettingMenuItem
v-for="profile in identities"
:key="profile.preferredUsername"
@ -22,7 +31,10 @@
params: { identityName: profile.preferredUsername },
}"
/>
<SettingMenuItem :title="$t('New profile')" :to="{ name: RouteName.CREATE_IDENTITY }" />
<SettingMenuItem
:title="$t('New profile')"
:to="{ name: RouteName.CREATE_IDENTITY }"
/>
</SettingMenuSection>
<SettingMenuSection
v-if="
@ -33,35 +45,54 @@
:title="$t('Moderation')"
:to="{ name: RouteName.MODERATION }"
>
<SettingMenuItem :title="$t('Reports')" :to="{ name: RouteName.REPORTS }" />
<SettingMenuItem :title="$t('Moderation log')" :to="{ name: RouteName.REPORT_LOGS }" />
<SettingMenuItem
:title="$t('Reports')"
:to="{ name: RouteName.REPORTS }"
/>
<SettingMenuItem
:title="$t('Moderation log')"
:to="{ name: RouteName.REPORT_LOGS }"
/>
<SettingMenuItem :title="$t('Users')" :to="{ name: RouteName.USERS }" />
<SettingMenuItem :title="$t('Profiles')" :to="{ name: RouteName.PROFILES }" />
<SettingMenuItem :title="$t('Groups')" :to="{ name: RouteName.ADMIN_GROUPS }" />
<SettingMenuItem
:title="$t('Profiles')"
:to="{ name: RouteName.PROFILES }"
/>
<SettingMenuItem
:title="$t('Groups')"
:to="{ name: RouteName.ADMIN_GROUPS }"
/>
</SettingMenuSection>
<SettingMenuSection
v-if="this.currentUser.role == ICurrentUserRole.ADMINISTRATOR"
:title="$t('Admin')"
:to="{ name: RouteName.ADMIN }"
>
<SettingMenuItem :title="$t('Dashboard')" :to="{ name: RouteName.ADMIN_DASHBOARD }" />
<SettingMenuItem
:title="$t('Dashboard')"
:to="{ name: RouteName.ADMIN_DASHBOARD }"
/>
<SettingMenuItem
:title="$t('Instance settings')"
:to="{ name: RouteName.ADMIN_SETTINGS }"
/>
<SettingMenuItem :title="$t('Federation')" :to="{ name: RouteName.RELAYS }" />
<SettingMenuItem
:title="$t('Federation')"
:to="{ name: RouteName.RELAYS }"
/>
</SettingMenuSection>
</ul>
</aside>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { Component, Vue } from "vue-property-decorator";
import { ICurrentUserRole } from "@/types/enums";
import SettingMenuSection from "./SettingMenuSection.vue";
import SettingMenuItem from "./SettingMenuItem.vue";
import { IDENTITIES } from "../../graphql/actor";
import { IPerson, Person } from "../../types/actor";
import { CURRENT_USER_CLIENT } from "../../graphql/user";
import { ICurrentUser, ICurrentUserRole } from "../../types/current-user.model";
import { ICurrentUser } from "../../types/current-user.model";
import RouteName from "../../router/name";
@ -70,7 +101,8 @@ import RouteName from "../../router/name";
apollo: {
identities: {
query: IDENTITIES,
update: (data) => data.identities.map((identity: IPerson) => new Person(identity)),
update: (data) =>
data.identities.map((identity: IPerson) => new Person(identity)),
},
currentUser: CURRENT_USER_CLIENT,
},

View File

@ -39,7 +39,10 @@
timezone,
})
}}
<b-message type="is-danger" v-if="!$apollo.loading && !supportedTimezone">
<b-message
type="is-danger"
v-if="!$apollo.loading && !supportedTimezone"
>
{{ $t("Your timezone {timezone} isn't supported.", { timezone }) }}
</b-message>
</p>

View File

@ -2,9 +2,10 @@
<div class="card" v-if="todo">
<div class="card-content">
<b-checkbox v-model="status" />
<router-link :to="{ name: RouteName.TODO, params: { todoId: todo.id } }">{{
todo.title
}}</router-link>
<router-link
:to="{ name: RouteName.TODO, params: { todoId: todo.id } }"
>{{ todo.title }}</router-link
>
<span class="details has-text-grey">
<span v-if="todo.dueDate" class="due_date">
<b-icon icon="calendar" />
@ -13,7 +14,9 @@
<span v-if="todo.assignedTo" class="assigned_to">
<b-icon icon="account" />
{{ `@${todo.assignedTo.preferredUsername}` }}
<span v-if="todo.assignedTo.domain">{{ `@${todo.assignedTo.domain}` }}</span>
<span v-if="todo.assignedTo.domain">{{
`@${todo.assignedTo.domain}`
}}</span>
</span>
</span>
</div>
@ -53,7 +56,11 @@ export default class Todo extends Vue {
});
this.editMode = false;
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}
}

View File

@ -18,7 +18,7 @@
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { debounce } from "lodash";
import { debounce, DebouncedFunc } from "lodash";
import { SnackbarProgrammatic as Snackbar } from "buefy";
import { ITodo } from "../../types/todos";
import RouteName from "../../router/name";
@ -36,7 +36,9 @@ export default class Todo extends Vue {
editMode = false;
debounceUpdateTodo!: Function;
debounceUpdateTodo!: DebouncedFunc<
(obj: Record<string, unknown>) => Promise<void>
>;
// We put this in data because of issues like
// https://github.com/vuejs/vue-class-component/issues/263
@ -89,7 +91,11 @@ export default class Todo extends Vue {
});
this.editMode = false;
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}
}

View File

@ -7,7 +7,11 @@
<b-icon :icon="oauthProvider.id" />
<span>{{ SELECTED_PROVIDERS[oauthProvider.id] }}</span></a
>
<a class="button is-light" :href="`/auth/${oauthProvider.id}`" v-else-if="isProviderSelected">
<a
class="button is-light"
:href="`/auth/${oauthProvider.id}`"
v-else-if="isProviderSelected"
>
<b-icon icon="lock" />
<span>{{ oauthProvider.label }}</span>
</a>

View File

@ -20,7 +20,9 @@
</div>
<vertical-divider :content="$t('Or')" />
<div class="column">
<subtitle>{{ $t("I have an account on another Mobilizon instance.") }}</subtitle>
<subtitle>{{
$t("I have an account on another Mobilizon instance.")
}}</subtitle>
<p>{{ $t("Other software may also support this.") }}</p>
<p>{{ sentence }}</p>
<form @submit.prevent="redirectToInstance">
@ -34,7 +36,9 @@
:placeholder="$t('profile@instance')"
></b-input>
<p class="control">
<button class="button is-primary" type="submit">{{ $t("Go") }}</button>
<button class="button is-primary" type="submit">
{{ $t("Go") }}
</button>
</p>
</b-field>
</b-field>
@ -54,7 +58,7 @@
import { Component, Prop, Vue } from "vue-property-decorator";
import VerticalDivider from "@/components/Utils/VerticalDivider.vue";
import Subtitle from "@/components/Utils/Subtitle.vue";
import { LoginErrorCode } from "@/types/login-error-code.model";
import { LoginErrorCode } from "@/types/enums";
import RouteName from "../../router/name";
@Component({
@ -80,14 +84,22 @@ export default class RedirectWithAccount extends Vue {
async redirectToInstance(): Promise<void> {
const [, host] = this.remoteActorAddress.split("@", 2);
const remoteInteractionURI = await this.webFingerFetch(host, this.remoteActorAddress);
const remoteInteractionURI = await this.webFingerFetch(
host,
this.remoteActorAddress
);
window.open(remoteInteractionURI);
}
private async webFingerFetch(hostname: string, identity: string): Promise<string> {
private async webFingerFetch(
hostname: string,
identity: string
): Promise<string> {
const scheme = process.env.NODE_ENV === "production" ? "https" : "http";
const data = await (
await fetch(`${scheme}://${hostname}/.well-known/webfinger?resource=acct:${identity}`)
await fetch(
`${scheme}://${hostname}/.well-known/webfinger?resource=acct:${identity}`
)
).json();
if (data && Array.isArray(data.links)) {
const link: { template: string } = data.links.find(

View File

@ -14,7 +14,10 @@ function formatDateString(value: string): string {
}
function formatTimeString(value: string): string {
return parseDateTime(value).toLocaleTimeString(undefined, { hour: "numeric", minute: "numeric" });
return parseDateTime(value).toLocaleTimeString(undefined, {
hour: "numeric",
minute: "numeric",
});
}
function formatDateTimeString(value: string, showTime = true): string {

View File

@ -1,7 +1,12 @@
import nl2br from "@/filters/utils";
import { formatDateString, formatTimeString, formatDateTimeString } from "./datetime";
import {
formatDateString,
formatTimeString,
formatDateTimeString,
} from "./datetime";
export default {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
install(vue: any): void {
vue.filter("formatDateString", formatDateString);
vue.filter("formatTimeString", formatTimeString);

View File

@ -10,6 +10,7 @@ export const FETCH_PERSON = gql`
summary
preferredUsername
suspended
mediaSize
avatar {
id
name
@ -51,6 +52,7 @@ export const GET_PERSON = gql`
summary
preferredUsername
suspended
mediaSize
avatar {
id
name
@ -63,7 +65,10 @@ export const GET_PERSON = gql`
feedTokens {
token
}
organizedEvents(page: $organizedEventsPage, limit: $organizedEventsLimit) {
organizedEvents(
page: $organizedEventsPage
limit: $organizedEventsLimit
) {
total
elements {
id
@ -206,6 +211,18 @@ export const LOGGED_USER_PARTICIPATIONS = gql`
url
}
}
attributedTo {
avatar {
id
url
}
preferredUsername
name
summary
domain
url
id
}
participantStats {
going
notApproved
@ -215,6 +232,11 @@ export const LOGGED_USER_PARTICIPATIONS = gql`
maximumAttendeeCapacity
remainingAttendeeCapacity
}
tags {
id
slug
title
}
}
id
role
@ -282,6 +304,16 @@ export const LOGGED_USER_MEMBERSHIPS = gql`
elements {
id
role
actor {
id
avatar {
id
url
}
preferredUsername
name
domain
}
parent {
id
preferredUsername
@ -419,7 +451,7 @@ export const CREATE_PERSON = gql`
$preferredUsername: String!
$name: String!
$summary: String
$avatar: PictureInput
$avatar: MediaInput
) {
createPerson(
preferredUsername: $preferredUsername
@ -440,7 +472,12 @@ export const CREATE_PERSON = gql`
`;
export const UPDATE_PERSON = gql`
mutation UpdatePerson($id: ID!, $name: String, $summary: String, $avatar: PictureInput) {
mutation UpdatePerson(
$id: ID!
$name: String
$summary: String
$avatar: MediaInput
) {
updatePerson(id: $id, name: $name, summary: $summary, avatar: $avatar) {
id
preferredUsername
@ -467,7 +504,12 @@ export const DELETE_PERSON = gql`
* Prefer CREATE_PERSON when creating another identity
*/
export const REGISTER_PERSON = gql`
mutation($preferredUsername: String!, $name: String!, $summary: String!, $email: String!) {
mutation(
$preferredUsername: String!
$name: String!
$summary: String!
$email: String!
) {
registerPerson(
preferredUsername: $preferredUsername
name: $name

View File

@ -70,13 +70,11 @@ export const COMMENTS_THREADS = gql`
export const CREATE_COMMENT_FROM_EVENT = gql`
mutation CreateCommentFromEvent(
$eventId: ID!
$actorId: ID!
$text: String!
$inReplyToCommentId: ID
) {
createComment(
eventId: $eventId
actorId: $actorId
text: $text
inReplyToCommentId: $inReplyToCommentId
) {

View File

@ -51,6 +51,9 @@ export const CONFIG = gql`
endpoint
attribution
}
routing {
type
}
}
geocoding {
provider

View File

@ -84,8 +84,8 @@ export const DISCUSSION_FIELDS_FRAGMENT = gql`
`;
export const CREATE_DISCUSSION = gql`
mutation createDiscussion($title: String!, $creatorId: ID!, $actorId: ID!, $text: String!) {
createDiscussion(title: $title, text: $text, creatorId: $creatorId, actorId: $actorId) {
mutation createDiscussion($title: String!, $actorId: ID!, $text: String!) {
createDiscussion(title: $title, text: $text, actorId: $actorId) {
...DiscussionFields
}
}

View File

@ -176,57 +176,73 @@ export const FETCH_EVENT = gql`
}
`;
export const FETCH_EVENT_BASIC = gql`
query($uuid: UUID!) {
event(uuid: $uuid) {
id
uuid
joinOptions
participantStats {
going
notApproved
notConfirmed
participant
}
}
}
`;
export const FETCH_EVENTS = gql`
query {
events {
id,
uuid,
url,
local,
title,
description,
beginsOn,
endsOn,
status,
visibility,
picture {
total
elements {
id
uuid
url
},
publishAt,
# online_address,
# phone_address,
physicalAddress {
id,
description,
locality
},
organizerActor {
id,
avatar {
local
title
description
beginsOn
endsOn
status
visibility
picture {
id
url
},
preferredUsername,
domain,
name,
},
# attributedTo {
# avatar {
# id
# url
# },
# preferredUsername,
# name,
# },
category,
participants {
${participantsQuery}
},
tags {
slug,
title
},
}
publishAt
# online_address,
# phone_address,
physicalAddress {
id
description
locality
}
organizerActor {
id
avatar {
id
url
}
preferredUsername
domain
name
}
# attributedTo {
# avatar {
# id
# url
# },
# preferredUsername,
# name,
# },
category
tags {
slug
title
}
}
}
}
`;
@ -244,7 +260,7 @@ export const CREATE_EVENT = gql`
$joinOptions: EventJoinOptions,
$draft: Boolean,
$tags: [String],
$picture: PictureInput,
$picture: MediaInput,
$onlineAddress: String,
$phoneAddress: String,
$category: String,
@ -355,7 +371,7 @@ export const EDIT_EVENT = gql`
$joinOptions: EventJoinOptions,
$draft: Boolean,
$tags: [String],
$picture: PictureInput,
$picture: MediaInput,
$onlineAddress: String,
$phoneAddress: String,
$organizerActorId: ID,
@ -498,8 +514,8 @@ export const CONFIRM_PARTICIPATION = gql`
`;
export const UPDATE_PARTICIPANT = gql`
mutation AcceptParticipant($id: ID!, $moderatorActorId: ID!, $role: ParticipantRoleEnum!) {
updateParticipation(id: $id, moderatorActorId: $moderatorActorId, role: $role) {
mutation UpdateParticipant($id: ID!, $role: ParticipantRoleEnum!) {
updateParticipation(id: $id, role: $role) {
role
id
}
@ -507,20 +523,20 @@ export const UPDATE_PARTICIPANT = gql`
`;
export const DELETE_EVENT = gql`
mutation DeleteEvent($eventId: ID!, $actorId: ID!) {
deleteEvent(eventId: $eventId, actorId: $actorId) {
mutation DeleteEvent($eventId: ID!) {
deleteEvent(eventId: $eventId) {
id
}
}
`;
export const PARTICIPANTS = gql`
query($uuid: UUID!, $page: Int, $limit: Int, $roles: String, $actorId: ID!) {
query($uuid: UUID!, $page: Int, $limit: Int, $roles: String) {
event(uuid: $uuid) {
id,
uuid,
title,
participants(page: $page, limit: $limit, roles: $roles, actorId: $actorId) {
participants(page: $page, limit: $limit, roles: $roles) {
${participantsQuery}
},
participantStats {
@ -606,3 +622,54 @@ export const GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED = gql`
}
}
`;
export const FETCH_GROUP_EVENTS = gql`
query(
$name: String!
$afterDateTime: DateTime
$beforeDateTime: DateTime
$organisedEventsPage: Int
$organisedEventslimit: Int
) {
group(preferredUsername: $name) {
id
preferredUsername
domain
name
organizedEvents(
afterDatetime: $afterDateTime
beforeDatetime: $beforeDateTime
page: $organisedEventsPage
limit: $organisedEventslimit
) {
elements {
id
uuid
title
beginsOn
draft
options {
maximumAttendeeCapacity
}
participantStats {
participant
notApproved
}
attributedTo {
id
preferredUsername
name
domain
}
organizerActor {
id
preferredUsername
name
domain
}
}
total
}
}
}
`;

View File

@ -95,6 +95,7 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
uuid
title
beginsOn
draft
options {
maximumAttendeeCapacity
}
@ -212,6 +213,7 @@ export const GET_GROUP = gql`
$organisedEventslimit: Int
) {
getGroup(id: $id) {
mediaSize
...GroupFullFields
}
}
@ -223,15 +225,13 @@ export const GET_GROUP = gql`
export const CREATE_GROUP = gql`
mutation CreateGroup(
$creatorActorId: ID!
$preferredUsername: String!
$name: String!
$summary: String
$avatar: PictureInput
$banner: PictureInput
$avatar: MediaInput
$banner: MediaInput
) {
createGroup(
creatorActorId: $creatorActorId
preferredUsername: $preferredUsername
name: $name
summary: $summary
@ -260,8 +260,8 @@ export const UPDATE_GROUP = gql`
$id: ID!
$name: String
$summary: String
$avatar: PictureInput
$banner: PictureInput
$avatar: MediaInput
$banner: MediaInput
$visibility: GroupVisibility
$openness: Openness
$physicalAddress: AddressInput

Some files were not shown because too many files have changed in this diff Show More