merge with version 1.0.3 for osm
@ -39,9 +39,9 @@ lint:
|
|||||||
- mix format --check-formatted --dry-run || export EXITVALUE=1
|
- mix format --check-formatted --dry-run || export EXITVALUE=1
|
||||||
- cd js
|
- cd js
|
||||||
- yarn install
|
- yarn install
|
||||||
#- yarn run lint || export EXITVALUE=1
|
- yarn run lint || export EXITVALUE=1
|
||||||
- yarn run prettier --ignore-path="src/i18n/*" -c . || export EXITVALUE=1
|
- yarn run prettier -c . || export EXITVALUE=1
|
||||||
- yarn run build
|
- yarn run build:assets
|
||||||
- cd ../
|
- cd ../
|
||||||
- exit $EXITVALUE
|
- exit $EXITVALUE
|
||||||
artifacts:
|
artifacts:
|
||||||
@ -69,7 +69,7 @@ exunit:
|
|||||||
before_script:
|
before_script:
|
||||||
- cd js
|
- cd js
|
||||||
- yarn install
|
- yarn install
|
||||||
- yarn run build
|
- yarn run build:assets
|
||||||
- cd ../
|
- cd ../
|
||||||
- mix deps.get
|
- mix deps.get
|
||||||
- MIX_ENV=test mix ecto.create
|
- MIX_ENV=test mix ecto.create
|
||||||
@ -78,6 +78,21 @@ exunit:
|
|||||||
- lint
|
- lint
|
||||||
script:
|
script:
|
||||||
- mix coveralls
|
- 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:
|
# cypress:
|
||||||
# stage: test
|
# stage: test
|
||||||
# services:
|
# services:
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
projects:
|
|
||||||
Mobilizon:
|
|
||||||
schemaPath: schema.graphql
|
|
||||||
extensions:
|
|
||||||
endpoints:
|
|
||||||
dev:
|
|
||||||
url: 'http://localhost:4000/api'
|
|
||||||
introspect: true
|
|
127
CHANGELOG.md
@ -1,10 +1,101 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## 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
|
## 1.0.2 - 2020-11-15
|
||||||
|
|
||||||
@ -163,20 +254,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Special operations
|
### Special operations
|
||||||
|
|
||||||
* We added `application/ld+json` as acceptable MIME type for ActivityPub requests, so you'll need to recompile the `mime` library we use before recompiling Mobilizon:
|
- We added `application/ld+json` as acceptable MIME type for ActivityPub requests, so you'll need to recompile the `mime` library we use before recompiling Mobilizon:
|
||||||
```
|
|
||||||
MIX_ENV=prod mix deps.clean mime --build
|
|
||||||
```
|
|
||||||
|
|
||||||
* The [nginx configuration](https://framagit.org/framasoft/mobilizon/-/blob/master/support/nginx/mobilizon.conf) has been changed with improvements and support for custom error pages.
|
```
|
||||||
|
MIX_ENV=prod mix deps.clean mime --build
|
||||||
|
```
|
||||||
|
|
||||||
* The cmake dependency has been added (see [our documentation](https://docs.joinmobilizon.org/administration/dependencies/#basic-tools))
|
- The [nginx configuration](https://framagit.org/framasoft/mobilizon/-/blob/master/support/nginx/mobilizon.conf) has been changed with improvements and support for custom error pages.
|
||||||
|
|
||||||
|
- The cmake dependency has been added (see [our documentation](https://docs.joinmobilizon.org/administration/dependencies/#basic-tools))
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Possibility to login using LDAP
|
- Possibility to login using LDAP
|
||||||
- Possibility to login using OAuth providers
|
- Possibility to login using OAuth providers
|
||||||
- Enabled group features in production mode
|
- Enabled group features in production mode
|
||||||
- including posts (that can be public, unlisted, or restricted to your group members)
|
- including posts (that can be public, unlisted, or restricted to your group members)
|
||||||
- resources (collections of links, with folders, accessible to your group members)
|
- resources (collections of links, with folders, accessible to your group members)
|
||||||
- discussions (group private and organized chats)
|
- discussions (group private and organized chats)
|
||||||
@ -200,11 +292,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Fix group settings being accessible and editable by non-group-admins (thx @pigpig for reporting this responsibly)
|
- Fix group settings being accessible and editable by non-group-admins (thx @pigpig for reporting this responsibly)
|
||||||
- Fix events being editable by profiles without permissions (thx @pigpig for reporting this responsibly)
|
- Fix events being editable by profiles without permissions (thx @pigpig for reporting this responsibly)
|
||||||
|
|
||||||
## [1.0.0-beta.3] - 2020-06-24
|
## [1.0.0-beta.3] - 2020-06-24
|
||||||
|
|
||||||
### Special operations
|
### Special operations
|
||||||
|
|
||||||
Config has moved from `.env` files to a more traditional way to handle things in the Elixir world, with `.exs` files.
|
Config has moved from `.env` files to a more traditional way to handle things in the Elixir world, with `.exs` files.
|
||||||
|
|
||||||
To migrate existing configuration, you can simply run `mix mobilizon.instance gen` and fill in the adequate values previously in `.env` files (you don't need to perform the operations to create the database).
|
To migrate existing configuration, you can simply run `mix mobilizon.instance gen` and fill in the adequate values previously in `.env` files (you don't need to perform the operations to create the database).
|
||||||
@ -214,6 +307,7 @@ A minimal file template [is available](https://framagit.org/framasoft/mobilizon/
|
|||||||
Also make sure to remove the `EnvironmentFile=` line from the systemd service and set `Environment=MIX_ENV=prod` instead. See [the updated file](https://framagit.org/framasoft/mobilizon/blob/master/support/systemd/mobilizon.service).
|
Also make sure to remove the `EnvironmentFile=` line from the systemd service and set `Environment=MIX_ENV=prod` instead. See [the updated file](https://framagit.org/framasoft/mobilizon/blob/master/support/systemd/mobilizon.service).
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Possibility to participate to an event without an account (confirmation through email required)
|
- Possibility to participate to an event without an account (confirmation through email required)
|
||||||
- Possibility to participate to a remote event (being redirected by providing federated identity)
|
- Possibility to participate to a remote event (being redirected by providing federated identity)
|
||||||
- Possibility to add a note as a participant when event participation is manually validated (required when participating without an account)
|
- Possibility to add a note as a participant when event participation is manually validated (required when participating without an account)
|
||||||
@ -230,6 +324,7 @@ Also make sure to remove the `EnvironmentFile=` line from the systemd service an
|
|||||||
- Allow user to change language
|
- Allow user to change language
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Configuration handling (see above)
|
- Configuration handling (see above)
|
||||||
- Improved a bit color theme
|
- Improved a bit color theme
|
||||||
- Signature validation also now checks if `Date` header has acceptable values
|
- Signature validation also now checks if `Date` header has acceptable values
|
||||||
@ -240,6 +335,7 @@ Also make sure to remove the `EnvironmentFile=` line from the systemd service an
|
|||||||
- Improved public event page
|
- Improved public event page
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed URL search
|
- Fixed URL search
|
||||||
- Fixed content accessed through URL search being public
|
- Fixed content accessed through URL search being public
|
||||||
- Fix event links in some emails
|
- Fix event links in some emails
|
||||||
@ -247,17 +343,21 @@ Also make sure to remove the `EnvironmentFile=` line from the systemd service an
|
|||||||
## [1.0.0-beta.2] - 2019-12-18
|
## [1.0.0-beta.2] - 2019-12-18
|
||||||
|
|
||||||
### Special operations
|
### Special operations
|
||||||
|
|
||||||
These two operations couldn't be handled during migrations.
|
These two operations couldn't be handled during migrations.
|
||||||
They are optional, but you won't be able to search or get participant stats on existing events if they are not executed.
|
They are optional, but you won't be able to search or get participant stats on existing events if they are not executed.
|
||||||
These commands will be removed in Mobilizon 1.0.0-beta.3.
|
These commands will be removed in Mobilizon 1.0.0-beta.3.
|
||||||
|
|
||||||
In order to populate search index for existing events, you need to run the following command (with prod environment):
|
In order to populate search index for existing events, you need to run the following command (with prod environment):
|
||||||
* `mix mobilizon.setup_search`
|
|
||||||
|
- `mix mobilizon.setup_search`
|
||||||
|
|
||||||
In order to move participant stats to the event table for existing events, you need to run the following command (with prod environment):
|
In order to move participant stats to the event table for existing events, you need to run the following command (with prod environment):
|
||||||
* `mix mobilizon.move_participant_stats`
|
|
||||||
|
- `mix mobilizon.move_participant_stats`
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Federation is active
|
- Federation is active
|
||||||
- Added an interface for admins to view and manage instance followers and followings
|
- Added an interface for admins to view and manage instance followers and followings
|
||||||
- Ability to comment below events
|
- Ability to comment below events
|
||||||
@ -282,6 +382,7 @@ In order to move participant stats to the event table for existing events, you n
|
|||||||
- Upgraded frontend and backend dependencies
|
- Upgraded frontend and backend dependencies
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Move participant stats to event table **(read special instructions above)**
|
- Move participant stats to event table **(read special instructions above)**
|
||||||
- Limit length (20 characters) and number (10) of tags allowed
|
- Limit length (20 characters) and number (10) of tags allowed
|
||||||
- Added some backend changes and validation for field length
|
- Added some backend changes and validation for field length
|
||||||
@ -295,6 +396,7 @@ In order to move participant stats to the event table for existing events, you n
|
|||||||
- Also consider the PeerTube `CommentsEnabled` property to know if you can reply to an event
|
- Also consider the PeerTube `CommentsEnabled` property to know if you can reply to an event
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fix event URL validation and check if hostname is correct before showing it
|
- Fix event URL validation and check if hostname is correct before showing it
|
||||||
- Fix participations stats on the MyEvents page
|
- Fix participations stats on the MyEvents page
|
||||||
- Fix event description lists margin
|
- Fix event description lists margin
|
||||||
@ -324,8 +426,11 @@ In order to move participant stats to the event table for existing events, you n
|
|||||||
- Fixed event HTML representation when `GET` request has no `Accept` header
|
- Fixed event HTML representation when `GET` request has no `Accept` header
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Sanitize event title to avoid XSS
|
- Sanitize event title to avoid XSS
|
||||||
|
|
||||||
## [1.0.0-beta.1] - 2019-10-15
|
## [1.0.0-beta.1] - 2019-10-15
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Initial release
|
- Initial release
|
||||||
|
29
README.md
@ -20,7 +20,7 @@ Mobilizon is your federated organization and mobilization platform. Gather peopl
|
|||||||
|
|
||||||
Mobilizon is a tool designed to create platforms for managing communities and events. Its purpose is to help as many people as possible to free themselves from Facebook groups and events, from Meetup, etc.
|
Mobilizon is a tool designed to create platforms for managing communities and events. Its purpose is to help as many people as possible to free themselves from Facebook groups and events, from Meetup, etc.
|
||||||
|
|
||||||
The Mobilizon software is under a Free licence, so anyone can host a Mobilizon server, called an instance. These instances may federate with each other, so any person with an account on *ExampleMeet* will be able to register to an event created on *SpecimenEvent*.
|
The Mobilizon software is under a Free licence, so anyone can host a Mobilizon server, called an instance. These instances may federate with each other, so any person with an account on _ExampleMeet_ will be able to register to an event created on _SpecimenEvent_.
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ You will have the power to create multiple identities from the same account, lik
|
|||||||
|
|
||||||
### 📅 Events and groups
|
### 📅 Events and groups
|
||||||
|
|
||||||
Create your events and make sure they will appeal to everybody.
|
Create your events and make sure they will appeal to everybody.
|
||||||
Privacy settings and participants roles are supported.
|
Privacy settings and participants roles are supported.
|
||||||
There's no lock-in, you can interact with the event without registration.
|
There's no lock-in, you can interact with the event without registration.
|
||||||
|
|
||||||
@ -46,23 +46,26 @@ We appreciate any contribution to Mobilizon. Check our [CONTRIBUTING](CONTRIBUTI
|
|||||||
## Links
|
## Links
|
||||||
|
|
||||||
### Learn more
|
### Learn more
|
||||||
* 🌐 Official website: [https://joinmobilizon.org](https://joinmobilizon.org)
|
|
||||||
* 🔢 Pick an instance [https://mobilizon.org](https://mobilizon.org)
|
- 🌐 Official website: [https://joinmobilizon.org](https://joinmobilizon.org)
|
||||||
* 💻 Source: [https://framagit.org/framasoft/mobilizon](https://framagit.org/framasoft/mobilizon)
|
- 🔢 Pick an instance [https://mobilizon.org](https://mobilizon.org)
|
||||||
* 📜 Documentation [https://docs.joinmobilizon.org](https://docs.joinmobilizon.org)
|
- 💻 Source: [https://framagit.org/framasoft/mobilizon](https://framagit.org/framasoft/mobilizon)
|
||||||
|
- 📜 Documentation [https://docs.joinmobilizon.org](https://docs.joinmobilizon.org)
|
||||||
|
|
||||||
### Discuss
|
### Discuss
|
||||||
* 💬 Riot/Matrix: [https://riot.im/app/#/room/#Mobilizon:matrix.org](https://riot.im/app/#/room/#Mobilizon:matrix.org)
|
|
||||||
* 🗣️ Forum: [https://framacolibri.org/c/mobilizon](https://framacolibri.org/c/mobilizon)
|
- 💬 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
|
### Follow
|
||||||
* 🐘 Mastodon: [https://framapiaf.org/@mobilizon](https://framapiaf.org/@mobilizon)
|
|
||||||
* 🐦 Twitter [https://twitter.com/@joinmobilizon](https://twitter.com/@joinmobilizon)
|
- 🐘 Mastodon: [https://framapiaf.org/@mobilizon](https://framapiaf.org/@mobilizon)
|
||||||
|
- 🐦 Twitter [https://twitter.com/@joinmobilizon](https://twitter.com/@joinmobilizon)
|
||||||
|
|
||||||
Note: Most federation code comes from [Pleroma](https://pleroma.social), which is `Copyright © 2017-2018 Pleroma Authors - AGPL-3.0`.
|
Note: Most federation code comes from [Pleroma](https://pleroma.social), which is `Copyright © 2017-2018 Pleroma Authors - AGPL-3.0`.
|
||||||
|
|
||||||
|
|
||||||
## ❤️ Supports of our crowdfunding
|
## ❤️ Supports of our crowdfunding
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
We have run [a crowdfunding campaign](https://framablog.org/2019/05/14/mobilizon-lets-finance-a-software-to-free-our-events-from-facebook/) to pave the road to the version 1.0.0 of Mobilizon. Thanks to everyone who pitched in and shared the news around! The list of [everyone who donated is available here](https://joinmobilizon.org/hall-of-fame).
|
We have run [a crowdfunding campaign](https://framablog.org/2019/05/14/mobilizon-lets-finance-a-software-to-free-our-events-from-facebook/) to pave the road to the version 1.0.0 of Mobilizon. Thanks to everyone who pitched in and shared the news around! The list of [everyone who donated is available here](https://joinmobilizon.org/hall-of-fame).
|
||||||
|
32
SECURITY.md
@ -5,15 +5,15 @@ Framasoft, the Mobilizon maintainer team and community take all security bugs in
|
|||||||
|
|
||||||
### Goals
|
### Goals
|
||||||
|
|
||||||
* Mobilizon users can understand the distinctions between public data and private data/metadata on Mobilizon.
|
- Mobilizon users can understand the distinctions between public data and private data/metadata on Mobilizon.
|
||||||
|
|
||||||
* Users always know where their private data/metadata resides, who has access to it, and are able to access, export, and delete it.
|
- Users always know where their private data/metadata resides, who has access to it, and are able to access, export, and delete it.
|
||||||
|
|
||||||
* Protect private user data/metadata, not just from hackers but also (as much as is possible) from other users, instance admins, community moderators, and external applications.
|
- Protect private user data/metadata, not just from hackers but also (as much as is possible) from other users, instance admins, community moderators, and external applications.
|
||||||
|
|
||||||
* Secure from malicious creation, alteration or deletion of public data.
|
- Secure from malicious creation, alteration or deletion of public data.
|
||||||
|
|
||||||
* GDPR compliance.
|
- GDPR compliance.
|
||||||
|
|
||||||
Framasoft is both a developer of open-source/free/libre self-hosted software, and a service provider with users in the European Union. As a result, we are putting user privacy, data sovereignty, and GDPR compliance into our security plans, including asking both the Framasoft community and outside hackers to review our approaches and implementations.
|
Framasoft is both a developer of open-source/free/libre self-hosted software, and a service provider with users in the European Union. As a result, we are putting user privacy, data sovereignty, and GDPR compliance into our security plans, including asking both the Framasoft community and outside hackers to review our approaches and implementations.
|
||||||
|
|
||||||
@ -21,11 +21,11 @@ Framasoft is both a developer of open-source/free/libre self-hosted software, an
|
|||||||
|
|
||||||
[Mobilizon](https://joinmobilizon.org) will be challenging to keep secure, as it is:
|
[Mobilizon](https://joinmobilizon.org) will be challenging to keep secure, as it is:
|
||||||
|
|
||||||
* open source, both back-end and front-end
|
- open source, both back-end and front-end
|
||||||
|
|
||||||
* self-hosted by diverse organisations and individuals
|
- self-hosted by diverse organisations and individuals
|
||||||
|
|
||||||
* federated (data is transmitted between different hosted instances)
|
- federated (data is transmitted between different hosted instances)
|
||||||
|
|
||||||
This means there are more attack surfaces compared to typical proprietary, centralised platforms, but also means that hackers and even users can review every part of Mobilizon and make sure that it works as expected. This should result in more secure software, and higher trust in the application and its ecosystem.
|
This means there are more attack surfaces compared to typical proprietary, centralised platforms, but also means that hackers and even users can review every part of Mobilizon and make sure that it works as expected. This should result in more secure software, and higher trust in the application and its ecosystem.
|
||||||
|
|
||||||
@ -33,14 +33,14 @@ This means there are more attack surfaces compared to typical proprietary, centr
|
|||||||
|
|
||||||
We are committed to working with security researchers to verify, reproduce, and respond to legitimate reported vulnerabilities. You can help us by following these simple guidelines:
|
We are committed to working with security researchers to verify, reproduce, and respond to legitimate reported vulnerabilities. You can help us by following these simple guidelines:
|
||||||
|
|
||||||
* Alert us about the vulnerability as soon as you become aware of it by emailing the lead maintainer at tcit+mobilizon@framasoft.org.
|
- Alert us about the vulnerability as soon as you become aware of it by emailing the lead maintainer at tcit+mobilizon@framasoft.org.
|
||||||
* Provide details needed to reproduce and validate the vulnerability and a Proof of Concept (PoC) as soon as possible
|
- Provide details needed to reproduce and validate the vulnerability and a Proof of Concept (PoC) as soon as possible
|
||||||
* Act in good faith to avoid privacy violations, destruction of data, and interruption or degradation of services
|
- Act in good faith to avoid privacy violations, destruction of data, and interruption or degradation of services
|
||||||
* Do not access or modify users’ private data, without explicit permission of the owner. Only interact with your own accounts or test accounts for security research purposes;
|
- Do not access or modify users’ private data, without explicit permission of the owner. Only interact with your own accounts or test accounts for security research purposes;
|
||||||
* Contact Framasoft or a maintainer of the Mobilizon project (or the instance admin) immediately if you do inadvertently encounter user data. Do not view, alter, save, store, transfer, or otherwise access the data, and immediately purge any local information upon reporting the vulnerability;
|
- Contact Framasoft or a maintainer of the Mobilizon project (or the instance admin) immediately if you do inadvertently encounter user data. Do not view, alter, save, store, transfer, or otherwise access the data, and immediately purge any local information upon reporting the vulnerability;
|
||||||
* The lead maintainer will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating the next steps in handling your report. After the initial reply to your report, the security team will endeavor to keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
|
- The lead maintainer will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating the next steps in handling your report. After the initial reply to your report, the security team will endeavor to keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
|
||||||
* Give us time to confirm, determine the affected versions and prepare fixes to correct the issue before disclosing it to other parties (if after waiting a reasonable amount of time, we are clearly unable or unwilling to do anything about it, please do hold us accountable!)
|
- Give us time to confirm, determine the affected versions and prepare fixes to correct the issue before disclosing it to other parties (if after waiting a reasonable amount of time, we are clearly unable or unwilling to do anything about it, please do hold us accountable!)
|
||||||
* Please test against a local instance of the software, and refrain from running any Denial of Service or automated testing tools against Framasoft's (and our partners') infrastructure
|
- Please test against a local instance of the software, and refrain from running any Denial of Service or automated testing tools against Framasoft's (and our partners') infrastructure
|
||||||
|
|
||||||
Note : Please report security bugs in third-party modules to the person or team maintaining the module.
|
Note : Please report security bugs in third-party modules to the person or team maintaining the module.
|
||||||
|
|
||||||
|
@ -28,6 +28,10 @@ config :mobilizon, :instance,
|
|||||||
upload_limit: 10_000_000,
|
upload_limit: 10_000_000,
|
||||||
avatar_upload_limit: 2_000_000,
|
avatar_upload_limit: 2_000_000,
|
||||||
banner_upload_limit: 4_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_from: "noreply@localhost",
|
||||||
email_reply_to: "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, 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,
|
config :mobilizon, Mobilizon.Web.Email.Mailer,
|
||||||
adapter: Bamboo.SMTPAdapter,
|
adapter: Bamboo.SMTPAdapter,
|
||||||
server: "localhost",
|
server: "localhost",
|
||||||
@ -142,6 +135,10 @@ config :mobilizon, :ldap,
|
|||||||
base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
|
base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
|
||||||
uid: System.get_env("LDAP_UID") || "cn",
|
uid: System.get_env("LDAP_UID") || "cn",
|
||||||
require_bind_for_search: !(System.get_env("LDAP_REQUIRE_BIND_FOR_SEARCH") == "false"),
|
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_uid: System.get_env("LDAP_BIND_UID"),
|
||||||
bind_password: System.get_env("LDAP_BIND_PASSWORD")
|
bind_password: System.get_env("LDAP_BIND_PASSWORD")
|
||||||
|
|
||||||
@ -154,22 +151,20 @@ config :geolix,
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
config :auto_linker,
|
config :mobilizon, Mobilizon.Service.Formatter,
|
||||||
opts: [
|
class: false,
|
||||||
scheme: true,
|
rel: "noopener noreferrer ugc",
|
||||||
extra: true,
|
new_window: true,
|
||||||
# TODO: Set to :no_scheme when it works properly
|
truncate: false,
|
||||||
validate_tld: true,
|
strip_prefix: false,
|
||||||
class: false,
|
extra: true,
|
||||||
strip_prefix: false,
|
validate_tld: :no_scheme
|
||||||
new_window: true,
|
|
||||||
rel: "noopener noreferrer ugc"
|
|
||||||
]
|
|
||||||
|
|
||||||
config :tesla, adapter: Tesla.Adapter.Hackney
|
config :tesla, adapter: Tesla.Adapter.Hackney
|
||||||
|
|
||||||
config :phoenix, :format_encoders, json: Jason, "activity-json": Jason
|
config :phoenix, :format_encoders, json: Jason, "activity-json": Jason
|
||||||
config :phoenix, :json_library, Jason
|
config :phoenix, :json_library, Jason
|
||||||
|
config :phoenix, :filter_parameters, ["password", "token"]
|
||||||
|
|
||||||
config :ex_cldr,
|
config :ex_cldr,
|
||||||
default_locale: "en",
|
default_locale: "en",
|
||||||
@ -180,26 +175,8 @@ config :http_signatures,
|
|||||||
|
|
||||||
config :mobilizon, :cldr,
|
config :mobilizon, :cldr,
|
||||||
locales: [
|
locales: [
|
||||||
"ar",
|
|
||||||
"be",
|
|
||||||
"ca",
|
|
||||||
"cs",
|
|
||||||
"de",
|
|
||||||
"en",
|
|
||||||
"es",
|
|
||||||
"fi",
|
|
||||||
"fr",
|
"fr",
|
||||||
"gl",
|
"en"
|
||||||
"hu",
|
|
||||||
"it",
|
|
||||||
"ja",
|
|
||||||
"nl",
|
|
||||||
"nn",
|
|
||||||
"oc",
|
|
||||||
"pl",
|
|
||||||
"pt",
|
|
||||||
"ru",
|
|
||||||
"sv"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
config :mobilizon, :activitypub,
|
config :mobilizon, :activitypub,
|
||||||
@ -233,6 +210,9 @@ config :mobilizon, :maps,
|
|||||||
tiles: [
|
tiles: [
|
||||||
endpoint: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
endpoint: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||||
attribution: "© The OpenStreetMap Contributors"
|
attribution: "© The OpenStreetMap Contributors"
|
||||||
|
],
|
||||||
|
routing: [
|
||||||
|
type: :openstreetmap
|
||||||
]
|
]
|
||||||
|
|
||||||
config :mobilizon, :anonymous,
|
config :mobilizon, :anonymous,
|
||||||
@ -266,7 +246,10 @@ config :mobilizon, Oban,
|
|||||||
queues: [default: 10, search: 5, mailers: 10, background: 5],
|
queues: [default: 10, search: 5, mailers: 10, background: 5],
|
||||||
crontab: [
|
crontab: [
|
||||||
{"@hourly", Mobilizon.Service.Workers.BuildSiteMap, queue: :background},
|
{"@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,
|
config :mobilizon, :rich_media,
|
||||||
|
@ -19,7 +19,6 @@ config :mobilizon, Mobilizon.Web.Endpoint,
|
|||||||
code_reloader: true,
|
code_reloader: true,
|
||||||
check_origin: false,
|
check_origin: false,
|
||||||
watchers: [
|
watchers: [
|
||||||
# yarn: ["run", "dev", cd: Path.expand("../js", __DIR__)]
|
|
||||||
node: [
|
node: [
|
||||||
"node_modules/webpack/bin/webpack.js",
|
"node_modules/webpack/bin/webpack.js",
|
||||||
"--mode",
|
"--mode",
|
||||||
@ -53,8 +52,8 @@ config :mobilizon, Mobilizon.Web.Endpoint,
|
|||||||
patterns: [
|
patterns: [
|
||||||
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
|
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
|
||||||
~r{priv/gettext/.*(po)$},
|
~r{priv/gettext/.*(po)$},
|
||||||
~r{lib/mobilizon_web/views/.*(ex)$},
|
~r{lib/web/(live|views)/.*(ex)$},
|
||||||
~r{lib/mobilizon_web/templates/.*(eex)$}
|
~r{lib/web/templates/.*(eex)$}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -92,13 +91,6 @@ config :mobilizon, :instance,
|
|||||||
|
|
||||||
# config :mobilizon, :activitypub, sign_object_fetches: false
|
# 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,
|
config :mobilizon, :anonymous,
|
||||||
reports: [
|
reports: [
|
||||||
allowed: true
|
allowed: true
|
||||||
|
@ -13,6 +13,31 @@ config :mobilizon, Mobilizon.Web.Endpoint,
|
|||||||
# Do not print debug messages in production
|
# Do not print debug messages in production
|
||||||
config :logger, level: :info
|
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
|
cond do
|
||||||
System.get_env("INSTANCE_CONFIG") &&
|
System.get_env("INSTANCE_CONFIG") &&
|
||||||
File.exists?("./config/#{System.get_env("INSTANCE_CONFIG")}") ->
|
File.exists?("./config/#{System.get_env("INSTANCE_CONFIG")}") ->
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
version: '3'
|
version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
version: '3'
|
version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@ -15,7 +15,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
build: .
|
build: .
|
||||||
volumes:
|
volumes:
|
||||||
- '.:/app'
|
- ".:/app"
|
||||||
ports:
|
ports:
|
||||||
- "4000:4000"
|
- "4000:4000"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# First build the application assets
|
# First build the application assets
|
||||||
FROM node:alpine as 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 .
|
COPY js .
|
||||||
RUN yarn install \
|
RUN yarn install \
|
||||||
@ -33,6 +33,8 @@ FROM alpine
|
|||||||
|
|
||||||
RUN apk add --no-cache openssl ncurses-libs file postgresql-client
|
RUN apk add --no-cache openssl ncurses-libs file postgresql-client
|
||||||
|
|
||||||
|
RUN mkdir -p /app/uploads && chown nobody:nobody /app/uploads
|
||||||
|
|
||||||
USER nobody
|
USER nobody
|
||||||
EXPOSE 4000
|
EXPOSE 4000
|
||||||
|
|
||||||
|
@ -4,4 +4,4 @@ indent_size = 2
|
|||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
max_line_length = 100
|
max_line_length = 80
|
||||||
|
@ -7,13 +7,9 @@ module.exports = {
|
|||||||
|
|
||||||
extends: [
|
extends: [
|
||||||
"plugin:vue/essential",
|
"plugin:vue/essential",
|
||||||
"@vue/airbnb",
|
|
||||||
"@vue/typescript/recommended",
|
|
||||||
"plugin:cypress/recommended",
|
|
||||||
"plugin:prettier/recommended",
|
|
||||||
"prettier",
|
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"@vue/prettier",
|
"@vue/prettier",
|
||||||
|
"@vue/typescript/recommended",
|
||||||
"@vue/prettier/@typescript-eslint",
|
"@vue/prettier/@typescript-eslint",
|
||||||
],
|
],
|
||||||
|
|
||||||
@ -21,6 +17,7 @@ module.exports = {
|
|||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
},
|
},
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
@ -35,29 +32,24 @@ module.exports = {
|
|||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"cypress/no-unnecessary-waiting": "off",
|
"cypress/no-unnecessary-waiting": "off",
|
||||||
"vue/max-len": [
|
"vue/max-len": [
|
||||||
"error",
|
"off",
|
||||||
{
|
{
|
||||||
ignoreStrings: true,
|
ignoreStrings: true,
|
||||||
|
ignoreHTMLTextContents: true,
|
||||||
|
ignoreTemplateLiterals: true,
|
||||||
|
ignoreComments: true,
|
||||||
template: 170,
|
template: 170,
|
||||||
code: 100,
|
code: 80,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": "error",
|
||||||
"@typescript-eslint/interface-name-prefix": "off",
|
"@typescript-eslint/interface-name-prefix": "off",
|
||||||
"@typescript-eslint/no-use-before-define": "off",
|
"@typescript-eslint/no-use-before-define": "off",
|
||||||
"import/prefer-default-export": "off",
|
|
||||||
"import/extensions": "off",
|
"import/extensions": "off",
|
||||||
"import/no-unresolved": "off",
|
"import/no-unresolved": "off",
|
||||||
|
"no-shadow": "off",
|
||||||
|
"@typescript-eslint/no-shadow": ["error"],
|
||||||
},
|
},
|
||||||
|
|
||||||
ignorePatterns: ["src/typings/*.d.ts", "vue.config.js"],
|
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
@ -4,6 +4,7 @@ node_modules
|
|||||||
|
|
||||||
/tests/e2e/videos/
|
/tests/e2e/videos/
|
||||||
/tests/e2e/screenshots/
|
/tests/e2e/screenshots/
|
||||||
|
/coverage
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
|
@ -1 +1,2 @@
|
|||||||
src/i18n/*.json
|
src/i18n/*.json
|
||||||
|
coverage/
|
@ -24,7 +24,9 @@ fetch(`http://localhost:4000/api`, {
|
|||||||
.then((result) => result.json())
|
.then((result) => result.json())
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
// here we're filtering out any type information unrelated to unions or interfaces
|
// 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;
|
result.data.__schema.types = filteredData;
|
||||||
fs.writeFile("./fragmentTypes.json", JSON.stringify(result.data), (err) => {
|
fs.writeFile("./fragmentTypes.json", JSON.stringify(result.data), (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
19
js/jest.config.js
Normal 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",
|
||||||
|
// },
|
||||||
|
};
|
@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "mobilizon",
|
"name": "mobilizon",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build --modern",
|
"build:assets": "vue-cli-service build --modern",
|
||||||
"test:unit": "vue-cli-service test:unit",
|
"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",
|
"test:e2e": "vue-cli-service test:e2e",
|
||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
@ -29,7 +31,7 @@
|
|||||||
"eslint-plugin-cypress": "^2.10.3",
|
"eslint-plugin-cypress": "^2.10.3",
|
||||||
"graphql": "^15.0.0",
|
"graphql": "^15.0.0",
|
||||||
"graphql-tag": "^2.10.3",
|
"graphql-tag": "^2.10.3",
|
||||||
"intersection-observer": "^0.11.0",
|
"intersection-observer": "^0.12.0",
|
||||||
"leaflet": "^1.4.0",
|
"leaflet": "^1.4.0",
|
||||||
"leaflet.locatecontrol": "^0.72.0",
|
"leaflet.locatecontrol": "^0.72.0",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.11",
|
||||||
@ -39,6 +41,7 @@
|
|||||||
"tippy.js": "^6.2.3",
|
"tippy.js": "^6.2.3",
|
||||||
"tiptap": "^1.26.0",
|
"tiptap": "^1.26.0",
|
||||||
"tiptap-extensions": "^1.29.1",
|
"tiptap-extensions": "^1.29.1",
|
||||||
|
"unfetch": "^4.2.0",
|
||||||
"v-tooltip": "2.0.2",
|
"v-tooltip": "2.0.2",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-apollo": "^3.0.3",
|
"vue-apollo": "^3.0.3",
|
||||||
@ -52,6 +55,7 @@
|
|||||||
"vuedraggable": "2.23.2"
|
"vuedraggable": "2.23.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jest": "^26.0.18",
|
||||||
"@types/leaflet": "^1.5.2",
|
"@types/leaflet": "^1.5.2",
|
||||||
"@types/leaflet.locatecontrol": "^0.60.7",
|
"@types/leaflet.locatecontrol": "^0.60.7",
|
||||||
"@types/lodash": "^4.14.141",
|
"@types/lodash": "^4.14.141",
|
||||||
@ -63,27 +67,27 @@
|
|||||||
"@types/vuedraggable": "^2.23.0",
|
"@types/vuedraggable": "^2.23.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.0.1",
|
"@typescript-eslint/eslint-plugin": "^4.0.1",
|
||||||
"@typescript-eslint/parser": "^4.0.1",
|
"@typescript-eslint/parser": "^4.0.1",
|
||||||
"@vue/cli-plugin-babel": "~4.5.8",
|
"@vue/cli-plugin-babel": "~4.5.9",
|
||||||
"@vue/cli-plugin-e2e-cypress": "~4.5.8",
|
"@vue/cli-plugin-e2e-cypress": "~4.5.9",
|
||||||
"@vue/cli-plugin-eslint": "~4.5.8",
|
"@vue/cli-plugin-eslint": "~4.5.9",
|
||||||
"@vue/cli-plugin-pwa": "~4.5.8",
|
"@vue/cli-plugin-pwa": "~4.5.9",
|
||||||
"@vue/cli-plugin-router": "~4.5.8",
|
"@vue/cli-plugin-router": "~4.5.9",
|
||||||
"@vue/cli-plugin-typescript": "~4.5.8",
|
"@vue/cli-plugin-typescript": "~4.5.9",
|
||||||
"@vue/cli-service": "~4.5.8",
|
"@vue/cli-plugin-unit-jest": "~4.5.0",
|
||||||
"@vue/eslint-config-airbnb": "^5.0.2",
|
"@vue/cli-service": "~4.5.9",
|
||||||
"@vue/eslint-config-prettier": "^6.0.0",
|
"@vue/eslint-config-prettier": "^6.0.0",
|
||||||
"@vue/eslint-config-typescript": "^7.0.0",
|
"@vue/eslint-config-typescript": "^7.0.0",
|
||||||
"@vue/test-utils": "^1.1.0",
|
"@vue/test-utils": "^1.1.0",
|
||||||
"eslint": "^7.7.0",
|
"eslint": "^7.7.0",
|
||||||
"eslint-config-prettier": "^6.11.0",
|
"eslint-config-prettier": "^7.0.0",
|
||||||
"eslint-plugin-import": "^2.20.2",
|
|
||||||
"eslint-plugin-prettier": "^3.1.3",
|
"eslint-plugin-prettier": "^3.1.3",
|
||||||
"eslint-plugin-vue": "^7.0.0",
|
"eslint-plugin-vue": "^7.0.0",
|
||||||
"prettier": "2.1.2",
|
"mock-apollo-client": "^0.4",
|
||||||
"prettier-eslint": "^11.0.0",
|
"prettier": "2.2.1",
|
||||||
|
"prettier-eslint": "^12.0.0",
|
||||||
"sass": "^1.29.0",
|
"sass": "^1.29.0",
|
||||||
"sass-loader": "^10.0.1",
|
"sass-loader": "^10.0.1",
|
||||||
"typescript": "~4.0.2",
|
"typescript": "~4.1.2",
|
||||||
"vue-cli-plugin-svg": "~0.1.3",
|
"vue-cli-plugin-svg": "~0.1.3",
|
||||||
"vue-i18n-extract": "^1.0.2",
|
"vue-i18n-extract": "^1.0.2",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"vue-template-compiler": "^2.6.11",
|
||||||
|
Before Width: | Height: | Size: 725 KiB After Width: | Height: | Size: 725 KiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
3291
js/schema.graphql
Normal file
90
js/scripts/build/pictures.sh
Executable 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
@ -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 {};
|
@ -32,8 +32,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-property-decorator";
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
import NavBar from "./components/NavBar.vue";
|
import NavBar from "./components/NavBar.vue";
|
||||||
import { AUTH_ACCESS_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID, AUTH_USER_ROLE } from "./constants";
|
import {
|
||||||
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from "./graphql/user";
|
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 Footer from "./components/Footer.vue";
|
||||||
import Logo from "./components/Logo.vue";
|
import Logo from "./components/Logo.vue";
|
||||||
import { initializeCurrentActor } from "./utils/auth";
|
import { initializeCurrentActor } from "./utils/auth";
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import { ICurrentUserRole } from "@/types/enums";
|
||||||
import { ApolloCache } from "apollo-cache";
|
import { ApolloCache } from "apollo-cache";
|
||||||
import { NormalizedCacheObject } from "apollo-cache-inmemory";
|
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({
|
cache.writeData({
|
||||||
data: {
|
data: {
|
||||||
currentUser: {
|
currentUser: {
|
||||||
@ -53,7 +56,12 @@ export default function buildCurrentUserResolver(cache: ApolloCache<NormalizedCa
|
|||||||
preferredUsername,
|
preferredUsername,
|
||||||
avatar,
|
avatar,
|
||||||
name,
|
name,
|
||||||
}: { id: string; preferredUsername: string; avatar: string; name: string },
|
}: {
|
||||||
|
id: string;
|
||||||
|
preferredUsername: string;
|
||||||
|
avatar: string;
|
||||||
|
name: string;
|
||||||
|
},
|
||||||
{ cache: localCache }: { cache: ApolloCache<NormalizedCacheObject> }
|
{ cache: localCache }: { cache: ApolloCache<NormalizedCacheObject> }
|
||||||
) => {
|
) => {
|
||||||
const data = {
|
const data = {
|
||||||
|
@ -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 { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from "@/constants";
|
||||||
import { REFRESH_TOKEN } from "@/graphql/auth";
|
import { REFRESH_TOKEN } from "@/graphql/auth";
|
||||||
import { saveTokenData } from "@/utils/auth";
|
import { saveTokenData } from "@/utils/auth";
|
||||||
@ -11,7 +14,11 @@ export const fragmentMatcher = new IntrospectionFragmentMatcher({
|
|||||||
{
|
{
|
||||||
kind: "UNION",
|
kind: "UNION",
|
||||||
name: "SearchResult",
|
name: "SearchResult",
|
||||||
possibleTypes: [{ name: "Event" }, { name: "Person" }, { name: "Group" }],
|
possibleTypes: [
|
||||||
|
{ name: "Event" },
|
||||||
|
{ name: "Person" },
|
||||||
|
{ name: "Group" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "INTERFACE",
|
kind: "INTERFACE",
|
||||||
|
@ -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 |
@ -16,7 +16,6 @@ a.out,
|
|||||||
text-decoration-thickness: 2px;
|
text-decoration-thickness: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
padding: 1rem 1% 4rem;
|
padding: 1rem 1% 4rem;
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,12 @@
|
|||||||
<template slot-scope="props">
|
<template slot-scope="props">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left">
|
<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" />
|
<b-icon v-else icon="account-circle" />
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
@ -21,7 +26,9 @@
|
|||||||
{{ props.option.name }}
|
{{ props.option.name }}
|
||||||
<br />
|
<br />
|
||||||
<small>{{ `@${props.option.preferredUsername}` }}</small>
|
<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>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ `@${props.option.preferredUsername}` }}
|
{{ `@${props.option.preferredUsername}` }}
|
||||||
@ -53,7 +60,9 @@ export default class ActorAutoComplete extends Vue {
|
|||||||
|
|
||||||
selected: IPerson | null = this.defaultSelected;
|
selected: IPerson | null = this.defaultSelected;
|
||||||
|
|
||||||
name: string = this.defaultSelected ? this.defaultSelected.preferredUsername : "";
|
name: string = this.defaultSelected
|
||||||
|
? this.defaultSelected.preferredUsername
|
||||||
|
: "";
|
||||||
|
|
||||||
page = 1;
|
page = 1;
|
||||||
|
|
||||||
|
@ -12,8 +12,15 @@
|
|||||||
<p>
|
<p>
|
||||||
{{ actor.name || `@${usernameWithDomain(actor)}` }}
|
{{ actor.name || `@${usernameWithDomain(actor)}` }}
|
||||||
</p>
|
</p>
|
||||||
<p class="has-text-grey" v-if="actor.name">@{{ usernameWithDomain(actor) }}</p>
|
<p class="has-text-grey" v-if="actor.name">
|
||||||
<div v-if="full" class="summary" :class="{ limit: limit }" v-html="actor.summary" />
|
@{{ usernameWithDomain(actor) }}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="full"
|
||||||
|
class="summary"
|
||||||
|
:class="{ limit: limit }"
|
||||||
|
v-html="actor.summary"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,10 @@
|
|||||||
<ul class="identities">
|
<ul class="identities">
|
||||||
<li v-for="identity in identities" :key="identity.id">
|
<li v-for="identity in identities" :key="identity.id">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'UpdateIdentity', params: { identityName: identity.preferredUsername } }"
|
:to="{
|
||||||
|
name: 'UpdateIdentity',
|
||||||
|
params: { identityName: identity.preferredUsername },
|
||||||
|
}"
|
||||||
class="media identity"
|
class="media identity"
|
||||||
v-bind:class="{ 'is-current-identity': isCurrentIdentity(identity) }"
|
v-bind:class="{ 'is-current-identity': isCurrentIdentity(identity) }"
|
||||||
>
|
>
|
||||||
@ -24,7 +27,10 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</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") }}
|
{{ $t("Create a new identity") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</section>
|
</section>
|
||||||
@ -53,7 +59,7 @@ export default class Identities extends Vue {
|
|||||||
|
|
||||||
errors: string[] = [];
|
errors: string[] = [];
|
||||||
|
|
||||||
isCurrentIdentity(identity: IPerson) {
|
isCurrentIdentity(identity: IPerson): boolean {
|
||||||
return identity.preferredUsername === this.currentIdentityName;
|
return identity.preferredUsername === this.currentIdentityName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
@ -12,8 +12,9 @@
|
|||||||
</v-popover>
|
</v-popover>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { ActorType } from "@/types/enums";
|
||||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||||
import { IActor, ActorType } from "../../types/actor";
|
import { IActor } from "../../types/actor";
|
||||||
import ActorCard from "./ActorCard.vue";
|
import ActorCard from "./ActorCard.vue";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -16,11 +16,21 @@
|
|||||||
checkable
|
checkable
|
||||||
checkbox-position="left"
|
checkbox-position="left"
|
||||||
>
|
>
|
||||||
<b-table-column field="actor.id" label="ID" width="40" numeric v-slot="props">{{
|
<b-table-column
|
||||||
props.row.actor.id
|
field="actor.id"
|
||||||
}}</b-table-column>
|
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="lan" v-if="RelayMixin.isInstance(props.row.actor)" />
|
||||||
<b-icon icon="account-circle" v-else />
|
<b-icon icon="account-circle" v-else />
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
@ -33,26 +43,39 @@
|
|||||||
centered
|
centered
|
||||||
v-slot="props"
|
v-slot="props"
|
||||||
>
|
>
|
||||||
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`">{{
|
<span
|
||||||
props.row.approved ? $t("Accepted") : $t("Pending")
|
:class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
|
||||||
}}</span>
|
>{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
|
||||||
|
>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="actor.domain" :label="$t('Domain')" sortable>
|
<b-table-column field="actor.domain" :label="$t('Domain')" sortable>
|
||||||
<template v-slot:default="props">
|
<template v-slot:default="props">
|
||||||
<a @click="toggle(props.row)" v-if="RelayMixin.isInstance(props.row.actor)">{{
|
<a
|
||||||
props.row.actor.domain
|
@click="toggle(props.row)"
|
||||||
}}</a>
|
v-if="RelayMixin.isInstance(props.row.actor)"
|
||||||
|
>{{ props.row.actor.domain }}</a
|
||||||
|
>
|
||||||
<a @click="toggle(props.row)" v-else>{{
|
<a @click="toggle(props.row)" v-else>{{
|
||||||
`${props.row.actor.preferredUsername}@${props.row.actor.domain}`
|
`${props.row.actor.preferredUsername}@${props.row.actor.domain}`
|
||||||
}}</a>
|
}}</a>
|
||||||
</template>
|
</template>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable v-slot="props">
|
<b-table-column
|
||||||
<span :title="$options.filters.formatDateTimeString(props.row.updatedAt)">{{
|
field="targetActor.updatedAt"
|
||||||
formatDistanceToNow(new Date(props.row.updatedAt), { locale: $dateFnsLocale })
|
:label="$t('Date')"
|
||||||
}}</span></b-table-column
|
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">
|
<template slot="detail" slot-scope="props">
|
||||||
@ -143,7 +166,11 @@ export default class Followers extends Mixins(RelayMixin) {
|
|||||||
await this.$apollo.queries.relayFollowers.refetch();
|
await this.$apollo.queries.relayFollowers.refetch();
|
||||||
this.checkedRows = [];
|
this.checkedRows = [];
|
||||||
} catch (e) {
|
} 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();
|
await this.$apollo.queries.relayFollowers.refetch();
|
||||||
this.checkedRows = [];
|
this.checkedRows = [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
Snackbar.open({
|
||||||
|
message: e.message,
|
||||||
|
type: "is-danger",
|
||||||
|
position: "is-bottom",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<form @submit="followRelay">
|
<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">
|
<b-field grouped expanded size="is-large">
|
||||||
<p class="control">
|
<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>
|
||||||
<p class="control">
|
<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>
|
</p>
|
||||||
</b-field>
|
</b-field>
|
||||||
</b-field>
|
</b-field>
|
||||||
@ -29,12 +38,25 @@
|
|||||||
checkable
|
checkable
|
||||||
checkbox-position="left"
|
checkbox-position="left"
|
||||||
>
|
>
|
||||||
<b-table-column field="targetActor.id" label="ID" width="40" numeric v-slot="props">{{
|
<b-table-column
|
||||||
props.row.targetActor.id
|
field="targetActor.id"
|
||||||
}}</b-table-column>
|
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-table-column
|
||||||
<b-icon icon="lan" v-if="RelayMixin.isInstance(props.row.targetActor)" />
|
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-icon icon="account-circle" v-else />
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
@ -46,26 +68,39 @@
|
|||||||
centered
|
centered
|
||||||
v-slot="props"
|
v-slot="props"
|
||||||
>
|
>
|
||||||
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`">{{
|
<span
|
||||||
props.row.approved ? $t("Accepted") : $t("Pending")
|
:class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
|
||||||
}}</span>
|
>{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
|
||||||
|
>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="targetActor.domain" :label="$t('Domain')" sortable>
|
<b-table-column field="targetActor.domain" :label="$t('Domain')" sortable>
|
||||||
<template v-slot:default="props">
|
<template v-slot:default="props">
|
||||||
<a @click="toggle(props.row)" v-if="RelayMixin.isInstance(props.row.targetActor)">{{
|
<a
|
||||||
props.row.targetActor.domain
|
@click="toggle(props.row)"
|
||||||
}}</a>
|
v-if="RelayMixin.isInstance(props.row.targetActor)"
|
||||||
|
>{{ props.row.targetActor.domain }}</a
|
||||||
|
>
|
||||||
<a @click="toggle(props.row)" v-else>{{
|
<a @click="toggle(props.row)" v-else>{{
|
||||||
`${props.row.targetActor.preferredUsername}@${props.row.targetActor.domain}`
|
`${props.row.targetActor.preferredUsername}@${props.row.targetActor.domain}`
|
||||||
}}</a>
|
}}</a>
|
||||||
</template>
|
</template>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable v-slot="props">
|
<b-table-column
|
||||||
<span :title="$options.filters.formatDateTimeString(props.row.updatedAt)">{{
|
field="targetActor.updatedAt"
|
||||||
formatDistanceToNow(new Date(props.row.updatedAt), { locale: $dateFnsLocale })
|
:label="$t('Date')"
|
||||||
}}</span></b-table-column
|
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">
|
<template slot="detail" slot-scope="props">
|
||||||
@ -103,7 +138,6 @@ import { SnackbarProgrammatic as Snackbar } from "buefy";
|
|||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { ADD_RELAY, REMOVE_RELAY } from "../../graphql/admin";
|
import { ADD_RELAY, REMOVE_RELAY } from "../../graphql/admin";
|
||||||
import { IFollower } from "../../types/actor/follower.model";
|
import { IFollower } from "../../types/actor/follower.model";
|
||||||
import { Paginate } from "../../types/paginate";
|
|
||||||
import RelayMixin from "../../mixins/relay";
|
import RelayMixin from "../../mixins/relay";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -127,19 +161,25 @@ export default class Followings extends Mixins(RelayMixin) {
|
|||||||
await this.$apollo.mutate({
|
await this.$apollo.mutate({
|
||||||
mutation: ADD_RELAY,
|
mutation: ADD_RELAY,
|
||||||
variables: {
|
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();
|
await this.$apollo.queries.relayFollowings.refetch();
|
||||||
this.newRelayAddress = "";
|
this.newRelayAddress = "";
|
||||||
} catch (err) {
|
} 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> {
|
async removeRelays(): Promise<void> {
|
||||||
await this.checkedRows.forEach((row: IFollower) => {
|
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();
|
await this.$apollo.queries.relayFollowings.refetch();
|
||||||
this.checkedRows = [];
|
this.checkedRows = [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
Snackbar.open({
|
||||||
|
message: e.message,
|
||||||
|
type: "is-danger",
|
||||||
|
position: "is-bottom",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<li :class="{ reply: comment.inReplyToComment }">
|
<li :class="{ reply: comment.inReplyToComment }">
|
||||||
<article class="media" :class="{ selected: commentSelected }" :id="commentId">
|
<article
|
||||||
|
class="media"
|
||||||
|
:class="{ selected: commentSelected }"
|
||||||
|
:id="commentId"
|
||||||
|
>
|
||||||
<popover-actor-card
|
<popover-actor-card
|
||||||
class="media-left"
|
class="media-left"
|
||||||
:actor="comment.actor"
|
:actor="comment.actor"
|
||||||
:inline="true"
|
:inline="true"
|
||||||
v-if="comment.actor"
|
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="" />
|
<img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
|
||||||
</figure>
|
</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>
|
</popover-actor-card>
|
||||||
<div v-else class="media-left">
|
<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="" />
|
<img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
|
||||||
</figure>
|
</figure>
|
||||||
<b-icon v-else size="is-large" icon="account-circle" />
|
<b-icon v-else size="is-large" icon="account-circle" />
|
||||||
@ -21,7 +36,9 @@
|
|||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<span class="first-line" v-if="!comment.deletedAt">
|
<span class="first-line" v-if="!comment.deletedAt">
|
||||||
<strong :class="{ organizer: commentFromOrganizer }">{{ comment.actor.name }}</strong>
|
<strong :class="{ organizer: commentFromOrganizer }">{{
|
||||||
|
comment.actor.name
|
||||||
|
}}</strong>
|
||||||
<small>@{{ usernameWithDomain(comment.actor) }}</small>
|
<small>@{{ usernameWithDomain(comment.actor) }}</small>
|
||||||
<a class="comment-link has-text-grey" :href="commentURL">
|
<a class="comment-link has-text-grey" :href="commentURL">
|
||||||
<small>{{
|
<small>{{
|
||||||
@ -54,10 +71,15 @@
|
|||||||
<div class="load-replies" v-if="comment.totalReplies">
|
<div class="load-replies" v-if="comment.totalReplies">
|
||||||
<p v-if="!showReplies" @click="fetchReplies">
|
<p v-if="!showReplies" @click="fetchReplies">
|
||||||
<b-icon icon="chevron-down" /><span>{{
|
<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>
|
}}</span>
|
||||||
</p>
|
</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" />
|
<b-icon icon="chevron-up" />
|
||||||
<span>{{ $t("Hide replies") }}</span>
|
<span>{{ $t("Hide replies") }}</span>
|
||||||
</p>
|
</p>
|
||||||
@ -86,14 +108,24 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</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">
|
<article class="media reply">
|
||||||
<figure class="media-left" v-if="currentActor.avatar">
|
<figure class="media-left" v-if="currentActor.avatar">
|
||||||
<p class="image is-48x48">
|
<p class="image is-48x48">
|
||||||
<img :src="currentActor.avatar.url" alt="" />
|
<img :src="currentActor.avatar.url" alt="" />
|
||||||
</p>
|
</p>
|
||||||
</figure>
|
</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="media-content">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<span class="first-line">
|
<span class="first-line">
|
||||||
@ -102,7 +134,12 @@
|
|||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span class="editor-line">
|
<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
|
<b-button
|
||||||
:disabled="newComment.text.trim().length === 0"
|
:disabled="newComment.text.trim().length === 0"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
@ -118,7 +155,12 @@
|
|||||||
<div class="left">
|
<div class="left">
|
||||||
<div class="vertical-border" @click="showReplies = false" />
|
<div class="vertical-border" @click="showReplies = false" />
|
||||||
</div>
|
</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
|
<comment
|
||||||
class="reply"
|
class="reply"
|
||||||
v-for="reply in comment.replies"
|
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 EditorComponent from "@/components/Editor.vue";
|
||||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
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 { CommentModel, IComment } from "../../types/comment.model";
|
||||||
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
||||||
import { IPerson, usernameWithDomain } from "../../types/actor";
|
import { IPerson, usernameWithDomain } from "../../types/actor";
|
||||||
@ -155,7 +197,8 @@ import PopoverActorCard from "../Account/PopoverActorCard.vue";
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
editor: () =>
|
||||||
|
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
||||||
comment: () => import(/* webpackChunkName: "comment" */ "./Comment.vue"),
|
comment: () => import(/* webpackChunkName: "comment" */ "./Comment.vue"),
|
||||||
PopoverActorCard,
|
PopoverActorCard,
|
||||||
},
|
},
|
||||||
@ -167,7 +210,9 @@ export default class Comment extends Vue {
|
|||||||
|
|
||||||
// Hack because Vue only exports it's own interface.
|
// Hack because Vue only exports it's own interface.
|
||||||
// See https://github.com/kaorun343/vue-property-decorator/issues/257
|
// 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;
|
currentActor!: IPerson;
|
||||||
|
|
||||||
@ -231,7 +276,9 @@ export default class Comment extends Vue {
|
|||||||
if (!eventData) return;
|
if (!eventData) return;
|
||||||
const { event } = eventData;
|
const { event } = eventData;
|
||||||
const { comments } = event;
|
const { comments } = event;
|
||||||
const parentCommentIndex = comments.findIndex((oldComment) => oldComment.id === parentId);
|
const parentCommentIndex = comments.findIndex(
|
||||||
|
(oldComment) => oldComment.id === parentId
|
||||||
|
);
|
||||||
const parentComment = comments[parentCommentIndex];
|
const parentComment = comments[parentCommentIndex];
|
||||||
if (!parentComment) return;
|
if (!parentComment) return;
|
||||||
parentComment.replies = thread;
|
parentComment.replies = thread;
|
||||||
@ -288,7 +335,6 @@ export default class Comment extends Vue {
|
|||||||
mutation: CREATE_REPORT,
|
mutation: CREATE_REPORT,
|
||||||
variables: {
|
variables: {
|
||||||
eventId: this.event.id,
|
eventId: this.event.id,
|
||||||
reporterId: this.currentActor.id,
|
|
||||||
reportedId: this.comment.actor.id,
|
reportedId: this.comment.actor.id,
|
||||||
commentsIds: [this.comment.id],
|
commentsIds: [this.comment.id],
|
||||||
content,
|
content,
|
||||||
@ -304,7 +350,11 @@ export default class Comment extends Vue {
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
Snackbar.open({
|
||||||
|
message: e.message,
|
||||||
|
type: "is-danger",
|
||||||
|
position: "is-bottom",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,11 @@
|
|||||||
@submit.prevent="createCommentForEvent(newComment)"
|
@submit.prevent="createCommentForEvent(newComment)"
|
||||||
@keyup.ctrl.enter="createCommentForEvent(newComment)"
|
@keyup.ctrl.enter="createCommentForEvent(newComment)"
|
||||||
>
|
>
|
||||||
<b-notification v-if="isEventOrganiser && !areCommentsClosed" :closable="false">{{
|
<b-notification
|
||||||
$t("Comments are closed for everybody else.")
|
v-if="isEventOrganiser && !areCommentsClosed"
|
||||||
}}</b-notification>
|
:closable="false"
|
||||||
|
>{{ $t("Comments are closed for everybody else.") }}</b-notification
|
||||||
|
>
|
||||||
<article class="media">
|
<article class="media">
|
||||||
<figure class="media-left">
|
<figure class="media-left">
|
||||||
<identity-picker-wrapper :inline="false" v-model="newComment.actor" />
|
<identity-picker-wrapper :inline="false" v-model="newComment.actor" />
|
||||||
@ -16,11 +18,20 @@
|
|||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<editor ref="commenteditor" mode="comment" v-model="newComment.text" />
|
<editor
|
||||||
|
ref="commenteditor"
|
||||||
|
mode="comment"
|
||||||
|
v-model="newComment.text"
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="send-comment">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@ -28,8 +39,19 @@
|
|||||||
<b-notification v-else :closable="false">{{
|
<b-notification v-else :closable="false">{{
|
||||||
$t("The organiser has chosen to close comments.")
|
$t("The organiser has chosen to close comments.")
|
||||||
}}</b-notification>
|
}}</b-notification>
|
||||||
<transition name="comment-empty-list" mode="out-in">
|
<p
|
||||||
<transition-group name="comment-list" v-if="comments.length" class="comment-list" tag="ul">
|
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
|
<comment
|
||||||
class="root-comment"
|
class="root-comment"
|
||||||
:comment="comment"
|
:comment="comment"
|
||||||
@ -51,7 +73,7 @@
|
|||||||
import { Prop, Vue, Component, Watch } from "vue-property-decorator";
|
import { Prop, Vue, Component, Watch } from "vue-property-decorator";
|
||||||
import Comment from "@/components/Comment/Comment.vue";
|
import Comment from "@/components/Comment/Comment.vue";
|
||||||
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.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 { CommentModel, IComment } from "../../types/comment.model";
|
||||||
import {
|
import {
|
||||||
CREATE_COMMENT_FROM_EVENT,
|
CREATE_COMMENT_FROM_EVENT,
|
||||||
@ -76,7 +98,9 @@ import { IEvent } from "../../types/event.model";
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
update(data) {
|
update(data) {
|
||||||
return data.event.comments.map((comment: IComment) => new CommentModel(comment));
|
return data.event.comments.map(
|
||||||
|
(comment: IComment) => new CommentModel(comment)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
skip() {
|
skip() {
|
||||||
return !this.event.uuid;
|
return !this.event.uuid;
|
||||||
@ -86,7 +110,8 @@ import { IEvent } from "../../types/event.model";
|
|||||||
components: {
|
components: {
|
||||||
Comment,
|
Comment,
|
||||||
IdentityPickerWrapper,
|
IdentityPickerWrapper,
|
||||||
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
editor: () =>
|
||||||
|
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class CommentTree extends Vue {
|
export default class CommentTree extends Vue {
|
||||||
@ -112,9 +137,10 @@ export default class CommentTree extends Vue {
|
|||||||
mutation: CREATE_COMMENT_FROM_EVENT,
|
mutation: CREATE_COMMENT_FROM_EVENT,
|
||||||
variables: {
|
variables: {
|
||||||
eventId: this.event.id,
|
eventId: this.event.id,
|
||||||
actorId: comment.actor.id,
|
|
||||||
text: comment.text,
|
text: comment.text,
|
||||||
inReplyToCommentId: comment.inReplyToComment ? comment.inReplyToComment.id : null,
|
inReplyToCommentId: comment.inReplyToComment
|
||||||
|
? comment.inReplyToComment.id
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
update: (store, { data }) => {
|
update: (store, { data }) => {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
@ -204,7 +230,6 @@ export default class CommentTree extends Vue {
|
|||||||
mutation: DELETE_COMMENT,
|
mutation: DELETE_COMMENT,
|
||||||
variables: {
|
variables: {
|
||||||
commentId: comment.id,
|
commentId: comment.id,
|
||||||
actorId: this.currentActor.id,
|
|
||||||
},
|
},
|
||||||
update: (store, { data }) => {
|
update: (store, { data }) => {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
@ -230,7 +255,9 @@ export default class CommentTree extends Vue {
|
|||||||
});
|
});
|
||||||
if (!localData) return;
|
if (!localData) return;
|
||||||
const { thread: oldReplyList } = localData;
|
const { thread: oldReplyList } = localData;
|
||||||
const replies = oldReplyList.filter((reply) => reply.id !== deletedCommentId);
|
const replies = oldReplyList.filter(
|
||||||
|
(reply) => reply.id !== deletedCommentId
|
||||||
|
);
|
||||||
store.writeQuery({
|
store.writeQuery({
|
||||||
query: FETCH_THREAD_REPLIES,
|
query: FETCH_THREAD_REPLIES,
|
||||||
variables: {
|
variables: {
|
||||||
@ -251,7 +278,9 @@ export default class CommentTree extends Vue {
|
|||||||
event.comments = oldComments;
|
event.comments = oldComments;
|
||||||
} else {
|
} else {
|
||||||
// we have deleted a thread itself
|
// we have deleted a thread itself
|
||||||
event.comments = oldComments.filter((reply) => reply.id !== deletedCommentId);
|
event.comments = oldComments.filter(
|
||||||
|
(reply) => reply.id !== deletedCommentId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
store.writeQuery({
|
store.writeQuery({
|
||||||
query: COMMENTS_THREADS,
|
query: COMMENTS_THREADS,
|
||||||
@ -276,14 +305,18 @@ export default class CommentTree extends Vue {
|
|||||||
.filter((comment) => comment.inReplyToComment == null)
|
.filter((comment) => comment.inReplyToComment == null)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a.updatedAt && b.updatedAt) {
|
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;
|
return 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get filteredOrderedComments(): IComment[] {
|
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 {
|
get isEventOrganiser(): boolean {
|
||||||
@ -302,7 +335,7 @@ export default class CommentTree extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isAbleToComment(): boolean {
|
get isAbleToComment(): boolean {
|
||||||
if (this.currentActor.id) {
|
if (this.currentActor && this.currentActor.id) {
|
||||||
return this.areCommentsClosed || this.isEventOrganiser;
|
return this.areCommentsClosed || this.isEventOrganiser;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -15,7 +15,10 @@
|
|||||||
<span v-else class="name comment-link has-text-grey">
|
<span v-else class="name comment-link has-text-grey">
|
||||||
{{ $t("[deleted]") }}
|
{{ $t("[deleted]") }}
|
||||||
</span>
|
</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-dropdown aria-role="list">
|
||||||
<b-icon slot="trigger" role="button" icon="dots-horizontal" />
|
<b-icon slot="trigger" role="button" icon="dots-horizontal" />
|
||||||
|
|
||||||
@ -44,8 +47,9 @@
|
|||||||
<div class="post-infos">
|
<div class="post-infos">
|
||||||
<span :title="comment.insertedAt | formatDateTimeString">
|
<span :title="comment.insertedAt | formatDateTimeString">
|
||||||
{{
|
{{
|
||||||
formatDistanceToNow(new Date(comment.updatedAt), { locale: $dateFnsLocale }) ||
|
formatDistanceToNow(new Date(comment.updatedAt), {
|
||||||
$t("Right now")
|
locale: $dateFnsLocale,
|
||||||
|
}) || $t("Right now")
|
||||||
}}</span
|
}}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@ -77,7 +81,9 @@
|
|||||||
type="is-primary"
|
type="is-primary"
|
||||||
>{{ $t("Update") }}</b-button
|
>{{ $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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -95,7 +101,8 @@ import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
|||||||
currentActor: CURRENT_ACTOR_CLIENT,
|
currentActor: CURRENT_ACTOR_CLIENT,
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
editor: () =>
|
||||||
|
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class DiscussionComment extends Vue {
|
export default class DiscussionComment extends Vue {
|
||||||
|
@ -1,14 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-link
|
<router-link
|
||||||
class="discussion-minimalist-card-wrapper"
|
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">
|
<div class="media-left">
|
||||||
<figure
|
<figure
|
||||||
class="image is-32x32"
|
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>
|
</figure>
|
||||||
<b-icon v-else size="is-medium" icon="account-circle" />
|
<b-icon v-else size="is-medium" icon="account-circle" />
|
||||||
</div>
|
</div>
|
||||||
@ -17,15 +26,18 @@
|
|||||||
<p class="discussion-minimalist-title">{{ discussion.title }}</p>
|
<p class="discussion-minimalist-title">{{ discussion.title }}</p>
|
||||||
<span :title="actualDate | formatDateTimeString">
|
<span :title="actualDate | formatDateTimeString">
|
||||||
{{
|
{{
|
||||||
formatDistanceToNowStrict(new Date(actualDate), { locale: $dateFnsLocale }) ||
|
formatDistanceToNowStrict(new Date(actualDate), {
|
||||||
$t("Right now")
|
locale: $dateFnsLocale,
|
||||||
|
}) || $t("Right now")
|
||||||
}}</span
|
}}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="has-text-grey" v-if="!discussion.lastComment.deletedAt">
|
<div class="has-text-grey" v-if="!discussion.lastComment.deletedAt">
|
||||||
{{ htmlTextEllipsis }}
|
{{ htmlTextEllipsis }}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
@ -54,7 +66,10 @@ export default class DiscussionListItem extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get actualDate(): string | Date | undefined {
|
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.lastComment.publishedAt;
|
||||||
}
|
}
|
||||||
return this.discussion.updatedAt;
|
return this.discussion.updatedAt;
|
||||||
@ -83,7 +98,8 @@ export default class DiscussionListItem extends Vue {
|
|||||||
|
|
||||||
.discussion-minimalist-title {
|
.discussion-minimalist-title {
|
||||||
color: #3c376e;
|
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-size: 1.25rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -117,11 +117,21 @@
|
|||||||
<b-icon icon="format-quote-close" />
|
<b-icon icon="format-quote-close" />
|
||||||
</button>
|
</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" />
|
<b-icon icon="undo" />
|
||||||
</button>
|
</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" />
|
<b-icon icon="redo" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -181,7 +191,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -212,7 +224,7 @@ import { SEARCH_PERSONS } from "../graphql/search";
|
|||||||
import { Actor, IActor, IPerson } from "../types/actor";
|
import { Actor, IActor, IPerson } from "../types/actor";
|
||||||
import Image from "./Editor/Image";
|
import Image from "./Editor/Image";
|
||||||
import MaxSize from "./Editor/MaxSize";
|
import MaxSize from "./Editor/MaxSize";
|
||||||
import { UPLOAD_PICTURE } from "../graphql/upload";
|
import { UPLOAD_MEDIA } from "../graphql/upload";
|
||||||
import { listenFileUpload } from "../utils/upload";
|
import { listenFileUpload } from "../utils/upload";
|
||||||
import { CURRENT_ACTOR_CLIENT } from "../graphql/actor";
|
import { CURRENT_ACTOR_CLIENT } from "../graphql/actor";
|
||||||
import { IComment } from "../types/comment.model";
|
import { IComment } from "../types/comment.model";
|
||||||
@ -372,7 +384,7 @@ export default class EditorComponent extends Vue {
|
|||||||
searchText: query,
|
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;
|
this.filteredActors = result.data.searchPersons.elements;
|
||||||
return this.filteredActors;
|
return this.filteredActors;
|
||||||
},
|
},
|
||||||
@ -395,6 +407,7 @@ export default class EditorComponent extends Vue {
|
|||||||
new Image(),
|
new Image(),
|
||||||
new MaxSize({ maxSize: this.maxSize }),
|
new MaxSize({ maxSize: this.maxSize }),
|
||||||
],
|
],
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
onUpdate: ({ getHTML }: { getHTML: Function }) => {
|
onUpdate: ({ getHTML }: { getHTML: Function }) => {
|
||||||
this.$emit("input", getHTML());
|
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 {
|
showLinkMenu(command: Function, active: boolean): Function | undefined {
|
||||||
if (!this.editor) return undefined;
|
if (!this.editor) return undefined;
|
||||||
if (active) return command({ href: null });
|
if (active) return command({ href: null });
|
||||||
@ -430,7 +444,8 @@ export default class EditorComponent extends Vue {
|
|||||||
|
|
||||||
upHandler(): void {
|
upHandler(): void {
|
||||||
this.navigatedActorIndex =
|
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
|
* if it's the last item, navigate to the first one
|
||||||
*/
|
*/
|
||||||
downHandler(): void {
|
downHandler(): void {
|
||||||
this.navigatedActorIndex = (this.navigatedActorIndex + 1) % this.filteredActors.length;
|
this.navigatedActorIndex =
|
||||||
|
(this.navigatedActorIndex + 1) % this.filteredActors.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
enterHandler(): void {
|
enterHandler(): void {
|
||||||
@ -522,19 +538,22 @@ export default class EditorComponent extends Vue {
|
|||||||
* Show a file prompt, upload picture and insert it into editor
|
* Show a file prompt, upload picture and insert it into editor
|
||||||
* @param command
|
* @param command
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
async showImagePrompt(command: Function): Promise<void> {
|
async showImagePrompt(command: Function): Promise<void> {
|
||||||
const image = await listenFileUpload();
|
const image = await listenFileUpload();
|
||||||
try {
|
try {
|
||||||
const { data } = await this.$apollo.mutate({
|
const { data } = await this.$apollo.mutate({
|
||||||
mutation: UPLOAD_PICTURE,
|
mutation: UPLOAD_MEDIA,
|
||||||
variables: {
|
variables: {
|
||||||
file: image,
|
file: image,
|
||||||
name: image.name,
|
name: image.name,
|
||||||
actorId: this.currentActor.id,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (data.uploadPicture && data.uploadPicture.url) {
|
if (data.uploadMedia && data.uploadMedia.url) {
|
||||||
command({ src: data.uploadPicture.url });
|
command({
|
||||||
|
src: data.uploadMedia.url,
|
||||||
|
"data-media-id": data.uploadMedia.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { Node } from "tiptap";
|
import { Node } from "tiptap";
|
||||||
import { UPLOAD_PICTURE } from "@/graphql/upload";
|
import { UPLOAD_MEDIA } from "@/graphql/upload";
|
||||||
import apolloProvider from "@/vue-apollo";
|
import apolloProvider from "@/vue-apollo";
|
||||||
import ApolloClient from "apollo-client";
|
import ApolloClient from "apollo-client";
|
||||||
import { NormalizedCacheObject } from "apollo-cache-inmemory";
|
import { NormalizedCacheObject } from "apollo-cache-inmemory";
|
||||||
@ -12,6 +13,7 @@ import { EditorView } from "prosemirror-view";
|
|||||||
/* eslint-disable class-methods-use-this */
|
/* eslint-disable class-methods-use-this */
|
||||||
|
|
||||||
export default class Image extends Node {
|
export default class Image extends Node {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
get name() {
|
get name() {
|
||||||
return "image";
|
return "image";
|
||||||
}
|
}
|
||||||
@ -27,16 +29,18 @@ export default class Image extends Node {
|
|||||||
title: {
|
title: {
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
"data-media-id": {},
|
||||||
},
|
},
|
||||||
group: "inline",
|
group: "inline",
|
||||||
draggable: true,
|
draggable: true,
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{
|
{
|
||||||
tag: "img[src]",
|
tag: "img",
|
||||||
getAttrs: (dom: any) => ({
|
getAttrs: (dom: any) => ({
|
||||||
src: dom.getAttribute("src"),
|
src: dom.getAttribute("src"),
|
||||||
title: dom.getAttribute("title"),
|
title: dom.getAttribute("title"),
|
||||||
alt: dom.getAttribute("alt"),
|
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 {
|
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 { 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 node = type.create(attrs);
|
||||||
const transaction = state.tr.insert(position, node);
|
const transaction = state.tr.insert(position, node);
|
||||||
dispatch(transaction);
|
dispatch(transaction);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
get plugins() {
|
get plugins() {
|
||||||
return [
|
return [
|
||||||
new Plugin({
|
new Plugin({
|
||||||
@ -72,7 +82,8 @@ export default class Image extends Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const images = Array.from(realEvent.dataTransfer.files).filter(
|
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) {
|
if (images.length === 0) {
|
||||||
@ -88,21 +99,24 @@ export default class Image extends Node {
|
|||||||
});
|
});
|
||||||
if (!coordinates) return false;
|
if (!coordinates) return false;
|
||||||
const client = apolloProvider.defaultClient as ApolloClient<NormalizedCacheObject>;
|
const client = apolloProvider.defaultClient as ApolloClient<NormalizedCacheObject>;
|
||||||
const editorElem = document.getElementById("tiptab-editor");
|
|
||||||
const actorId = editorElem && editorElem.dataset.actorId;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
images.forEach(async (image) => {
|
images.forEach(async (image) => {
|
||||||
const { data } = await client.mutate({
|
const { data } = await client.mutate({
|
||||||
mutation: UPLOAD_PICTURE,
|
mutation: UPLOAD_MEDIA,
|
||||||
variables: {
|
variables: {
|
||||||
actorId,
|
|
||||||
file: image,
|
file: image,
|
||||||
name: image.name,
|
name: image.name,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const node = schema.nodes.image.create({ src: data.uploadPicture.url });
|
const node = schema.nodes.image.create({
|
||||||
const transaction = view.state.tr.insert(coordinates.pos, node);
|
src: data.uploadMedia.url,
|
||||||
|
"data-media-id": data.uploadMedia.id,
|
||||||
|
});
|
||||||
|
const transaction = view.state.tr.insert(
|
||||||
|
coordinates.pos,
|
||||||
|
node
|
||||||
|
);
|
||||||
view.dispatch(transaction);
|
view.dispatch(transaction);
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { Extension, Plugin } from "tiptap";
|
import { Extension, Plugin } from "tiptap";
|
||||||
|
|
||||||
export default class MaxSize extends Extension {
|
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() {
|
get name() {
|
||||||
return "maxSize";
|
return "maxSize";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
get defaultOptions() {
|
get defaultOptions() {
|
||||||
return {
|
return {
|
||||||
maxSize: null,
|
maxSize: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
get plugins() {
|
get plugins() {
|
||||||
return [
|
return [
|
||||||
new Plugin({
|
new Plugin({
|
||||||
@ -21,7 +27,7 @@ export default class MaxSize extends Extension {
|
|||||||
const newLength = newState.doc.content.size;
|
const newLength = newState.doc.content.size;
|
||||||
|
|
||||||
if (newLength > max && newLength > oldLength) {
|
if (newLength > max && newLength > oldLength) {
|
||||||
let newTr = newState.tr;
|
const newTr = newState.tr;
|
||||||
newTr.insertText("", max + 1, newLength);
|
newTr.insertText("", max + 1, newLength);
|
||||||
|
|
||||||
return newTr;
|
return newTr;
|
||||||
|
@ -21,9 +21,13 @@
|
|||||||
</b-autocomplete>
|
</b-autocomplete>
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field v-if="isSecureContext()">
|
<b-field v-if="isSecureContext()">
|
||||||
<b-button type="is-text" v-if="!gettingLocation" icon-right="target" @click="locateMe">{{
|
<b-button
|
||||||
$t("Use my location")
|
type="is-text"
|
||||||
}}</b-button>
|
v-if="!gettingLocation"
|
||||||
|
icon-right="target"
|
||||||
|
@click="locateMe"
|
||||||
|
>{{ $t("Use my location") }}</b-button
|
||||||
|
>
|
||||||
<span v-else>{{ $t("Getting location") }}</span>
|
<span v-else>{{ $t("Getting location") }}</span>
|
||||||
</b-field>
|
</b-field>
|
||||||
<!--
|
<!--
|
||||||
@ -50,7 +54,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
import { LatLng } from "leaflet";
|
import { LatLng } from "leaflet";
|
||||||
import { debounce } from "lodash";
|
import { debounce, DebouncedFunc } from "lodash";
|
||||||
import { Address, IAddress } from "../../types/address.model";
|
import { Address, IAddress } from "../../types/address.model";
|
||||||
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
|
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
|
||||||
import { CONFIG } from "../../graphql/config";
|
import { CONFIG } from "../../graphql/config";
|
||||||
@ -58,7 +62,8 @@ import { IConfig } from "../../types/config.model";
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
"map-leaflet": () => import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
|
"map-leaflet": () =>
|
||||||
|
import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
config: CONFIG,
|
config: CONFIG,
|
||||||
@ -81,7 +86,8 @@ export default class AddressAutoComplete extends Vue {
|
|||||||
|
|
||||||
private gettingLocation = false;
|
private gettingLocation = false;
|
||||||
|
|
||||||
private location!: Position;
|
// eslint-disable-next-line no-undef
|
||||||
|
private location!: GeolocationPosition;
|
||||||
|
|
||||||
private gettingLocationError: any;
|
private gettingLocationError: any;
|
||||||
|
|
||||||
@ -89,7 +95,7 @@ export default class AddressAutoComplete extends Vue {
|
|||||||
|
|
||||||
config!: IConfig;
|
config!: IConfig;
|
||||||
|
|
||||||
fetchAsyncData!: Function;
|
fetchAsyncData!: DebouncedFunc<(query: string) => Promise<void>>;
|
||||||
|
|
||||||
// We put this in data because of issues like
|
// We put this in data because of issues like
|
||||||
// https://github.com/vuejs/vue-class-component/issues/263
|
// 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;
|
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) {
|
if (this.addressData.length > 0) {
|
||||||
const defaultAddress = new Address(this.addressData[0]);
|
const defaultAddress = new Address(this.addressData[0]);
|
||||||
this.selected = defaultAddress;
|
this.selected = defaultAddress;
|
||||||
@ -197,7 +207,10 @@ export default class AddressAutoComplete extends Vue {
|
|||||||
this.location = await AddressAutoComplete.getLocation();
|
this.location = await AddressAutoComplete.getLocation();
|
||||||
this.mapDefaultZoom = 12;
|
this.mapDefaultZoom = 12;
|
||||||
this.reverseGeoCode(
|
this.reverseGeoCode(
|
||||||
new LatLng(this.location.coords.latitude, this.location.coords.longitude),
|
new LatLng(
|
||||||
|
this.location.coords.latitude,
|
||||||
|
this.location.coords.longitude
|
||||||
|
),
|
||||||
12
|
12
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -207,7 +220,8 @@ export default class AddressAutoComplete extends Vue {
|
|||||||
this.gettingLocation = false;
|
this.gettingLocation = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getLocation(): Promise<Position> {
|
// eslint-disable-next-line no-undef
|
||||||
|
static async getLocation(): Promise<GeolocationPosition> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!("geolocation" in navigator)) {
|
if (!("geolocation" in navigator)) {
|
||||||
reject(new Error("Geolocation is not available."));
|
reject(new Error("Geolocation is not available."));
|
||||||
|
@ -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>
|
|
@ -1,5 +1,8 @@
|
|||||||
<template>
|
<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">
|
<div class="card-image">
|
||||||
<figure
|
<figure
|
||||||
class="image is-16by9"
|
class="image is-16by9"
|
||||||
@ -7,10 +10,19 @@
|
|||||||
event.picture ? event.picture.url : '/img/mobilizon_default_card.png'
|
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
|
<router-link
|
||||||
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
|
: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"
|
:key="tag.slug"
|
||||||
>
|
>
|
||||||
<b-tag type="is-light">{{ tag.title }}</b-tag>
|
<b-tag type="is-light">{{ tag.title }}</b-tag>
|
||||||
@ -21,14 +33,18 @@
|
|||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left">
|
<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>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="event-title">{{ event.title }}</p>
|
<p class="event-title">{{ event.title }}</p>
|
||||||
<div class="event-subtitle" v-if="event.physicalAddress">
|
<div class="event-subtitle" v-if="event.physicalAddress">
|
||||||
<!-- <p>{{ $t('By @{username}', { username: actor.preferredUsername }) }}</p>-->
|
<!-- <p>{{ $t('By @{username}', { username: actor.preferredUsername }) }}</p>-->
|
||||||
<span>
|
<span>
|
||||||
{{ event.physicalAddress.description }}, {{ event.physicalAddress.locality }}
|
{{ event.physicalAddress.description }},
|
||||||
|
{{ event.physicalAddress.locality }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -77,7 +93,7 @@ import { IEvent, IEventCardOptions } from "@/types/event.model";
|
|||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||||
import { Actor, Person } from "@/types/actor";
|
import { Actor, Person } from "@/types/actor";
|
||||||
import { ParticipantRole } from "../../types/participant.model";
|
import { EventStatus, ParticipantRole } from "@/types/enums";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -92,6 +108,8 @@ export default class EventCard extends Vue {
|
|||||||
|
|
||||||
ParticipantRole = ParticipantRole;
|
ParticipantRole = ParticipantRole;
|
||||||
|
|
||||||
|
EventStatus = EventStatus;
|
||||||
|
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
|
|
||||||
defaultOptions: IEventCardOptions = {
|
defaultOptions: IEventCardOptions = {
|
||||||
|
@ -18,7 +18,9 @@
|
|||||||
</docs>
|
</docs>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span v-if="!endsOn">{{ beginsOn | formatDateTimeString(showStartTime) }}</span>
|
<span v-if="!endsOn">{{
|
||||||
|
beginsOn | formatDateTimeString(showStartTime)
|
||||||
|
}}</span>
|
||||||
<span v-else-if="isSameDay() && showStartTime && showEndTime">
|
<span v-else-if="isSameDay() && showStartTime && showEndTime">
|
||||||
{{
|
{{
|
||||||
$t("On {date} from {startTime} to {endTime}", {
|
$t("On {date} from {startTime} to {endTime}", {
|
||||||
@ -44,7 +46,9 @@
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</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">
|
<span v-else-if="endsOn && showStartTime && showEndTime">
|
||||||
{{
|
{{
|
||||||
$t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
|
$t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
|
||||||
@ -97,7 +101,9 @@ export default class EventFullDate extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isSameDay(): boolean {
|
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;
|
return this.endsOn !== undefined && sameDay;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<article class="box">
|
<article class="box">
|
||||||
<div class="columns">
|
<div class="identity-header">
|
||||||
<div class="content column">
|
<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="title-wrapper">
|
||||||
<div class="date-component">
|
<div class="date-component">
|
||||||
<date-calendar-icon :date="participation.event.beginsOn" />
|
<date-calendar-icon :date="participation.event.beginsOn" />
|
||||||
</div>
|
</div>
|
||||||
<router-link :to="{ name: RouteName.EVENT, params: { uuid: participation.event.uuid } }">
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: RouteName.EVENT,
|
||||||
|
params: { uuid: participation.event.uuid },
|
||||||
|
}"
|
||||||
|
>
|
||||||
<h3 class="title">{{ participation.event.title }}</h3>
|
<h3 class="title">{{ participation.event.title }}</h3>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="participation-actor has-text-grey">
|
<div class="participation-actor has-text-grey">
|
||||||
<span>
|
<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
|
<b-icon
|
||||||
icon="link"
|
icon="link"
|
||||||
v-else-if="participation.event.visibility === EventVisibility.UNLISTED"
|
v-else-if="
|
||||||
|
participation.event.visibility === EventVisibility.UNLISTED
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<b-icon
|
<b-icon
|
||||||
icon="lock"
|
icon="lock"
|
||||||
v-else-if="participation.event.visibility === EventVisibility.PRIVATE"
|
v-else-if="
|
||||||
|
participation.event.visibility === EventVisibility.PRIVATE
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="
|
v-if="
|
||||||
participation.event.physicalAddress && participation.event.physicalAddress.locality
|
participation.event.physicalAddress &&
|
||||||
|
participation.event.physicalAddress.locality
|
||||||
"
|
"
|
||||||
>{{ participation.event.physicalAddress.locality }} -</span
|
>{{ participation.event.physicalAddress.locality }} -</span
|
||||||
>
|
>
|
||||||
@ -32,19 +51,10 @@
|
|||||||
<i18n tag="span" path="Organized by {name}">
|
<i18n tag="span" path="Organized by {name}">
|
||||||
<popover-actor-card
|
<popover-actor-card
|
||||||
slot="name"
|
slot="name"
|
||||||
:actor="participation.event.organizerActor"
|
:actor="organizerActor"
|
||||||
:inline="true"
|
:inline="true"
|
||||||
>
|
>
|
||||||
{{ participation.event.organizerActor.displayName() }}
|
{{ 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() }}
|
|
||||||
</popover-actor-card>
|
</popover-actor-card>
|
||||||
</i18n>
|
</i18n>
|
||||||
</span>
|
</span>
|
||||||
@ -53,12 +63,15 @@
|
|||||||
<span
|
<span
|
||||||
class="participant-stats"
|
class="participant-stats"
|
||||||
v-if="
|
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(
|
$tc(
|
||||||
"{available}/{capacity} available places",
|
"{available}/{capacity} available places",
|
||||||
@ -68,16 +81,21 @@
|
|||||||
available:
|
available:
|
||||||
participation.event.options.maximumAttendeeCapacity -
|
participation.event.options.maximumAttendeeCapacity -
|
||||||
participation.event.participantStats.participant,
|
participation.event.participantStats.participant,
|
||||||
capacity: participation.event.options.maximumAttendeeCapacity,
|
capacity:
|
||||||
|
participation.event.options.maximumAttendeeCapacity,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{
|
{{
|
||||||
$tc("{count} participants", participation.event.participantStats.participant, {
|
$tc(
|
||||||
count: participation.event.participantStats.participant,
|
"{count} participants",
|
||||||
})
|
participation.event.participantStats.participant,
|
||||||
|
{
|
||||||
|
count: participation.event.participantStats.participant,
|
||||||
|
}
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="participation.event.participantStats.notApproved > 0">
|
<span v-if="participation.event.participantStats.notApproved > 0">
|
||||||
@ -103,80 +121,90 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions column is-narrow">
|
<div class="actions">
|
||||||
<ul>
|
<b-dropdown aria-role="list" position="is-bottom-left">
|
||||||
<li
|
<b-button slot="trigger" role="button" icon-right="dots-horizontal">
|
||||||
|
{{ $t("Actions") }}
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
<b-dropdown-item
|
||||||
v-if="
|
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
|
<b-icon icon="pencil" />
|
||||||
type="is-text"
|
{{ $t("Edit") }}
|
||||||
@click="
|
</b-dropdown-item>
|
||||||
gotToWithCheck(participation, {
|
|
||||||
name: RouteName.EDIT_EVENT,
|
<b-dropdown-item
|
||||||
params: { eventId: participation.event.uuid },
|
v-if="participation.role === ParticipantRole.CREATOR"
|
||||||
})
|
aria-role="listitem"
|
||||||
"
|
@click="
|
||||||
icon-left="pencil"
|
gotToWithCheck(participation, {
|
||||||
>{{ $t("Edit") }}</b-button
|
name: RouteName.DUPLICATE_EVENT,
|
||||||
>
|
params: { eventId: participation.event.uuid },
|
||||||
</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="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"
|
@click="openDeleteEventModalWrapper"
|
||||||
>
|
>
|
||||||
<b-button type="is-text" icon-left="delete">{{ $t("Delete") }}</b-button>
|
<b-icon icon="delete" />
|
||||||
</li>
|
{{ $t("Delete") }}
|
||||||
<li
|
</b-dropdown-item>
|
||||||
|
|
||||||
|
<b-dropdown-item
|
||||||
v-if="
|
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
|
<b-icon icon="account-multiple-plus" />
|
||||||
type="is-text"
|
{{ $t("Manage participations") }}
|
||||||
@click="
|
</b-dropdown-item>
|
||||||
gotToWithCheck(participation, {
|
|
||||||
name: RouteName.PARTICIPATIONS,
|
<b-dropdown-item aria-role="listitem" has-link>
|
||||||
params: { eventId: participation.event.uuid },
|
<router-link
|
||||||
})
|
:to="{
|
||||||
"
|
name: RouteName.EVENT,
|
||||||
icon-left="account-multiple-plus"
|
params: { uuid: participation.event.uuid },
|
||||||
>{{ $t("Manage participations") }}</b-button
|
}"
|
||||||
>
|
>
|
||||||
</li>
|
<b-icon icon="view-compact" />
|
||||||
<li>
|
{{ $t("View event page") }}
|
||||||
<b-button
|
</router-link>
|
||||||
tag="router-link"
|
</b-dropdown-item>
|
||||||
icon-left="view-compact"
|
</b-dropdown>
|
||||||
type="is-text"
|
|
||||||
:to="{ name: RouteName.EVENT, params: { uuid: participation.event.uuid } }"
|
|
||||||
>{{ $t("View event page") }}</b-button
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@ -187,9 +215,10 @@ import { Component, Prop } from "vue-property-decorator";
|
|||||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||||
import { mixins } from "vue-class-component";
|
import { mixins } from "vue-class-component";
|
||||||
import { RawLocation, Route } from "vue-router";
|
import { RawLocation, Route } from "vue-router";
|
||||||
import { IParticipant, ParticipantRole } from "../../types/participant.model";
|
import { EventVisibility, ParticipantRole } from "@/types/enums";
|
||||||
import { EventVisibility, IEventCardOptions } from "../../types/event.model";
|
import { IParticipant } from "../../types/participant.model";
|
||||||
import { IPerson } from "../../types/actor";
|
import { IEventCardOptions } from "../../types/event.model";
|
||||||
|
import { displayNameAndUsername, IActor, IPerson } from "../../types/actor";
|
||||||
import ActorMixin from "../../mixins/actor";
|
import ActorMixin from "../../mixins/actor";
|
||||||
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
||||||
import EventMixin from "../../mixins/event";
|
import EventMixin from "../../mixins/event";
|
||||||
@ -234,6 +263,8 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
|
|||||||
|
|
||||||
EventVisibility = EventVisibility;
|
EventVisibility = EventVisibility;
|
||||||
|
|
||||||
|
displayNameAndUsername = displayNameAndUsername;
|
||||||
|
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
|
|
||||||
get mergedOptions(): IEventCardOptions {
|
get mergedOptions(): IEventCardOptions {
|
||||||
@ -246,11 +277,17 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
|
|||||||
async openDeleteEventModalWrapper(): Promise<void> {
|
async openDeleteEventModalWrapper(): Promise<void> {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await this.openDeleteEventModal(this.participation.event, this.currentActor);
|
await this.openDeleteEventModal(this.participation.event);
|
||||||
}
|
}
|
||||||
|
|
||||||
async gotToWithCheck(participation: IParticipant, route: RawLocation): Promise<Route> {
|
async gotToWithCheck(
|
||||||
if (participation.actor.id !== this.currentActor.id && participation.event.organizerActor) {
|
participation: IParticipant,
|
||||||
|
route: RawLocation
|
||||||
|
): Promise<Route> {
|
||||||
|
if (
|
||||||
|
participation.actor.id !== this.currentActor.id &&
|
||||||
|
participation.event.organizerActor
|
||||||
|
) {
|
||||||
const organizer = participation.event.organizerActor as IPerson;
|
const organizer = participation.event.organizerActor as IPerson;
|
||||||
await changeIdentity(this.$apollo.provider.defaultClient, organizer);
|
await changeIdentity(this.$apollo.provider.defaultClient, organizer);
|
||||||
this.$buefy.notification.open({
|
this.$buefy.notification.open({
|
||||||
@ -267,6 +304,16 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
|
|||||||
}
|
}
|
||||||
return this.$router.push(route);
|
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>
|
</script>
|
||||||
|
|
||||||
@ -293,74 +340,71 @@ article.box {
|
|||||||
line-height: 1.75em;
|
line-height: 1.75em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div.content {
|
|
||||||
|
.list-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
padding-right: 7.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
.participation-actor span,
|
||||||
|
.participant-stats span {
|
||||||
|
padding: 0 5px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
height: auto;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.title-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
div.date-component {
|
||||||
|
flex: 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1em;
|
||||||
|
font-size: 1.6em;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
margin: auto 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.identity-header {
|
||||||
|
background: $yellow-2;
|
||||||
|
display: flex;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|
||||||
.participation-actor span,
|
figure {
|
||||||
.participant-stats span {
|
padding-right: 3px;
|
||||||
padding: 0 5px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
height: auto;
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
div.title-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
div.date-component {
|
|
||||||
flex: 0;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1em;
|
|
||||||
font-size: 1.6em;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
margin: auto 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
& > .columns {
|
||||||
ul li {
|
padding: 1.25rem;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -6,7 +6,9 @@
|
|||||||
<div class="date-component">
|
<div class="date-component">
|
||||||
<date-calendar-icon :date="event.beginsOn" />
|
<date-calendar-icon :date="event.beginsOn" />
|
||||||
</div>
|
</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>
|
<h2 class="title">{{ event.title }}</h2>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
@ -15,17 +17,34 @@
|
|||||||
{{ event.physicalAddress.locality }}
|
{{ event.physicalAddress.locality }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="event.attributedTo && options.memberofGroup">
|
<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>
|
||||||
<span v-else-if="options.memberofGroup">
|
<span v-else-if="options.memberofGroup">
|
||||||
{{ $t("Organized by {name}", { name: usernameWithDomain(event.organizerActor) }) }}
|
{{
|
||||||
|
$t("Organized by {name}", {
|
||||||
|
name: usernameWithDomain(event.organizerActor),
|
||||||
|
})
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<span class="column is-narrow">
|
<span class="column is-narrow">
|
||||||
<b-icon icon="earth" v-if="event.visibility === EventVisibility.PUBLIC" />
|
<b-icon
|
||||||
<b-icon icon="link" v-if="event.visibility === EventVisibility.UNLISTED" />
|
icon="earth"
|
||||||
<b-icon icon="lock" v-if="event.visibility === EventVisibility.PRIVATE" />
|
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>
|
||||||
<span class="column is-narrow participant-stats">
|
<span class="column is-narrow participant-stats">
|
||||||
<span v-if="event.options.maximumAttendeeCapacity !== 0">
|
<span v-if="event.options.maximumAttendeeCapacity !== 0">
|
||||||
@ -38,9 +57,13 @@
|
|||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{
|
{{
|
||||||
$tc("{count} participants", event.participantStats.participant, {
|
$tc(
|
||||||
count: event.participantStats.participant,
|
"{count} participants",
|
||||||
})
|
event.participantStats.participant,
|
||||||
|
{
|
||||||
|
count: event.participantStats.participant,
|
||||||
|
}
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@ -51,7 +74,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<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 { Component, Prop } from "vue-property-decorator";
|
||||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||||
import { IPerson, usernameWithDomain } from "@/types/actor";
|
import { IPerson, usernameWithDomain } from "@/types/actor";
|
||||||
@ -59,7 +82,7 @@ import { mixins } from "vue-class-component";
|
|||||||
import ActorMixin from "@/mixins/actor";
|
import ActorMixin from "@/mixins/actor";
|
||||||
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
|
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
|
||||||
import EventMixin from "@/mixins/event";
|
import EventMixin from "@/mixins/event";
|
||||||
import { ParticipantRole } from "../../types/participant.model";
|
import { EventVisibility, ParticipantRole } from "@/types/enums";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
|
||||||
const defaultOptions: IEventCardOptions = {
|
const defaultOptions: IEventCardOptions = {
|
||||||
|
@ -14,10 +14,12 @@
|
|||||||
{{
|
{{
|
||||||
$tc(
|
$tc(
|
||||||
"{available}/{capacity} available places",
|
"{available}/{capacity} available places",
|
||||||
event.options.maximumAttendeeCapacity - event.participantStats.participant,
|
event.options.maximumAttendeeCapacity -
|
||||||
|
event.participantStats.participant,
|
||||||
{
|
{
|
||||||
available:
|
available:
|
||||||
event.options.maximumAttendeeCapacity - event.participantStats.participant,
|
event.options.maximumAttendeeCapacity -
|
||||||
|
event.participantStats.participant,
|
||||||
capacity: event.options.maximumAttendeeCapacity,
|
capacity: event.options.maximumAttendeeCapacity,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -42,9 +44,13 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
$tc("{count} requests waiting", event.participantStats.notApproved, {
|
$tc(
|
||||||
count: event.participantStats.notApproved,
|
"{count} requests waiting",
|
||||||
})
|
event.participantStats.notApproved,
|
||||||
|
{
|
||||||
|
count: event.participantStats.notApproved,
|
||||||
|
}
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
</b-button>
|
</b-button>
|
||||||
</span>
|
</span>
|
||||||
@ -56,7 +62,7 @@
|
|||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import { IEvent } from "@/types/event.model";
|
import { IEvent } from "@/types/event.model";
|
||||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||||
import { ParticipantRole } from "../../types/participant.model";
|
import { ParticipantRole } from "@/types/enums";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -88,7 +94,8 @@ export default class EventMinimalistCard extends Vue {
|
|||||||
|
|
||||||
.event-minimalist-title {
|
.event-minimalist-title {
|
||||||
color: #3c376e;
|
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-size: 1.25rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
@ -33,9 +33,12 @@
|
|||||||
<div v-else-if="queryText.length >= 3" class="is-enabled">
|
<div v-else-if="queryText.length >= 3" class="is-enabled">
|
||||||
<span>{{ $t('No results for "{queryText}"') }}</span>
|
<span>{{ $t('No results for "{queryText}"') }}</span>
|
||||||
<span>{{
|
<span>{{
|
||||||
$t("You can try another search term or drag and drop the marker on the map", {
|
$t(
|
||||||
queryText,
|
"You can try another search term or drag and drop the marker on the map",
|
||||||
})
|
{
|
||||||
|
queryText,
|
||||||
|
}
|
||||||
|
)
|
||||||
}}</span>
|
}}</span>
|
||||||
<!-- <p class="control" @click="openNewAddressModal">-->
|
<!-- <p class="control" @click="openNewAddressModal">-->
|
||||||
<!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
|
<!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
|
||||||
@ -102,7 +105,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
import { LatLng } from "leaflet";
|
import { LatLng } from "leaflet";
|
||||||
import { debounce } from "lodash";
|
import { debounce, DebouncedFunc } from "lodash";
|
||||||
import { Address, IAddress } from "../../types/address.model";
|
import { Address, IAddress } from "../../types/address.model";
|
||||||
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
|
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
|
||||||
import { CONFIG } from "../../graphql/config";
|
import { CONFIG } from "../../graphql/config";
|
||||||
@ -110,7 +113,8 @@ import { IConfig } from "../../types/config.model";
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
"map-leaflet": () => import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
|
"map-leaflet": () =>
|
||||||
|
import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
config: CONFIG,
|
config: CONFIG,
|
||||||
@ -133,7 +137,8 @@ export default class FullAddressAutoComplete extends Vue {
|
|||||||
|
|
||||||
private gettingLocation = false;
|
private gettingLocation = false;
|
||||||
|
|
||||||
private location!: Position;
|
// eslint-disable-next-line no-undef
|
||||||
|
private location!: GeolocationPosition;
|
||||||
|
|
||||||
private gettingLocationError: any;
|
private gettingLocationError: any;
|
||||||
|
|
||||||
@ -141,11 +146,11 @@ export default class FullAddressAutoComplete extends Vue {
|
|||||||
|
|
||||||
config!: IConfig;
|
config!: IConfig;
|
||||||
|
|
||||||
fetchAsyncData!: Function;
|
fetchAsyncData!: DebouncedFunc<(query: string) => Promise<void>>;
|
||||||
|
|
||||||
// We put this in data because of issues like
|
// We put this in data because of issues like
|
||||||
// https://github.com/vuejs/vue-class-component/issues/263
|
// https://github.com/vuejs/vue-class-component/issues/263
|
||||||
data() {
|
data(): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
fetchAsyncData: debounce(this.asyncData, 200),
|
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;
|
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) {
|
if (this.addressData.length > 0) {
|
||||||
const defaultAddress = new Address(this.addressData[0]);
|
const defaultAddress = new Address(this.addressData[0]);
|
||||||
this.selected = defaultAddress;
|
this.selected = defaultAddress;
|
||||||
@ -248,7 +257,10 @@ export default class FullAddressAutoComplete extends Vue {
|
|||||||
this.location = await FullAddressAutoComplete.getLocation();
|
this.location = await FullAddressAutoComplete.getLocation();
|
||||||
this.mapDefaultZoom = 12;
|
this.mapDefaultZoom = 12;
|
||||||
this.reverseGeoCode(
|
this.reverseGeoCode(
|
||||||
new LatLng(this.location.coords.latitude, this.location.coords.longitude),
|
new LatLng(
|
||||||
|
this.location.coords.latitude,
|
||||||
|
this.location.coords.longitude
|
||||||
|
),
|
||||||
12
|
12
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -266,7 +278,8 @@ export default class FullAddressAutoComplete extends Vue {
|
|||||||
return window.isSecureContext;
|
return window.isSecureContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getLocation(): Promise<Position> {
|
// eslint-disable-next-line no-undef
|
||||||
|
static async getLocation(): Promise<GeolocationPosition> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!("geolocation" in navigator)) {
|
if (!("geolocation" in navigator)) {
|
||||||
reject(new Error("Geolocation is not available."));
|
reject(new Error("Geolocation is not available."));
|
||||||
|
@ -10,9 +10,18 @@
|
|||||||
>
|
>
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<figure class="image is-48x48" v-if="availableActor.avatar">
|
<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>
|
</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="media-content">
|
||||||
<h3>{{ availableActor.name }}</h3>
|
<h3>{{ availableActor.name }}</h3>
|
||||||
<small>{{ `@${availableActor.preferredUsername}` }}</small>
|
<small>{{ `@${availableActor.preferredUsername}` }}</small>
|
||||||
@ -23,9 +32,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
import { IMember, IPerson, MemberRole, IActor, Actor } from "@/types/actor";
|
import { IPerson, IActor, Actor } from "@/types/actor";
|
||||||
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||||
import { Paginate } from "@/types/paginate";
|
import { Paginate } from "@/types/paginate";
|
||||||
|
import { IMember } from "@/types/actor/member.model";
|
||||||
|
import { MemberRole } from "@/types/enums";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
apollo: {
|
apollo: {
|
||||||
@ -59,16 +70,21 @@ export default class OrganizerPicker extends Vue {
|
|||||||
get actualMemberships(): IMember[] {
|
get actualMemberships(): IMember[] {
|
||||||
if (this.restrictModeratorLevel) {
|
if (this.restrictModeratorLevel) {
|
||||||
return this.groupMemberships.elements.filter((membership: IMember) =>
|
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;
|
return this.groupMemberships.elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
get actualAvailableActors(): IActor[] {
|
get actualAvailableActors(): IActor[] {
|
||||||
return [this.identity, ...this.actualMemberships.map((member) => member.parent)];
|
return [
|
||||||
|
this.identity,
|
||||||
|
...this.actualMemberships.map((member) => member.parent),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch("currentActor")
|
@Watch("currentActor")
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="organizer-picker">
|
<div class="organizer-picker">
|
||||||
<!-- If we have a current actor (inline) -->
|
<!-- 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">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-48x48" v-if="currentActor.avatar">
|
<figure class="image is-48x48" v-if="currentActor.avatar">
|
||||||
@ -15,7 +19,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="media-content" v-if="currentActor.name">
|
<div class="media-content" v-if="currentActor.name">
|
||||||
<p class="is-4">{{ currentActor.name }}</p>
|
<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>
|
||||||
<div class="media-content" v-else>
|
<div class="media-content" v-else>
|
||||||
{{ `@${currentActor.preferredUsername}` }}
|
{{ `@${currentActor.preferredUsername}` }}
|
||||||
@ -26,7 +32,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- If we have a current actor -->
|
<!-- 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
|
<img
|
||||||
class="image is-48x48"
|
class="image is-48x48"
|
||||||
v-if="currentActor.avatar"
|
v-if="currentActor.avatar"
|
||||||
@ -40,13 +50,19 @@
|
|||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-48x48" v-if="identity.avatar">
|
<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>
|
</figure>
|
||||||
<b-icon v-else size="is-large" icon="account-circle" />
|
<b-icon v-else size="is-large" icon="account-circle" />
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content" v-if="identity.name">
|
<div class="media-content" v-if="identity.name">
|
||||||
<p class="is-4">{{ identity.name }}</p>
|
<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>
|
||||||
<div class="media-content" v-else>
|
<div class="media-content" v-else>
|
||||||
{{ `@${identity.preferredUsername}` }}
|
{{ `@${identity.preferredUsername}` }}
|
||||||
@ -74,7 +90,11 @@
|
|||||||
<div class="column">
|
<div class="column">
|
||||||
<div v-if="actorMembersForCurrentActor.length > 0">
|
<div v-if="actorMembersForCurrentActor.length > 0">
|
||||||
<p>{{ $t("Add a contact") }}</p>
|
<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">
|
<b-checkbox v-model="actualContacts" :native-value="actor.id">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
@ -89,7 +109,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="media-content" v-if="actor.name">
|
<div class="media-content" v-if="actor.name">
|
||||||
<p class="is-4">{{ actor.name }}</p>
|
<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>
|
||||||
<div class="media-content" v-else>
|
<div class="media-content" v-else>
|
||||||
{{ `@${actor.preferredUsername}` }}
|
{{ `@${actor.preferredUsername}` }}
|
||||||
@ -115,7 +137,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
import { 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 OrganizerPicker from "./OrganizerPicker.vue";
|
||||||
import { PERSON_MEMBERSHIPS_WITH_MEMBERS } from "../../graphql/actor";
|
import { PERSON_MEMBERSHIPS_WITH_MEMBERS } from "../../graphql/actor";
|
||||||
import { Paginate } from "../../types/paginate";
|
import { Paginate } from "../../types/paginate";
|
||||||
@ -150,7 +173,8 @@ export default class OrganizerPickerWrapper extends Vue {
|
|||||||
|
|
||||||
groupMemberships: Paginate<IMember> = { elements: [], total: 0 };
|
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);
|
actualContacts: (string | undefined)[] = this.contacts.map(({ id }) => id);
|
||||||
|
|
||||||
@ -171,7 +195,9 @@ export default class OrganizerPickerWrapper extends Vue {
|
|||||||
pickActor(): void {
|
pickActor(): void {
|
||||||
this.$emit(
|
this.$emit(
|
||||||
"update:contacts",
|
"update:contacts",
|
||||||
this.actorMembersForCurrentActor.filter(({ id }) => this.actualContacts.includes(id))
|
this.actorMembersForCurrentActor.filter(({ id }) =>
|
||||||
|
this.actualContacts.includes(id)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
this.$emit("input", this.currentActor);
|
this.$emit("input", this.currentActor);
|
||||||
this.isComponentModalActive = false;
|
this.isComponentModalActive = false;
|
||||||
@ -182,7 +208,9 @@ export default class OrganizerPickerWrapper extends Vue {
|
|||||||
({ parent: { id } }) => id === this.currentActor.id
|
({ parent: { id } }) => id === this.currentActor.id
|
||||||
);
|
);
|
||||||
if (currentMembership) {
|
if (currentMembership) {
|
||||||
return currentMembership.parent.members.elements.map(({ actor }) => actor);
|
return currentMembership.parent.members.elements.map(
|
||||||
|
({ actor }: { actor: IActor }) => actor
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -47,8 +47,16 @@ A button to set your participation
|
|||||||
>
|
>
|
||||||
</b-dropdown>
|
</b-dropdown>
|
||||||
|
|
||||||
<div v-else-if="participation && participation.role === ParticipantRole.NOT_APPROVED">
|
<div
|
||||||
<b-dropdown aria-role="list" position="is-bottom-left" class="dropdown-disabled">
|
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">
|
<button class="button is-success is-large" type="button" slot="trigger">
|
||||||
<b-icon icon="timer-sand-empty" />
|
<b-icon icon="timer-sand-empty" />
|
||||||
<template>
|
<template>
|
||||||
@ -74,9 +82,17 @@ A button to set your participation
|
|||||||
<small>{{ $t("Waiting for organization team approval.") }}</small>
|
<small>{{ $t("Waiting for organization team approval.") }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="participation && participation.role === ParticipantRole.REJECTED">
|
<div
|
||||||
|
v-else-if="
|
||||||
|
participation && participation.role === ParticipantRole.REJECTED
|
||||||
|
"
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
{{ $t("Unfortunately, your participation request was rejected by the organizers.") }}
|
{{
|
||||||
|
$t(
|
||||||
|
"Unfortunately, your participation request was rejected by the organizers."
|
||||||
|
)
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -92,7 +108,11 @@ A button to set your participation
|
|||||||
<b-icon icon="menu-down" />
|
<b-icon icon="menu-down" />
|
||||||
</button>
|
</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">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-32x32" v-if="currentActor.avatar">
|
<figure class="image is-32x32" v-if="currentActor.avatar">
|
||||||
@ -103,7 +123,8 @@ A button to set your participation
|
|||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
$t("as {identity}", {
|
$t("as {identity}", {
|
||||||
identity: currentActor.name || `@${currentActor.preferredUsername}`,
|
identity:
|
||||||
|
currentActor.name || `@${currentActor.preferredUsername}`,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
@ -121,7 +142,10 @@ A button to set your participation
|
|||||||
</b-dropdown>
|
</b-dropdown>
|
||||||
<b-button
|
<b-button
|
||||||
tag="router-link"
|
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"
|
v-else-if="!participation && hasAnonymousParticipationMethods"
|
||||||
type="is-primary"
|
type="is-primary"
|
||||||
size="is-large"
|
size="is-large"
|
||||||
@ -130,7 +154,10 @@ A button to set your participation
|
|||||||
>
|
>
|
||||||
<b-button
|
<b-button
|
||||||
tag="router-link"
|
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"
|
v-else-if="!currentActor.id"
|
||||||
type="is-primary"
|
type="is-primary"
|
||||||
size="is-large"
|
size="is-large"
|
||||||
@ -142,8 +169,9 @@ A button to set your participation
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import { IParticipant, ParticipantRole } from "../../types/participant.model";
|
import { EventJoinOptions, ParticipantRole } from "@/types/enums";
|
||||||
import { EventJoinOptions, IEvent } from "../../types/event.model";
|
import { IParticipant } from "../../types/participant.model";
|
||||||
|
import { IEvent } from "../../types/event.model";
|
||||||
import { IPerson, Person } from "../../types/actor";
|
import { IPerson, Person } from "../../types/actor";
|
||||||
import { CURRENT_ACTOR_CLIENT, IDENTITIES } from "../../graphql/actor";
|
import { CURRENT_ACTOR_CLIENT, IDENTITIES } from "../../graphql/actor";
|
||||||
import { CURRENT_USER_CLIENT } from "../../graphql/user";
|
import { CURRENT_USER_CLIENT } from "../../graphql/user";
|
||||||
@ -161,7 +189,9 @@ import RouteName from "../../router/name";
|
|||||||
identities: {
|
identities: {
|
||||||
query: IDENTITIES,
|
query: IDENTITIES,
|
||||||
update: ({ identities }) =>
|
update: ({ identities }) =>
|
||||||
identities ? identities.map((identity: IPerson) => new Person(identity)) : [],
|
identities
|
||||||
|
? identities.map((identity: IPerson) => new Person(identity))
|
||||||
|
: [],
|
||||||
skip() {
|
skip() {
|
||||||
return this.currentUser.isLoggedIn === false;
|
return this.currentUser.isLoggedIn === false;
|
||||||
},
|
},
|
||||||
|
@ -59,7 +59,12 @@
|
|||||||
<a :href="linkedInShareUrl" target="_blank" rel="nofollow noopener"
|
<a :href="linkedInShareUrl" target="_blank" rel="nofollow noopener"
|
||||||
><b-icon icon="linkedin" size="is-large" type="is-primary"
|
><b-icon icon="linkedin" size="is-large" type="is-primary"
|
||||||
/></a>
|
/></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">
|
<span data-v-5e15e80a="" class="icon has-text-primary is-large">
|
||||||
<DiasporaLogo alt="diaspora-logo" />
|
<DiasporaLogo alt="diaspora-logo" />
|
||||||
</span>
|
</span>
|
||||||
@ -67,7 +72,6 @@
|
|||||||
<a :href="emailShareUrl" target="_blank" rel="nofollow noopener"
|
<a :href="emailShareUrl" target="_blank" rel="nofollow noopener"
|
||||||
><b-icon icon="email" size="is-large" type="is-primary"
|
><b-icon icon="email" size="is-large" type="is-primary"
|
||||||
/></a>
|
/></a>
|
||||||
<!-- TODO: mailto: links are not used anymore, we should provide a popup to redact a message instead -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -76,7 +80,9 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Ref } from "vue-property-decorator";
|
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
|
// @ts-ignore
|
||||||
import DiasporaLogo from "../../assets/diaspora-icon.svg?inline";
|
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 {
|
export default class ShareEventModal extends Vue {
|
||||||
@Prop({ type: Object, required: true }) event!: IEvent;
|
@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;
|
@Ref("eventURLInput") readonly eventURLInput!: any;
|
||||||
|
|
||||||
@ -99,13 +106,15 @@ export default class ShareEventModal extends Vue {
|
|||||||
showCopiedTooltip = false;
|
showCopiedTooltip = false;
|
||||||
|
|
||||||
get twitterShareUrl(): string {
|
get twitterShareUrl(): string {
|
||||||
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(this.event.url)}&text=${
|
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(
|
||||||
this.event.title
|
this.event.url
|
||||||
}`;
|
)}&text=${this.event.title}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get facebookShareUrl(): string {
|
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 {
|
get linkedInShareUrl(): string {
|
||||||
@ -124,7 +133,7 @@ export default class ShareEventModal extends Vue {
|
|||||||
)}&url=${encodeURIComponent(this.event.url)}`;
|
)}&url=${encodeURIComponent(this.event.url)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
copyURL() {
|
copyURL(): void {
|
||||||
this.eventURLInput.$refs.input.select();
|
this.eventURLInput.$refs.input.select();
|
||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
this.showCopiedTooltip = true;
|
this.showCopiedTooltip = true;
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
{{ $t("Add some tags") }}
|
{{ $t("Add some tags") }}
|
||||||
<b-tooltip
|
<b-tooltip
|
||||||
type="is-dark"
|
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-icon size="is-small" icon="help-circle-outline"></b-icon>
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
@ -40,7 +42,6 @@ import { ITag } from "../../types/tag.model";
|
|||||||
if (typeof tag !== "string") {
|
if (typeof tag !== "string") {
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
|
||||||
return { title: tag, slug: tag } as ITag;
|
return { title: tag, slug: tag } as ITag;
|
||||||
});
|
});
|
||||||
this.$emit("input", tagEntities);
|
this.$emit("input", tagEntities);
|
||||||
@ -57,14 +58,14 @@ export default class TagInput extends Vue {
|
|||||||
|
|
||||||
filteredTags: ITag[] = [];
|
filteredTags: ITag[] = [];
|
||||||
|
|
||||||
getFilteredTags(text: string) {
|
getFilteredTags(text: string): void {
|
||||||
this.filteredTags = differenceBy(this.data, this.value, "id").filter(
|
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>
|
</script>
|
||||||
|
@ -1,22 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<footer class="footer" ref="footer">
|
<footer class="footer" ref="footer">
|
||||||
|
|
||||||
<!-- <img :src="`/img/pics/footer_${random}.jpg`" alt="" />-->
|
<!-- <img :src="`/img/pics/footer_${random}.jpg`" alt="" />-->
|
||||||
<a href="https://openstreetmap.fr">
|
<a href="https://openstreetmap.fr">
|
||||||
<h1 class="title is-1">
|
<h1 class="title is-1">OpenStreetMap.fr</h1>
|
||||||
|
|
||||||
OpenStreetMap.fr
|
|
||||||
</h1>
|
|
||||||
</a>
|
</a>
|
||||||
|
<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>
|
<ul>
|
||||||
<li>
|
<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>
|
||||||
<li>
|
<li>
|
||||||
<router-link :to="{ name: RouteName.TERMS }">{{ $t("Terms") }}</router-link>
|
<router-link :to="{ name: RouteName.ABOUT }">{{
|
||||||
|
$t("About")
|
||||||
|
}}</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a hreflang="en" href="https://framagit.org/framasoft/mobilizon/blob/master/LICENSE">
|
<router-link :to="{ name: RouteName.TERMS }">{{
|
||||||
|
$t("Terms")
|
||||||
|
}}</router-link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
hreflang="en"
|
||||||
|
href="https://framagit.org/framasoft/mobilizon/blob/master/LICENSE"
|
||||||
|
>
|
||||||
{{ $t("License") }}
|
{{ $t("License") }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -26,33 +57,57 @@
|
|||||||
tag="span"
|
tag="span"
|
||||||
path="Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}."
|
path="Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}."
|
||||||
>
|
>
|
||||||
<a slot="mobilizon" href="https://joinmobilizon.org">{{ $t("Mobilizon") }}</a>
|
<a slot="mobilizon" href="https://joinmobilizon.org">{{
|
||||||
|
$t("Mobilizon")
|
||||||
|
}}</a>
|
||||||
<span slot="date">{{ new Date().getFullYear() }}</span>
|
<span slot="date">{{ new Date().getFullYear() }}</span>
|
||||||
<a href="https://joinmobilizon.org/hall-of-fame" slot="contributors">{{
|
<a href="https://joinmobilizon.org/hall-of-fame" slot="contributors">{{
|
||||||
$t("more than 1360 contributors")
|
$t("more than 1360 contributors")
|
||||||
}}</a>
|
}}</a>
|
||||||
</i18n>
|
</i18n>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<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 RouteName from "../router/name";
|
||||||
|
import langs from "../i18n/langs.json";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class Footer extends Vue {
|
export default class Footer extends Vue {
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
|
|
||||||
|
locale: string | null = this.$i18n.locale;
|
||||||
|
|
||||||
|
langs: Record<string, string> = langs;
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
get random(): number {
|
get random(): number {
|
||||||
return Math.floor(Math.random() * 4) + 1;
|
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>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "~bulma/sass/utilities/mixins.sass";
|
@import "~bulma/sass/utilities/mixins.sass";
|
||||||
|
|
||||||
|
|
||||||
footer.footer {
|
footer.footer {
|
||||||
color: white;
|
color: white;
|
||||||
background: $primary;
|
background: $primary;
|
||||||
@ -95,5 +150,10 @@ footer.footer {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-color: $secondary;
|
text-decoration-color: $secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep span.select select {
|
||||||
|
background: $background-color;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -17,7 +17,9 @@
|
|||||||
>
|
>
|
||||||
<h3>{{ group.name }}</h3>
|
<h3>{{ group.name }}</h3>
|
||||||
<p class="is-6 has-text-grey">
|
<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>
|
<span v-else>{{ `@${group.preferredUsername}` }}</span>
|
||||||
</p>
|
</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<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 class="card-content">
|
||||||
<div>
|
<div>
|
||||||
<div class="media">
|
<div class="media">
|
||||||
@ -13,7 +19,9 @@
|
|||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.GROUP,
|
name: RouteName.GROUP,
|
||||||
params: { preferredUsername: usernameWithDomain(member.parent) },
|
params: {
|
||||||
|
preferredUsername: usernameWithDomain(member.parent),
|
||||||
|
},
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<h3>{{ member.parent.name }}</h3>
|
<h3>{{ member.parent.name }}</h3>
|
||||||
@ -23,12 +31,16 @@
|
|||||||
}}</span>
|
}}</span>
|
||||||
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
|
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
|
||||||
<b-taglist>
|
<b-taglist>
|
||||||
<b-tag type="is-info" v-if="member.role === MemberRole.ADMINISTRATOR">{{
|
<b-tag
|
||||||
$t("Administrator")
|
type="is-info"
|
||||||
}}</b-tag>
|
v-if="member.role === MemberRole.ADMINISTRATOR"
|
||||||
<b-tag type="is-info" v-else-if="member.role === MemberRole.MODERATOR">{{
|
>{{ $t("Administrator") }}</b-tag
|
||||||
$t("Moderator")
|
>
|
||||||
}}</b-tag>
|
<b-tag
|
||||||
|
type="is-info"
|
||||||
|
v-else-if="member.role === MemberRole.MODERATOR"
|
||||||
|
>{{ $t("Moderator") }}</b-tag
|
||||||
|
>
|
||||||
</b-taglist>
|
</b-taglist>
|
||||||
</p>
|
</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
@ -54,7 +66,9 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
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";
|
import RouteName from "../../router/name";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@ -65,20 +79,34 @@ export default class GroupMemberCard extends Vue {
|
|||||||
|
|
||||||
usernameWithDomain = usernameWithDomain;
|
usernameWithDomain = usernameWithDomain;
|
||||||
|
|
||||||
|
displayNameAndUsername = displayNameAndUsername;
|
||||||
|
|
||||||
MemberRole = MemberRole;
|
MemberRole = MemberRole;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.card-content {
|
.card {
|
||||||
display: flex;
|
.card-content {
|
||||||
align-items: center;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
& > div:first-child {
|
& > div:first-child {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div:last-child {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > div:last-child {
|
.identity-header {
|
||||||
cursor: pointer;
|
background: $yellow-2;
|
||||||
|
display: flex;
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
figure {
|
||||||
|
padding-right: 3px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -8,7 +8,9 @@
|
|||||||
<a
|
<a
|
||||||
class="list-item"
|
class="list-item"
|
||||||
v-for="groupMembership in actualMemberships"
|
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)"
|
@click="changeCurrentGroup(groupMembership.parent)"
|
||||||
:key="groupMembership.id"
|
:key="groupMembership.id"
|
||||||
>
|
>
|
||||||
@ -19,14 +21,25 @@
|
|||||||
:src="groupMembership.parent.avatar.url"
|
:src="groupMembership.parent.avatar.url"
|
||||||
alt=""
|
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">
|
<div class="media-content">
|
||||||
<h3>@{{ groupMembership.parent.name }}</h3>
|
<h3>@{{ groupMembership.parent.name }}</h3>
|
||||||
<small>{{ `@${groupMembership.parent.preferredUsername}` }}</small>
|
<small>{{
|
||||||
|
`@${groupMembership.parent.preferredUsername}`
|
||||||
|
}}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</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>
|
<h3>{{ $t("Unset group") }}</h3>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -36,9 +49,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import { IGroup, IMember, IPerson, Group, MemberRole } from "@/types/actor";
|
import { IGroup, IPerson, Group } from "@/types/actor";
|
||||||
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||||
import { Paginate } from "@/types/paginate";
|
import { Paginate } from "@/types/paginate";
|
||||||
|
import { IMember } from "@/types/actor/member.model";
|
||||||
|
import { MemberRole } from "@/types/enums";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
apollo: {
|
apollo: {
|
||||||
@ -77,9 +92,11 @@ export default class GroupPicker extends Vue {
|
|||||||
get actualMemberships(): IMember[] {
|
get actualMemberships(): IMember[] {
|
||||||
if (this.restrictModeratorLevel) {
|
if (this.restrictModeratorLevel) {
|
||||||
return this.groupMemberships.elements.filter((membership: IMember) =>
|
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;
|
return this.groupMemberships.elements;
|
||||||
|
@ -10,7 +10,11 @@
|
|||||||
{{ $t("The event will show the group as organizer.") }}
|
{{ $t("The event will show the group as organizer.") }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-48x48" v-if="currentGroup.avatar">
|
<figure class="image is-48x48" v-if="currentGroup.avatar">
|
||||||
@ -24,7 +28,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="media-content" v-if="currentGroup.name">
|
<div class="media-content" v-if="currentGroup.name">
|
||||||
<p class="is-4">{{ currentGroup.name }}</p>
|
<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>
|
||||||
<div class="media-content" v-else>
|
<div class="media-content" v-else>
|
||||||
{{ `@${currentGroup.preferredUsername}` }}
|
{{ `@${currentGroup.preferredUsername}` }}
|
||||||
@ -34,7 +40,11 @@
|
|||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
</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
|
<img
|
||||||
class="image is-48x48"
|
class="image is-48x48"
|
||||||
v-if="currentGroup.avatar"
|
v-if="currentGroup.avatar"
|
||||||
@ -44,7 +54,9 @@
|
|||||||
<b-icon v-else size="is-large" icon="account-circle" />
|
<b-icon v-else size="is-large" icon="account-circle" />
|
||||||
</span>
|
</span>
|
||||||
<div v-if="groupMemberships.total === 0" class="box">
|
<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">
|
<p class="is-6 is-size-6 has-text-grey">
|
||||||
{{ $t("You need to create the group before you create an event.") }}
|
{{ $t("You need to create the group before you create an event.") }}
|
||||||
</p>
|
</p>
|
||||||
@ -61,7 +73,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
import { 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 GroupPicker from "./GroupPicker.vue";
|
||||||
import { PERSON_MEMBERSHIPS } from "../../graphql/actor";
|
import { PERSON_MEMBERSHIPS } from "../../graphql/actor";
|
||||||
import { Paginate } from "../../types/paginate";
|
import { Paginate } from "../../types/paginate";
|
||||||
|
@ -26,7 +26,8 @@ export default class GroupSection extends Vue {
|
|||||||
|
|
||||||
@Prop({ required: true, type: String }) icon!: string;
|
@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;
|
@Prop({ required: true, type: Object }) route!: Route;
|
||||||
}
|
}
|
||||||
@ -76,7 +77,8 @@ div.group-section-title {
|
|||||||
::v-deep span {
|
::v-deep span {
|
||||||
display: inline;
|
display: inline;
|
||||||
padding: 3px 8px;
|
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-weight: 500;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -2,7 +2,10 @@
|
|||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<div class="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>
|
<b slot="invitedBy">{{ member.invitedBy.name }}</b>
|
||||||
</i18n>
|
</i18n>
|
||||||
</div>
|
</div>
|
||||||
@ -20,15 +23,21 @@
|
|||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.GROUP,
|
name: RouteName.GROUP,
|
||||||
params: { preferredUsername: usernameWithDomain(member.parent) },
|
params: {
|
||||||
|
preferredUsername: usernameWithDomain(member.parent),
|
||||||
|
},
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<h3>{{ member.parent.name }}</h3>
|
<h3>{{ member.parent.name }}</h3>
|
||||||
<p class="is-6 has-text-grey">
|
<p class="is-6 has-text-grey">
|
||||||
<span v-if="member.parent.domain">
|
<span v-if="member.parent.domain">
|
||||||
{{ `@${member.parent.preferredUsername}@${member.parent.domain}` }}
|
{{
|
||||||
|
`@${member.parent.preferredUsername}@${member.parent.domain}`
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
|
<span v-else>{{
|
||||||
|
`@${member.parent.preferredUsername}`
|
||||||
|
}}</span>
|
||||||
</p>
|
</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
@ -54,7 +63,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
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";
|
import RouteName from "../../router/name";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
@ -11,10 +11,10 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
|
import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
|
||||||
import { IMember } from "@/types/actor";
|
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import InvitationCard from "@/components/Group/InvitationCard.vue";
|
import InvitationCard from "@/components/Group/InvitationCard.vue";
|
||||||
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
|
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
|
||||||
|
import { IMember } from "@/types/actor/member.model";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@ -26,13 +26,15 @@ export default class Invitations extends Vue {
|
|||||||
|
|
||||||
async acceptInvitation(id: string): Promise<void> {
|
async acceptInvitation(id: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>({
|
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>(
|
||||||
mutation: ACCEPT_INVITATION,
|
{
|
||||||
variables: {
|
mutation: ACCEPT_INVITATION,
|
||||||
id,
|
variables: {
|
||||||
},
|
id,
|
||||||
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
|
},
|
||||||
});
|
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
|
||||||
|
}
|
||||||
|
);
|
||||||
if (data) {
|
if (data) {
|
||||||
this.$emit("accept-invitation", data.acceptInvitation);
|
this.$emit("accept-invitation", data.acceptInvitation);
|
||||||
}
|
}
|
||||||
@ -46,13 +48,15 @@ export default class Invitations extends Vue {
|
|||||||
|
|
||||||
async rejectInvitation(id: string): Promise<void> {
|
async rejectInvitation(id: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>({
|
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>(
|
||||||
mutation: REJECT_INVITATION,
|
{
|
||||||
variables: {
|
mutation: REJECT_INVITATION,
|
||||||
id,
|
variables: {
|
||||||
},
|
id,
|
||||||
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
|
},
|
||||||
});
|
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
|
||||||
|
}
|
||||||
|
);
|
||||||
if (data) {
|
if (data) {
|
||||||
this.$emit("reject-invitation", data.rejectInvitation);
|
this.$emit("reject-invitation", data.rejectInvitation);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
</script>
|
||||||
|
@ -1,18 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<MobilizonLogo />
|
<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>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
import MobilizonLogo from "../assets/mobilizon_logo.svg?inline";
|
|
||||||
|
|
||||||
@Component({
|
@Component
|
||||||
components: {
|
|
||||||
MobilizonLogo,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class Logo extends Vue {
|
export default class Logo extends Vue {
|
||||||
@Prop({ type: Boolean, required: false, default: false }) invert!: boolean;
|
@Prop({ type: Boolean, required: false, default: false }) invert!: boolean;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,11 @@
|
|||||||
@click="clickMap"
|
@click="clickMap"
|
||||||
@update:zoom="updateZoom"
|
@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' }" />
|
<v-locatecontrol :options="{ icon: 'mdi mdi-map-marker' }" />
|
||||||
<l-marker
|
<l-marker
|
||||||
:lat-lng="[lat, lon]"
|
:lat-lng="[lat, lon]"
|
||||||
@ -17,7 +21,9 @@
|
|||||||
:draggable="!readOnly"
|
:draggable="!readOnly"
|
||||||
>
|
>
|
||||||
<l-popup v-if="popupMultiLine">
|
<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-popup>
|
||||||
</l-marker>
|
</l-marker>
|
||||||
</l-map>
|
</l-map>
|
||||||
@ -51,12 +57,15 @@ export default class Map extends Vue {
|
|||||||
|
|
||||||
@Prop({ type: String, required: true }) coords!: string;
|
@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 })
|
@Prop({ type: Function, required: false })
|
||||||
updateDraggableMarkerCallback!: Function;
|
updateDraggableMarkerCallback!: (latlng: LatLng, zoom: number) => void;
|
||||||
|
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
zoom: number;
|
zoom: number;
|
||||||
@ -86,45 +95,48 @@ export default class Map extends Vue {
|
|||||||
}
|
}
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
|
||||||
openPopup(event: LeafletEvent) {
|
openPopup(event: LeafletEvent): void {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
event.target.openPopup();
|
event.target.openPopup();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get mergedOptions(): object {
|
get mergedOptions(): Record<string, unknown> {
|
||||||
return { ...this.defaultOptions, ...this.options };
|
return { ...this.defaultOptions, ...this.options };
|
||||||
}
|
}
|
||||||
|
|
||||||
get lat() {
|
get lat(): number {
|
||||||
return this.$props.coords.split(";")[1];
|
return this.$props.coords.split(";")[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
get lon() {
|
get lon(): number {
|
||||||
return this.$props.coords.split(";")[0];
|
return this.$props.coords.split(";")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
get popupMultiLine() {
|
get popupMultiLine(): Array<string> {
|
||||||
if (Array.isArray(this.marker.text)) {
|
if (Array.isArray(this.marker.text)) {
|
||||||
return this.marker.text;
|
return this.marker.text;
|
||||||
}
|
}
|
||||||
return [this.marker.text];
|
return [this.marker.text];
|
||||||
}
|
}
|
||||||
|
|
||||||
clickMap(event: LeafletMouseEvent) {
|
clickMap(event: LeafletMouseEvent): void {
|
||||||
this.updateDraggableMarkerPosition(event.latlng);
|
this.updateDraggableMarkerPosition(event.latlng);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDraggableMarkerPosition(e: LatLng) {
|
updateDraggableMarkerPosition(e: LatLng): void {
|
||||||
this.updateDraggableMarkerCallback(e, this.zoom);
|
this.updateDraggableMarkerCallback(e, this.zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateZoom(zoom: number) {
|
updateZoom(zoom: number): void {
|
||||||
this.zoom = zoom;
|
this.zoom = zoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
get attribution() {
|
get attribution(): string {
|
||||||
return this.config.maps.tiles.attribution || this.$t("© The OpenStreetMap Contributors");
|
return (
|
||||||
|
this.config.maps.tiles.attribution ||
|
||||||
|
(this.$t("© The OpenStreetMap Contributors") as string)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -17,13 +17,16 @@ import { Component, Prop, Vue } from "vue-property-decorator";
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.parentContainer.removeLayer(this);
|
this.parentContainer.removeLayer(this);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class Vue2LeafletLocateControl extends Vue {
|
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;
|
@Prop({ type: Boolean, default: true }) visible = true;
|
||||||
|
|
||||||
@ -33,7 +36,7 @@ export default class Vue2LeafletLocateControl extends Vue {
|
|||||||
|
|
||||||
parentContainer: any;
|
parentContainer: any;
|
||||||
|
|
||||||
mounted() {
|
mounted(): void {
|
||||||
this.mapObject = L.control.locate(this.options);
|
this.mapObject = L.control.locate(this.options);
|
||||||
DomEvent.on(this.mapObject, this.$listeners as any);
|
DomEvent.on(this.mapObject, this.$listeners as any);
|
||||||
propsBinder(this, this.mapObject, this.$props);
|
propsBinder(this, this.mapObject, this.$props);
|
||||||
@ -42,7 +45,7 @@ export default class Vue2LeafletLocateControl extends Vue {
|
|||||||
this.mapObject.addTo(this.parentContainer.mapObject, !this.visible);
|
this.mapObject.addTo(this.parentContainer.mapObject, !this.visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
public locate() {
|
public locate(): void {
|
||||||
this.mapObject.start();
|
this.mapObject.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
<template>
|
<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">
|
<template slot="brand">
|
||||||
<b-navbar-item tag="router-link" :to="{ name: RouteName.HOME }" :aria-label="$t('Home')">
|
<b-navbar-item
|
||||||
|
tag="router-link"
|
||||||
|
:to="{ name: RouteName.HOME }"
|
||||||
|
:aria-label="$t('Home')"
|
||||||
|
>
|
||||||
<logo />
|
<logo />
|
||||||
</b-navbar-item>
|
</b-navbar-item>
|
||||||
</template>
|
</template>
|
||||||
@ -19,9 +27,12 @@
|
|||||||
>{{ $t("My groups") }}
|
>{{ $t("My groups") }}
|
||||||
</b-navbar-item>
|
</b-navbar-item>
|
||||||
<b-navbar-item tag="span" v-if="config && config.features.eventCreation">
|
<b-navbar-item tag="span" v-if="config && config.features.eventCreation">
|
||||||
<b-button tag="router-link" :to="{ name: RouteName.CREATE_EVENT }" type="is-primary"
|
<b-button
|
||||||
>{{ $t("Create") }}
|
tag="router-link"
|
||||||
</b-button>
|
:to="{ name: RouteName.CREATE_EVENT }"
|
||||||
|
type="is-primary"
|
||||||
|
>{{ $t("Create") }}</b-button
|
||||||
|
>
|
||||||
</b-navbar-item>
|
</b-navbar-item>
|
||||||
</template>
|
</template>
|
||||||
<template slot="end">
|
<template slot="end">
|
||||||
@ -30,9 +41,17 @@
|
|||||||
</b-navbar-item>
|
</b-navbar-item>
|
||||||
|
|
||||||
<b-navbar-dropdown v-if="currentActor.id && currentUser.isLoggedIn" right>
|
<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">
|
<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>
|
</figure>
|
||||||
<b-icon v-else icon="account-circle" />
|
<b-icon v-else icon="account-circle" />
|
||||||
</template>
|
</template>
|
||||||
@ -65,9 +84,11 @@
|
|||||||
<hr class="navbar-divider" />
|
<hr class="navbar-divider" />
|
||||||
</b-navbar-item>
|
</b-navbar-item>
|
||||||
|
|
||||||
<b-navbar-item tag="router-link" :to="{ name: RouteName.UPDATE_IDENTITY }"
|
<b-navbar-item
|
||||||
>{{ $t("My account") }}
|
tag="router-link"
|
||||||
</b-navbar-item>
|
:to="{ name: RouteName.UPDATE_IDENTITY }"
|
||||||
|
>{{ $t("My account") }}</b-navbar-item
|
||||||
|
>
|
||||||
|
|
||||||
<!-- <b-navbar-item tag="router-link" :to="{ name: RouteName.CREATE_GROUP }">-->
|
<!-- <b-navbar-item tag="router-link" :to="{ name: RouteName.CREATE_GROUP }">-->
|
||||||
<!-- {{ $t('Create group') }}-->
|
<!-- {{ $t('Create group') }}-->
|
||||||
@ -95,9 +116,11 @@
|
|||||||
<strong>{{ $t("Sign up") }}</strong>
|
<strong>{{ $t("Sign up") }}</strong>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link class="button is-light" :to="{ name: RouteName.LOGIN }"
|
<router-link
|
||||||
>{{ $t("Log in") }}
|
class="button is-light"
|
||||||
</router-link>
|
:to="{ name: RouteName.LOGIN }"
|
||||||
|
>{{ $t("Log in") }}</router-link
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</b-navbar-item>
|
</b-navbar-item>
|
||||||
</template>
|
</template>
|
||||||
@ -109,13 +132,18 @@ import { Component, Vue, Watch } from "vue-property-decorator";
|
|||||||
import Logo from "@/components/Logo.vue";
|
import Logo from "@/components/Logo.vue";
|
||||||
import { GraphQLError } from "graphql";
|
import { GraphQLError } from "graphql";
|
||||||
import { loadLanguageAsync } from "@/utils/i18n";
|
import { loadLanguageAsync } from "@/utils/i18n";
|
||||||
|
import { ICurrentUserRole } from "@/types/enums";
|
||||||
import { CURRENT_USER_CLIENT, USER_SETTINGS } from "../graphql/user";
|
import { CURRENT_USER_CLIENT, USER_SETTINGS } from "../graphql/user";
|
||||||
import { changeIdentity, logout } from "../utils/auth";
|
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 { IPerson, Person } from "../types/actor";
|
||||||
import { CONFIG } from "../graphql/config";
|
import { CONFIG } from "../graphql/config";
|
||||||
import { IConfig } from "../types/config.model";
|
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 SearchField from "./SearchField.vue";
|
||||||
import RouteName from "../router/name";
|
import RouteName from "../router/name";
|
||||||
|
|
||||||
@ -130,7 +158,9 @@ import RouteName from "../router/name";
|
|||||||
identities: {
|
identities: {
|
||||||
query: IDENTITIES,
|
query: IDENTITIES,
|
||||||
update: ({ identities }) =>
|
update: ({ identities }) =>
|
||||||
identities ? identities.map((identity: IPerson) => new Person(identity)) : [],
|
identities
|
||||||
|
? identities.map((identity: IPerson) => new Person(identity))
|
||||||
|
: [],
|
||||||
skip() {
|
skip() {
|
||||||
return this.currentUser.isLoggedIn === false;
|
return this.currentUser.isLoggedIn === false;
|
||||||
},
|
},
|
||||||
@ -201,7 +231,8 @@ export default class NavBar extends Vue {
|
|||||||
async handleErrors(errors: GraphQLError[]): Promise<void> {
|
async handleErrors(errors: GraphQLError[]): Promise<void> {
|
||||||
if (
|
if (
|
||||||
errors.length > 0 &&
|
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();
|
await this.logout();
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="section container">
|
<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-else>
|
||||||
<div v-if="failed">
|
<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(
|
$t(
|
||||||
"Either the participation request has already been validated, either the validation token is incorrect."
|
"Either the participation request has already been validated, either the validation token is incorrect."
|
||||||
@ -12,9 +17,16 @@
|
|||||||
</b-message>
|
</b-message>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<h1 class="title">{{ $t("Your participation request has been validated") }}</h1>
|
<h1 class="title">
|
||||||
<p class="content" v-if="participation.event.joinOptions == EventJoinOptions.RESTRICTED">
|
{{ $t("Your participation request has been validated") }}
|
||||||
{{ $t("Your participation still has to be approved by the organisers.") }}
|
</h1>
|
||||||
|
<p
|
||||||
|
class="content"
|
||||||
|
v-if="participation.event.joinOptions == EventJoinOptions.RESTRICTED"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t("Your participation still has to be approved by the organisers.")
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
<div class="columns has-text-centered">
|
<div class="columns has-text-centered">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
@ -38,9 +50,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import { confirmLocalAnonymousParticipation } from "@/services/AnonymousParticipationStorage";
|
import { confirmLocalAnonymousParticipation } from "@/services/AnonymousParticipationStorage";
|
||||||
|
import { EventJoinOptions } from "@/types/enums";
|
||||||
import { IParticipant } from "../../types/participant.model";
|
import { IParticipant } from "../../types/participant.model";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
import { EventJoinOptions } from "../../types/event.model";
|
|
||||||
import { CONFIRM_PARTICIPATION } from "../../graphql/event";
|
import { CONFIRM_PARTICIPATION } from "../../graphql/event";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
240
js/src/components/Participation/ParticipationSection.vue
Normal 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>
|
@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<redirect-with-account :uri="uri" :pathAfterLogin="`/events/${uuid}`" :sentence="sentence" />
|
<redirect-with-account
|
||||||
|
:uri="uri"
|
||||||
|
:pathAfterLogin="`/events/${uuid}`"
|
||||||
|
:sentence="sentence"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
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>
|
</script>
|
||||||
|
@ -33,7 +33,13 @@
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</p>
|
</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-field :label="$t('Message')">
|
||||||
<b-input
|
<b-input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
@ -54,42 +60,93 @@
|
|||||||
</p>
|
</p>
|
||||||
</b-checkbox>
|
</b-checkbox>
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-button :disabled="sendingForm" type="is-primary" native-type="submit">{{
|
<b-button
|
||||||
$t("Send email")
|
:disabled="sendingForm"
|
||||||
}}</b-button>
|
type="is-primary"
|
||||||
|
native-type="submit"
|
||||||
|
>{{ $t("Send email") }}</b-button
|
||||||
|
>
|
||||||
<div class="has-text-centered">
|
<div class="has-text-centered">
|
||||||
<b-button native-type="button" tag="a" type="is-text" @click="$router.go(-1)">{{
|
<b-button
|
||||||
$t("Back to previous page")
|
native-type="button"
|
||||||
}}</b-button>
|
tag="a"
|
||||||
|
type="is-text"
|
||||||
|
@click="$router.go(-1)"
|
||||||
|
>{{ $t("Back to previous page") }}</b-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<h1 class="title">{{ $t("Request for participation confirmation sent") }}</h1>
|
<h1 class="title">
|
||||||
<p class="content">{{ $t("Check your inbox (and your junk mail folder).") }}</p>
|
{{ $t("Request for participation confirmation sent") }}
|
||||||
<p class="content">{{ $t("You may now close this window.") }}</p>
|
</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>
|
</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>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import { EventModel, IEvent, EventJoinOptions } from "@/types/event.model";
|
import { EventModel, IEvent } from "@/types/event.model";
|
||||||
import { FETCH_EVENT, JOIN_EVENT } from "@/graphql/event";
|
import { FETCH_EVENT_BASIC, JOIN_EVENT } from "@/graphql/event";
|
||||||
import { IConfig } from "@/types/config.model";
|
import { IConfig } from "@/types/config.model";
|
||||||
import { CONFIG } from "@/graphql/config";
|
import { CONFIG } from "@/graphql/config";
|
||||||
import { addLocalUnconfirmedAnonymousParticipation } from "@/services/AnonymousParticipationStorage";
|
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({
|
@Component({
|
||||||
apollo: {
|
apollo: {
|
||||||
event: {
|
event: {
|
||||||
query: FETCH_EVENT,
|
query: FETCH_EVENT_BASIC,
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
uuid: this.uuid,
|
uuid: this.uuid,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
error(e) {
|
||||||
|
this.error = e;
|
||||||
|
},
|
||||||
skip() {
|
skip() {
|
||||||
return !this.uuid;
|
return !this.uuid;
|
||||||
},
|
},
|
||||||
@ -101,7 +158,11 @@ import { IParticipant, ParticipantRole } from "../../types/participant.model";
|
|||||||
export default class ParticipationWithoutAccount extends Vue {
|
export default class ParticipationWithoutAccount extends Vue {
|
||||||
@Prop({ type: String, required: true }) uuid!: string;
|
@Prop({ type: String, required: true }) uuid!: string;
|
||||||
|
|
||||||
anonymousParticipation: { email: string; message: string; saveParticipation: boolean } = {
|
anonymousParticipation: {
|
||||||
|
email: string;
|
||||||
|
message: string;
|
||||||
|
saveParticipation: boolean;
|
||||||
|
} = {
|
||||||
email: "",
|
email: "",
|
||||||
message: "",
|
message: "",
|
||||||
saveParticipation: true,
|
saveParticipation: true,
|
||||||
@ -119,6 +180,8 @@ export default class ParticipationWithoutAccount extends Vue {
|
|||||||
|
|
||||||
EventJoinOptions = EventJoinOptions;
|
EventJoinOptions = EventJoinOptions;
|
||||||
|
|
||||||
|
RouteName = RouteName;
|
||||||
|
|
||||||
async joinEvent(): Promise<void> {
|
async joinEvent(): Promise<void> {
|
||||||
this.error = false;
|
this.error = false;
|
||||||
this.sendingForm = true;
|
this.sendingForm = true;
|
||||||
@ -134,21 +197,27 @@ export default class ParticipationWithoutAccount extends Vue {
|
|||||||
},
|
},
|
||||||
update: (store, { data: updateData }) => {
|
update: (store, { data: updateData }) => {
|
||||||
if (updateData == null) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedData = store.readQuery<{ event: IEvent }>({
|
const cachedData = store.readQuery<{ event: IEvent }>({
|
||||||
query: FETCH_EVENT,
|
query: FETCH_EVENT_BASIC,
|
||||||
variables: { uuid: this.event.uuid },
|
variables: { uuid: this.event.uuid },
|
||||||
});
|
});
|
||||||
if (cachedData == null) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const { event } = cachedData;
|
const { event } = cachedData;
|
||||||
if (event === null) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,16 +227,15 @@ export default class ParticipationWithoutAccount extends Vue {
|
|||||||
event.participantStats.going += 1;
|
event.participantStats.going += 1;
|
||||||
event.participantStats.participant += 1;
|
event.participantStats.participant += 1;
|
||||||
}
|
}
|
||||||
console.log("just before writequery");
|
|
||||||
|
|
||||||
store.writeQuery({
|
store.writeQuery({
|
||||||
query: FETCH_EVENT,
|
query: FETCH_EVENT_BASIC,
|
||||||
variables: { uuid: this.event.uuid },
|
variables: { uuid: this.event.uuid },
|
||||||
data: { event },
|
data: { event },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log("finished with store", data);
|
this.formSent = true;
|
||||||
if (
|
if (
|
||||||
data &&
|
data &&
|
||||||
data.joinEvent.metadata.cancellationToken &&
|
data.joinEvent.metadata.cancellationToken &&
|
||||||
@ -177,13 +245,28 @@ export default class ParticipationWithoutAccount extends Vue {
|
|||||||
this.event,
|
this.event,
|
||||||
data.joinEvent.metadata.cancellationToken
|
data.joinEvent.metadata.cancellationToken
|
||||||
);
|
);
|
||||||
console.log("done with crypto stuff");
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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.sendingForm = false;
|
||||||
this.formSent = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
section.container.section {
|
||||||
|
background: $white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -2,22 +2,34 @@
|
|||||||
<section class="section container hero">
|
<section class="section container hero">
|
||||||
<div class="hero-body" v-if="event">
|
<div class="hero-body" v-if="event">
|
||||||
<div class="container">
|
<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" />
|
<EventListViewCard v-if="event" :event="event" />
|
||||||
<div class="columns has-text-centered">
|
<div class="columns has-text-centered">
|
||||||
<div class="column">
|
<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">
|
<figure class="image is-128x128">
|
||||||
<img src="../../assets/undraw_profile.svg" alt="Profile illustration" />
|
<img
|
||||||
|
src="../../assets/undraw_profile.svg"
|
||||||
|
alt="Profile illustration"
|
||||||
|
/>
|
||||||
</figure>
|
</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>
|
</router-link>
|
||||||
<p>
|
<p>
|
||||||
<small>
|
<small>
|
||||||
{{
|
{{
|
||||||
$t("Either on the {instance} instance or on another instance.", {
|
$t(
|
||||||
instance: host,
|
"Either on the {instance} instance or on another instance.",
|
||||||
})
|
{
|
||||||
|
instance: host,
|
||||||
|
}
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
</small>
|
</small>
|
||||||
<b-tooltip
|
<b-tooltip
|
||||||
@ -32,25 +44,41 @@
|
|||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<vertical-divider :content="$t('Or')" v-if="anonymousParticipationAllowed" />
|
<vertical-divider
|
||||||
|
:content="$t('Or')"
|
||||||
|
v-if="anonymousParticipationAllowed"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
class="column"
|
class="column"
|
||||||
v-if="anonymousParticipationAllowed && hasAnonymousEmailParticipationMethod"
|
v-if="
|
||||||
|
anonymousParticipationAllowed &&
|
||||||
|
hasAnonymousEmailParticipationMethod
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: RouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT }"
|
:to="{ name: RouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT }"
|
||||||
v-if="event.local"
|
v-if="event.local"
|
||||||
>
|
>
|
||||||
<figure class="image is-128x128">
|
<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>
|
</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>
|
</router-link>
|
||||||
<a :href="`${event.url}/participate/without-account`" v-else>
|
<a :href="`${event.url}/participate/without-account`" v-else>
|
||||||
<figure class="image is-128x128">
|
<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>
|
</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>
|
</a>
|
||||||
<p>
|
<p>
|
||||||
<small>{{ $t("Participate using your email address") }}</small>
|
<small>{{ $t("Participate using your email address") }}</small>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<figure class="image" v-if="actualImageSrc">
|
<figure class="image" v-if="imageSrc">
|
||||||
<img :src="actualImageSrc" />
|
<img :src="imageSrc" />
|
||||||
</figure>
|
</figure>
|
||||||
<figure class="image is-128x128" v-else>
|
<figure class="image is-128x128" v-else>
|
||||||
<div class="image-placeholder">
|
<div class="image-placeholder">
|
||||||
@ -9,12 +9,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<b-upload @input="onFileChanged" :accept="accept">
|
<div class="action-buttons">
|
||||||
<a class="button is-primary">
|
<b-field class="file is-primary">
|
||||||
<b-icon icon="upload"></b-icon>
|
<b-upload @input="onFileChanged" :accept="accept" class="file-label">
|
||||||
<span>{{ $t("Click to upload") }}</span>
|
<span class="file-cta">
|
||||||
</a>
|
<b-icon class="file-icon" icon="upload" />
|
||||||
</b-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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -45,18 +52,28 @@ figure.image {
|
|||||||
color: #eee;
|
color: #eee;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { IMedia } from "@/types/media.model";
|
||||||
import { Component, Model, Prop, Vue, Watch } from "vue-property-decorator";
|
import { Component, Model, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class PictureUpload extends Vue {
|
export default class PictureUpload extends Vue {
|
||||||
@Model("change", { type: File }) readonly pictureFile!: File;
|
@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;
|
accept!: string;
|
||||||
|
|
||||||
@Prop({
|
@Prop({
|
||||||
@ -70,10 +87,14 @@ export default class PictureUpload extends Vue {
|
|||||||
})
|
})
|
||||||
textFallback!: string;
|
textFallback!: string;
|
||||||
|
|
||||||
imageSrc: string | null = null;
|
imageSrc: string | null = this.defaultImage ? this.defaultImage.url : null;
|
||||||
|
|
||||||
|
file!: File | null;
|
||||||
|
|
||||||
mounted(): void {
|
mounted(): void {
|
||||||
this.updatePreview(this.pictureFile);
|
if (this.pictureFile) {
|
||||||
|
this.updatePreview(this.pictureFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch("pictureFile")
|
@Watch("pictureFile")
|
||||||
@ -81,13 +102,23 @@ export default class PictureUpload extends Vue {
|
|||||||
this.updatePreview(val);
|
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.$emit("change", file);
|
||||||
|
|
||||||
this.updatePreview(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) {
|
if (file) {
|
||||||
this.imageSrc = URL.createObjectURL(file);
|
this.imageSrc = URL.createObjectURL(file);
|
||||||
return;
|
return;
|
||||||
@ -95,9 +126,5 @@ export default class PictureUpload extends Vue {
|
|||||||
|
|
||||||
this.imageSrc = null;
|
this.imageSrc = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get actualImageSrc(): string | null {
|
|
||||||
return this.imageSrc || this.defaultImageSrc;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -14,26 +14,46 @@
|
|||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="post-minimalist-title">{{ post.title }}</p>
|
<p class="post-minimalist-title">{{ post.title }}</p>
|
||||||
<div class="metadata">
|
<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
|
<small
|
||||||
v-if="post.visibility === PostVisibility.PUBLIC && isCurrentActorMember"
|
v-if="
|
||||||
|
post.visibility === PostVisibility.PUBLIC &&
|
||||||
|
isCurrentActorMember
|
||||||
|
"
|
||||||
class="has-text-grey"
|
class="has-text-grey"
|
||||||
>
|
>
|
||||||
<b-icon icon="earth" size="is-small" />{{ $t("Public") }}</small
|
<b-icon icon="earth" size="is-small" />{{ $t("Public") }}</small
|
||||||
>
|
>
|
||||||
<small v-else-if="post.visibility === PostVisibility.UNLISTED" class="has-text-grey">
|
<small
|
||||||
<b-icon icon="link" size="is-small" />{{ $t("Accessible through link") }}</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" />{{
|
<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
|
||||||
>
|
>
|
||||||
<small class="has-text-grey">{{
|
<small class="has-text-grey">{{
|
||||||
$options.filters.formatDateTimeString(new Date(post.insertedAt), false)
|
$options.filters.formatDateTimeString(
|
||||||
|
new Date(post.insertedAt),
|
||||||
|
false
|
||||||
|
)
|
||||||
}}</small>
|
}}</small>
|
||||||
<small class="has-text-grey" v-if="isCurrentActorMember">{{
|
<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>
|
}}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -43,15 +63,17 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { usernameWithDomain } from "@/types/actor";
|
import { usernameWithDomain } from "@/types/actor";
|
||||||
|
import { PostVisibility } from "@/types/enums";
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
import { IPost, PostVisibility } from "../../types/post.model";
|
import { IPost } from "../../types/post.model";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class PostElementItem extends Vue {
|
export default class PostElementItem extends Vue {
|
||||||
@Prop({ required: true, type: Object }) post!: IPost;
|
@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;
|
RouteName = RouteName;
|
||||||
|
|
||||||
@ -74,7 +96,8 @@ export default class PostElementItem extends Vue {
|
|||||||
|
|
||||||
.post-minimalist-title {
|
.post-minimalist-title {
|
||||||
color: #3c376e;
|
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-size: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -43,7 +43,8 @@ export default class PostListItem extends Vue {
|
|||||||
|
|
||||||
.post-minimalist-title {
|
.post-minimalist-title {
|
||||||
color: #3c376e;
|
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-size: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -22,10 +22,18 @@
|
|||||||
<div class="content columns">
|
<div class="content columns">
|
||||||
<div class="column is-one-quarter-desktop">
|
<div class="column is-one-quarter-desktop">
|
||||||
<span v-if="report.reporter.type === ActorType.APPLICATION">
|
<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>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ $t("Reported by {reporter}", { reporter: report.reporter.preferredUsername }) }}
|
{{
|
||||||
|
$t("Reported by {reporter}", {
|
||||||
|
reporter: report.reporter.preferredUsername,
|
||||||
|
})
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column" v-if="report.content" v-html="report.content" />
|
<div class="column" v-if="report.content" v-html="report.content" />
|
||||||
@ -36,7 +44,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import { IReport } from "@/types/report.model";
|
import { IReport } from "@/types/report.model";
|
||||||
import { ActorType } from "@/types/actor";
|
import { ActorType } from "@/types/enums";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class ReportCard extends Vue {
|
export default class ReportCard extends Vue {
|
||||||
|
@ -4,7 +4,10 @@
|
|||||||
<p class="modal-card-title">{{ title }}</p>
|
<p class="modal-card-title">{{ title }}</p>
|
||||||
</header>
|
</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">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<b-icon icon="alert" type="is-warning" size="is-large" />
|
<b-icon icon="alert" type="is-warning" size="is-large" />
|
||||||
@ -16,7 +19,12 @@
|
|||||||
<figure class="image is-48x48" v-if="comment.actor.avatar">
|
<figure class="image is-48x48" v-if="comment.actor.avatar">
|
||||||
<img :src="comment.actor.avatar.url" alt="Image" />
|
<img :src="comment.actor.avatar.url" alt="Image" />
|
||||||
</figure>
|
</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>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@ -82,7 +90,10 @@ import { IComment } from "../../types/comment.model";
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class ReportModal extends Vue {
|
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;
|
@Prop({ type: String }) title!: string;
|
||||||
|
|
||||||
|
@ -14,7 +14,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<h3>{{ resource.title }}</h3>
|
<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>
|
</div>
|
||||||
<draggable
|
<draggable
|
||||||
v-if="!inline"
|
v-if="!inline"
|
||||||
@ -93,21 +95,27 @@ export default class FolderItem extends Mixins(ResourceMixin) {
|
|||||||
|
|
||||||
async moveResource(resource: IResource): Promise<IResource | undefined> {
|
async moveResource(resource: IResource): Promise<IResource | undefined> {
|
||||||
try {
|
try {
|
||||||
const { data } = await this.$apollo.mutate<{ updateResource: IResource }>({
|
const { data } = await this.$apollo.mutate<{ updateResource: IResource }>(
|
||||||
mutation: UPDATE_RESOURCE,
|
{
|
||||||
variables: {
|
mutation: UPDATE_RESOURCE,
|
||||||
id: resource.id,
|
variables: {
|
||||||
path: `${this.resource.path}/${resource.title}`,
|
id: resource.id,
|
||||||
parentId: this.resource.id,
|
path: `${this.resource.path}/${resource.title}`,
|
||||||
},
|
parentId: this.resource.id,
|
||||||
});
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
console.error("Error while updating resource");
|
console.error("Error while updating resource");
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return data.updateResource;
|
return data.updateResource;
|
||||||
} catch (e) {
|
} 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;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
</b-dropdown>
|
</b-dropdown>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class ResourceDropdown extends Vue {}
|
export default class ResourceDropdown extends Vue {}
|
||||||
|
@ -2,7 +2,12 @@
|
|||||||
<div class="resource-wrapper">
|
<div class="resource-wrapper">
|
||||||
<a :href="resource.resourceUrl" target="_blank">
|
<a :href="resource.resourceUrl" target="_blank">
|
||||||
<div class="preview">
|
<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" />
|
<b-icon :icon="mapServiceTypeToIcon[resource.type]" size="is-large" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -21,7 +26,9 @@
|
|||||||
:src="resource.metadata.faviconUrl"
|
:src="resource.metadata.faviconUrl"
|
||||||
/>
|
/>
|
||||||
<h3>{{ resource.title }}</h3>
|
<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>
|
<span class="host" v-else>{{ urlHostname }}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -2,9 +2,15 @@
|
|||||||
<div v-if="resource">
|
<div v-if="resource">
|
||||||
<article class="panel is-primary">
|
<article class="panel is-primary">
|
||||||
<p class="panel-heading">
|
<p class="panel-heading">
|
||||||
{{ $t('Move "{resourceName}"', { resourceName: initialResource.title }) }}
|
{{
|
||||||
|
$t('Move "{resourceName}"', { resourceName: initialResource.title })
|
||||||
|
}}
|
||||||
</p>
|
</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">
|
<span class="panel-icon">
|
||||||
<b-icon icon="chevron-up" size="is-small" />
|
<b-icon icon="chevron-up" size="is-small" />
|
||||||
</span>
|
</span>
|
||||||
@ -23,12 +29,19 @@
|
|||||||
<a
|
<a
|
||||||
class="panel-block"
|
class="panel-block"
|
||||||
v-for="element in resource.children.elements"
|
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"
|
:key="element.id"
|
||||||
@click="goDown(element)"
|
@click="goDown(element)"
|
||||||
>
|
>
|
||||||
<span class="panel-icon">
|
<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 />
|
<b-icon icon="link" size="is-small" v-else />
|
||||||
</span>
|
</span>
|
||||||
{{ element.title }}
|
{{ element.title }}
|
||||||
@ -44,10 +57,17 @@
|
|||||||
{{ $t("No resources in this folder") }}
|
{{ $t("No resources in this folder") }}
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
<b-button type="is-primary" @click="updateResource" :disabled="moveDisabled">{{
|
<b-button
|
||||||
$t("Move resource to {folder}", { folder: resource.title })
|
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>
|
||||||
<b-button type="is-text" @click="$emit('closeMoveModal')">{{ $t("Cancel") }}</b-button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -81,31 +101,34 @@ export default class ResourceSelector extends Vue {
|
|||||||
|
|
||||||
resource: IResource | undefined = this.initialResource.parent;
|
resource: IResource | undefined = this.initialResource.parent;
|
||||||
|
|
||||||
goDown(element: IResource) {
|
goDown(element: IResource): void {
|
||||||
if (element.type === "folder" && element.id !== this.initialResource.id) {
|
if (element.type === "folder" && element.id !== this.initialResource.id) {
|
||||||
this.resource = element;
|
this.resource = element;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateResource() {
|
updateResource(): void {
|
||||||
this.$emit(
|
this.$emit(
|
||||||
"updateResource",
|
"update-resource",
|
||||||
{
|
{
|
||||||
id: this.initialResource.id,
|
id: this.initialResource.id,
|
||||||
title: this.initialResource.title,
|
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,
|
path: this.initialResource.path,
|
||||||
},
|
},
|
||||||
this.initialResource.parent
|
this.initialResource.parent
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get moveDisabled() {
|
get moveDisabled(): boolean | undefined {
|
||||||
return (
|
return (
|
||||||
(this.initialResource.parent &&
|
(this.initialResource.parent &&
|
||||||
this.resource &&
|
this.resource &&
|
||||||
this.initialResource.parent.path === this.resource.path) ||
|
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 === "/")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,9 +23,9 @@ export default class SearchField extends Vue {
|
|||||||
|
|
||||||
search = "";
|
search = "";
|
||||||
|
|
||||||
enter() {
|
async enter(): Promise<void> {
|
||||||
this.$emit("navbar-search");
|
this.$emit("navbar-search");
|
||||||
this.$router.push({
|
await this.$router.push({
|
||||||
name: RouteName.SEARCH,
|
name: RouteName.SEARCH,
|
||||||
query: { term: this.search },
|
query: { term: this.search },
|
||||||
});
|
});
|
||||||
|
@ -15,16 +15,27 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<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>
|
<strong>{{ $t("Notification on the day of the event") }}</strong>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</b-checkbox>
|
</b-checkbox>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -50,7 +61,11 @@ export default class NotificationsOnboarding extends mixins(Onboarding) {
|
|||||||
try {
|
try {
|
||||||
this.doUpdateSetting(variables);
|
this.doUpdateSetting(variables);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
Snackbar.open({
|
||||||
|
message: e.message,
|
||||||
|
type: "is-danger",
|
||||||
|
position: "is-bottom",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ export default class SettingMenuItem extends Vue {
|
|||||||
|
|
||||||
@Prop({ required: true, type: Object }) to!: Route;
|
@Prop({ required: true, type: Object }) to!: Route;
|
||||||
|
|
||||||
get isActive() {
|
get isActive(): boolean {
|
||||||
if (!this.to) return false;
|
if (!this.to) return false;
|
||||||
if (this.to.name === this.$route.name) {
|
if (this.to.name === this.$route.name) {
|
||||||
if (this.to.params) {
|
if (this.to.params) {
|
||||||
|
@ -20,11 +20,12 @@ export default class SettingMenuSection extends Vue {
|
|||||||
|
|
||||||
@Prop({ required: true, type: Object }) to!: Route;
|
@Prop({ required: true, type: Object }) to!: Route;
|
||||||
|
|
||||||
get sectionActive() {
|
get sectionActive(): boolean {
|
||||||
if (this.$slots.default) {
|
if (this.$slots.default) {
|
||||||
return this.$slots.default.some(
|
return this.$slots.default.some(
|
||||||
({
|
({
|
||||||
componentOptions: {
|
componentOptions: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
propsData: { to },
|
propsData: { to },
|
||||||
},
|
},
|
||||||
|
@ -1,18 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<aside>
|
<aside>
|
||||||
<ul>
|
<ul>
|
||||||
<SettingMenuSection :title="$t('Account')" :to="{ name: RouteName.ACCOUNT_SETTINGS }">
|
<SettingMenuSection
|
||||||
|
:title="$t('Account')"
|
||||||
|
:to="{ name: RouteName.ACCOUNT_SETTINGS }"
|
||||||
|
>
|
||||||
<SettingMenuItem
|
<SettingMenuItem
|
||||||
:title="this.$t('General')"
|
:title="this.$t('General')"
|
||||||
:to="{ name: RouteName.ACCOUNT_SETTINGS_GENERAL }"
|
:to="{ name: RouteName.ACCOUNT_SETTINGS_GENERAL }"
|
||||||
/>
|
/>
|
||||||
<SettingMenuItem :title="$t('Preferences')" :to="{ name: RouteName.PREFERENCES }" />
|
<SettingMenuItem
|
||||||
|
:title="$t('Preferences')"
|
||||||
|
:to="{ name: RouteName.PREFERENCES }"
|
||||||
|
/>
|
||||||
<SettingMenuItem
|
<SettingMenuItem
|
||||||
:title="this.$t('Email notifications')"
|
:title="this.$t('Email notifications')"
|
||||||
:to="{ name: RouteName.NOTIFICATIONS }"
|
:to="{ name: RouteName.NOTIFICATIONS }"
|
||||||
/>
|
/>
|
||||||
</SettingMenuSection>
|
</SettingMenuSection>
|
||||||
<SettingMenuSection :title="$t('Profiles')" :to="{ name: RouteName.IDENTITIES }">
|
<SettingMenuSection
|
||||||
|
:title="$t('Profiles')"
|
||||||
|
:to="{ name: RouteName.IDENTITIES }"
|
||||||
|
>
|
||||||
<SettingMenuItem
|
<SettingMenuItem
|
||||||
v-for="profile in identities"
|
v-for="profile in identities"
|
||||||
:key="profile.preferredUsername"
|
:key="profile.preferredUsername"
|
||||||
@ -22,7 +31,10 @@
|
|||||||
params: { identityName: profile.preferredUsername },
|
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>
|
||||||
<SettingMenuSection
|
<SettingMenuSection
|
||||||
v-if="
|
v-if="
|
||||||
@ -33,35 +45,54 @@
|
|||||||
:title="$t('Moderation')"
|
:title="$t('Moderation')"
|
||||||
:to="{ name: RouteName.MODERATION }"
|
:to="{ name: RouteName.MODERATION }"
|
||||||
>
|
>
|
||||||
<SettingMenuItem :title="$t('Reports')" :to="{ name: RouteName.REPORTS }" />
|
<SettingMenuItem
|
||||||
<SettingMenuItem :title="$t('Moderation log')" :to="{ name: RouteName.REPORT_LOGS }" />
|
: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('Users')" :to="{ name: RouteName.USERS }" />
|
||||||
<SettingMenuItem :title="$t('Profiles')" :to="{ name: RouteName.PROFILES }" />
|
<SettingMenuItem
|
||||||
<SettingMenuItem :title="$t('Groups')" :to="{ name: RouteName.ADMIN_GROUPS }" />
|
:title="$t('Profiles')"
|
||||||
|
:to="{ name: RouteName.PROFILES }"
|
||||||
|
/>
|
||||||
|
<SettingMenuItem
|
||||||
|
:title="$t('Groups')"
|
||||||
|
:to="{ name: RouteName.ADMIN_GROUPS }"
|
||||||
|
/>
|
||||||
</SettingMenuSection>
|
</SettingMenuSection>
|
||||||
<SettingMenuSection
|
<SettingMenuSection
|
||||||
v-if="this.currentUser.role == ICurrentUserRole.ADMINISTRATOR"
|
v-if="this.currentUser.role == ICurrentUserRole.ADMINISTRATOR"
|
||||||
:title="$t('Admin')"
|
:title="$t('Admin')"
|
||||||
:to="{ name: RouteName.ADMIN }"
|
:to="{ name: RouteName.ADMIN }"
|
||||||
>
|
>
|
||||||
<SettingMenuItem :title="$t('Dashboard')" :to="{ name: RouteName.ADMIN_DASHBOARD }" />
|
<SettingMenuItem
|
||||||
|
:title="$t('Dashboard')"
|
||||||
|
:to="{ name: RouteName.ADMIN_DASHBOARD }"
|
||||||
|
/>
|
||||||
<SettingMenuItem
|
<SettingMenuItem
|
||||||
:title="$t('Instance settings')"
|
:title="$t('Instance settings')"
|
||||||
:to="{ name: RouteName.ADMIN_SETTINGS }"
|
:to="{ name: RouteName.ADMIN_SETTINGS }"
|
||||||
/>
|
/>
|
||||||
<SettingMenuItem :title="$t('Federation')" :to="{ name: RouteName.RELAYS }" />
|
<SettingMenuItem
|
||||||
|
:title="$t('Federation')"
|
||||||
|
:to="{ name: RouteName.RELAYS }"
|
||||||
|
/>
|
||||||
</SettingMenuSection>
|
</SettingMenuSection>
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<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 SettingMenuSection from "./SettingMenuSection.vue";
|
||||||
import SettingMenuItem from "./SettingMenuItem.vue";
|
import SettingMenuItem from "./SettingMenuItem.vue";
|
||||||
import { IDENTITIES } from "../../graphql/actor";
|
import { IDENTITIES } from "../../graphql/actor";
|
||||||
import { IPerson, Person } from "../../types/actor";
|
import { IPerson, Person } from "../../types/actor";
|
||||||
import { CURRENT_USER_CLIENT } from "../../graphql/user";
|
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";
|
import RouteName from "../../router/name";
|
||||||
|
|
||||||
@ -70,7 +101,8 @@ import RouteName from "../../router/name";
|
|||||||
apollo: {
|
apollo: {
|
||||||
identities: {
|
identities: {
|
||||||
query: 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,
|
currentUser: CURRENT_USER_CLIENT,
|
||||||
},
|
},
|
||||||
|
@ -39,7 +39,10 @@
|
|||||||
timezone,
|
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 }) }}
|
{{ $t("Your timezone {timezone} isn't supported.", { timezone }) }}
|
||||||
</b-message>
|
</b-message>
|
||||||
</p>
|
</p>
|
||||||
|
@ -2,9 +2,10 @@
|
|||||||
<div class="card" v-if="todo">
|
<div class="card" v-if="todo">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<b-checkbox v-model="status" />
|
<b-checkbox v-model="status" />
|
||||||
<router-link :to="{ name: RouteName.TODO, params: { todoId: todo.id } }">{{
|
<router-link
|
||||||
todo.title
|
:to="{ name: RouteName.TODO, params: { todoId: todo.id } }"
|
||||||
}}</router-link>
|
>{{ todo.title }}</router-link
|
||||||
|
>
|
||||||
<span class="details has-text-grey">
|
<span class="details has-text-grey">
|
||||||
<span v-if="todo.dueDate" class="due_date">
|
<span v-if="todo.dueDate" class="due_date">
|
||||||
<b-icon icon="calendar" />
|
<b-icon icon="calendar" />
|
||||||
@ -13,7 +14,9 @@
|
|||||||
<span v-if="todo.assignedTo" class="assigned_to">
|
<span v-if="todo.assignedTo" class="assigned_to">
|
||||||
<b-icon icon="account" />
|
<b-icon icon="account" />
|
||||||
{{ `@${todo.assignedTo.preferredUsername}` }}
|
{{ `@${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>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -53,7 +56,11 @@ export default class Todo extends Vue {
|
|||||||
});
|
});
|
||||||
this.editMode = false;
|
this.editMode = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
Snackbar.open({
|
||||||
|
message: e.message,
|
||||||
|
type: "is-danger",
|
||||||
|
position: "is-bottom",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import { debounce } from "lodash";
|
import { debounce, DebouncedFunc } from "lodash";
|
||||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||||
import { ITodo } from "../../types/todos";
|
import { ITodo } from "../../types/todos";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
@ -36,7 +36,9 @@ export default class Todo extends Vue {
|
|||||||
|
|
||||||
editMode = false;
|
editMode = false;
|
||||||
|
|
||||||
debounceUpdateTodo!: Function;
|
debounceUpdateTodo!: DebouncedFunc<
|
||||||
|
(obj: Record<string, unknown>) => Promise<void>
|
||||||
|
>;
|
||||||
|
|
||||||
// We put this in data because of issues like
|
// We put this in data because of issues like
|
||||||
// https://github.com/vuejs/vue-class-component/issues/263
|
// https://github.com/vuejs/vue-class-component/issues/263
|
||||||
@ -89,7 +91,11 @@ export default class Todo extends Vue {
|
|||||||
});
|
});
|
||||||
this.editMode = false;
|
this.editMode = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
Snackbar.open({
|
||||||
|
message: e.message,
|
||||||
|
type: "is-danger",
|
||||||
|
position: "is-bottom",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,11 @@
|
|||||||
<b-icon :icon="oauthProvider.id" />
|
<b-icon :icon="oauthProvider.id" />
|
||||||
<span>{{ SELECTED_PROVIDERS[oauthProvider.id] }}</span></a
|
<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" />
|
<b-icon icon="lock" />
|
||||||
<span>{{ oauthProvider.label }}</span>
|
<span>{{ oauthProvider.label }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -20,7 +20,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<vertical-divider :content="$t('Or')" />
|
<vertical-divider :content="$t('Or')" />
|
||||||
<div class="column">
|
<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>{{ $t("Other software may also support this.") }}</p>
|
||||||
<p>{{ sentence }}</p>
|
<p>{{ sentence }}</p>
|
||||||
<form @submit.prevent="redirectToInstance">
|
<form @submit.prevent="redirectToInstance">
|
||||||
@ -34,7 +36,9 @@
|
|||||||
:placeholder="$t('profile@instance')"
|
:placeholder="$t('profile@instance')"
|
||||||
></b-input>
|
></b-input>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button class="button is-primary" type="submit">{{ $t("Go") }}</button>
|
<button class="button is-primary" type="submit">
|
||||||
|
{{ $t("Go") }}
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</b-field>
|
</b-field>
|
||||||
</b-field>
|
</b-field>
|
||||||
@ -54,7 +58,7 @@
|
|||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import VerticalDivider from "@/components/Utils/VerticalDivider.vue";
|
import VerticalDivider from "@/components/Utils/VerticalDivider.vue";
|
||||||
import Subtitle from "@/components/Utils/Subtitle.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";
|
import RouteName from "../../router/name";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -80,14 +84,22 @@ export default class RedirectWithAccount extends Vue {
|
|||||||
|
|
||||||
async redirectToInstance(): Promise<void> {
|
async redirectToInstance(): Promise<void> {
|
||||||
const [, host] = this.remoteActorAddress.split("@", 2);
|
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);
|
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 scheme = process.env.NODE_ENV === "production" ? "https" : "http";
|
||||||
const data = await (
|
const data = await (
|
||||||
await fetch(`${scheme}://${hostname}/.well-known/webfinger?resource=acct:${identity}`)
|
await fetch(
|
||||||
|
`${scheme}://${hostname}/.well-known/webfinger?resource=acct:${identity}`
|
||||||
|
)
|
||||||
).json();
|
).json();
|
||||||
if (data && Array.isArray(data.links)) {
|
if (data && Array.isArray(data.links)) {
|
||||||
const link: { template: string } = data.links.find(
|
const link: { template: string } = data.links.find(
|
||||||
|
@ -21,7 +21,8 @@ h2 {
|
|||||||
display: inline;
|
display: inline;
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
color: #3a384c;
|
color: #3a384c;
|
||||||
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
|
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
|
||||||
|
serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,10 @@ function formatDateString(value: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatTimeString(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 {
|
function formatDateTimeString(value: string, showTime = true): string {
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import nl2br from "@/filters/utils";
|
import nl2br from "@/filters/utils";
|
||||||
import { formatDateString, formatTimeString, formatDateTimeString } from "./datetime";
|
import {
|
||||||
|
formatDateString,
|
||||||
|
formatTimeString,
|
||||||
|
formatDateTimeString,
|
||||||
|
} from "./datetime";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
install(vue: any): void {
|
install(vue: any): void {
|
||||||
vue.filter("formatDateString", formatDateString);
|
vue.filter("formatDateString", formatDateString);
|
||||||
vue.filter("formatTimeString", formatTimeString);
|
vue.filter("formatTimeString", formatTimeString);
|
||||||
|
@ -10,6 +10,7 @@ export const FETCH_PERSON = gql`
|
|||||||
summary
|
summary
|
||||||
preferredUsername
|
preferredUsername
|
||||||
suspended
|
suspended
|
||||||
|
mediaSize
|
||||||
avatar {
|
avatar {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@ -51,6 +52,7 @@ export const GET_PERSON = gql`
|
|||||||
summary
|
summary
|
||||||
preferredUsername
|
preferredUsername
|
||||||
suspended
|
suspended
|
||||||
|
mediaSize
|
||||||
avatar {
|
avatar {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@ -63,7 +65,10 @@ export const GET_PERSON = gql`
|
|||||||
feedTokens {
|
feedTokens {
|
||||||
token
|
token
|
||||||
}
|
}
|
||||||
organizedEvents(page: $organizedEventsPage, limit: $organizedEventsLimit) {
|
organizedEvents(
|
||||||
|
page: $organizedEventsPage
|
||||||
|
limit: $organizedEventsLimit
|
||||||
|
) {
|
||||||
total
|
total
|
||||||
elements {
|
elements {
|
||||||
id
|
id
|
||||||
@ -206,6 +211,18 @@ export const LOGGED_USER_PARTICIPATIONS = gql`
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
attributedTo {
|
||||||
|
avatar {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
}
|
||||||
|
preferredUsername
|
||||||
|
name
|
||||||
|
summary
|
||||||
|
domain
|
||||||
|
url
|
||||||
|
id
|
||||||
|
}
|
||||||
participantStats {
|
participantStats {
|
||||||
going
|
going
|
||||||
notApproved
|
notApproved
|
||||||
@ -215,6 +232,11 @@ export const LOGGED_USER_PARTICIPATIONS = gql`
|
|||||||
maximumAttendeeCapacity
|
maximumAttendeeCapacity
|
||||||
remainingAttendeeCapacity
|
remainingAttendeeCapacity
|
||||||
}
|
}
|
||||||
|
tags {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
title
|
||||||
|
}
|
||||||
}
|
}
|
||||||
id
|
id
|
||||||
role
|
role
|
||||||
@ -282,6 +304,16 @@ export const LOGGED_USER_MEMBERSHIPS = gql`
|
|||||||
elements {
|
elements {
|
||||||
id
|
id
|
||||||
role
|
role
|
||||||
|
actor {
|
||||||
|
id
|
||||||
|
avatar {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
}
|
||||||
|
preferredUsername
|
||||||
|
name
|
||||||
|
domain
|
||||||
|
}
|
||||||
parent {
|
parent {
|
||||||
id
|
id
|
||||||
preferredUsername
|
preferredUsername
|
||||||
@ -419,7 +451,7 @@ export const CREATE_PERSON = gql`
|
|||||||
$preferredUsername: String!
|
$preferredUsername: String!
|
||||||
$name: String!
|
$name: String!
|
||||||
$summary: String
|
$summary: String
|
||||||
$avatar: PictureInput
|
$avatar: MediaInput
|
||||||
) {
|
) {
|
||||||
createPerson(
|
createPerson(
|
||||||
preferredUsername: $preferredUsername
|
preferredUsername: $preferredUsername
|
||||||
@ -440,7 +472,12 @@ export const CREATE_PERSON = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const UPDATE_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) {
|
updatePerson(id: $id, name: $name, summary: $summary, avatar: $avatar) {
|
||||||
id
|
id
|
||||||
preferredUsername
|
preferredUsername
|
||||||
@ -467,7 +504,12 @@ export const DELETE_PERSON = gql`
|
|||||||
* Prefer CREATE_PERSON when creating another identity
|
* Prefer CREATE_PERSON when creating another identity
|
||||||
*/
|
*/
|
||||||
export const REGISTER_PERSON = gql`
|
export const REGISTER_PERSON = gql`
|
||||||
mutation($preferredUsername: String!, $name: String!, $summary: String!, $email: String!) {
|
mutation(
|
||||||
|
$preferredUsername: String!
|
||||||
|
$name: String!
|
||||||
|
$summary: String!
|
||||||
|
$email: String!
|
||||||
|
) {
|
||||||
registerPerson(
|
registerPerson(
|
||||||
preferredUsername: $preferredUsername
|
preferredUsername: $preferredUsername
|
||||||
name: $name
|
name: $name
|
||||||
|