merge with version 1.0.3 for osm
@ -39,9 +39,9 @@ lint:
|
||||
- mix format --check-formatted --dry-run || export EXITVALUE=1
|
||||
- cd js
|
||||
- yarn install
|
||||
#- yarn run lint || export EXITVALUE=1
|
||||
- yarn run prettier --ignore-path="src/i18n/*" -c . || export EXITVALUE=1
|
||||
- yarn run build
|
||||
- yarn run lint || export EXITVALUE=1
|
||||
- yarn run prettier -c . || export EXITVALUE=1
|
||||
- yarn run build:assets
|
||||
- cd ../
|
||||
- exit $EXITVALUE
|
||||
artifacts:
|
||||
@ -69,7 +69,7 @@ exunit:
|
||||
before_script:
|
||||
- cd js
|
||||
- yarn install
|
||||
- yarn run build
|
||||
- yarn run build:assets
|
||||
- cd ../
|
||||
- mix deps.get
|
||||
- MIX_ENV=test mix ecto.create
|
||||
@ -78,6 +78,21 @@ exunit:
|
||||
- lint
|
||||
script:
|
||||
- mix coveralls
|
||||
|
||||
jest:
|
||||
stage: test
|
||||
before_script:
|
||||
- cd js
|
||||
- yarn install
|
||||
dependencies:
|
||||
- lint
|
||||
script:
|
||||
- yarn run test:unit --no-color
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- js/coverage
|
||||
expire_in: 30 days
|
||||
# cypress:
|
||||
# stage: test
|
||||
# services:
|
||||
|
@ -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
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
## 1.0.3 - 18-12-2020
|
||||
|
||||
**This release adds new migrations, be sure to run them before restarting Mobilizon**
|
||||
|
||||
**This release has repair steps, be sure to execute them right after restarting Mobilizon**
|
||||
|
||||
### Special operations
|
||||
|
||||
- **Reattach media files to their entity.**
|
||||
When media files were uploaded and added in events and posts bodies, they were only attached to the profile that uploaded them, not to the event or post. This task attaches them back to their entity so that the command to clean orphan media files doesn't remove them.
|
||||
|
||||
- Source install
|
||||
`MIX_ENV=prod mix mobilizon.maintenance.fix_unattached_media_in_body`
|
||||
- Docker
|
||||
`docker-compose exec mobilizon mobilizon_ctl maintenance.fix_unattached_media_in_body`
|
||||
|
||||
- **Refresh remote profiles to save avatars locally**
|
||||
Profile avatars and banners were previously only proxified and cached. Now we save them locally. Refreshing all remote actors will save profile media locally instead.
|
||||
|
||||
- Source install
|
||||
`MIX_ENV=prod mix mobilizon.actors.refresh --all`
|
||||
- Docker
|
||||
`docker-compose exec mobilizon mobilizon_ctl actors.refresh --all`
|
||||
|
||||
- **imagemagick and webp are now a required dependency** to build Mobilizon.
|
||||
Optimized versions of Mobilizon's pictures are now produced during front-end build.
|
||||
See [the documentation](https://docs.joinmobilizon.org/administration/dependencies/#misc) to make sure these dependencies are installed.
|
||||
|
||||
### Added
|
||||
|
||||
- **Add a command to clean orphan media files**. There's a `--dry-run` option to see what files would have been deleted.
|
||||
**Make sure all media files have been reattached properly (see above) before running this command.**
|
||||
In 1.1.0 a scheduled job will be enabled to clear orphan media files automatically after a while.
|
||||
- Added user and actors media usage information in administration
|
||||
- Added a scheduled job to clean unconfirmed users (and their eventual initial profile) after a 48 hour grace period
|
||||
- Added a mix task to manually clean unconfirmed users
|
||||
- Added OpenStreetMap (OSRM) or GoogleMaps routing pages on the event map modal
|
||||
- Added PWA support, Mobilizon can now be installed on Android (Firefox and Chrome), iOS (Safari) and desktop (Chrome)
|
||||
- Added possibility to pick language through a setting on the footer for unlogged users
|
||||
|
||||
### Changed
|
||||
|
||||
- Save remote avatars and banners instead of proxifying them
|
||||
- Forbid creating usernames with uppercase characters
|
||||
- Allow LDAP admin to use a fully qualified DN (different than the one for the users)
|
||||
- Allow LDAP users to be filtered by LDAP attribute `memberOf`.
|
||||
- Improve the "My events" and "My groups" page when there's nothing here yet
|
||||
- Show identity concerned when listing event participations (in "My events") and group membership (in "My groups")
|
||||
- The datetime picker on the event's edition page has been changed and allows directly editing the text
|
||||
- Allow to clear and remove pictures from events and posts
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed inline media that weren't being tracked, so that they are not considered orphans media files.
|
||||
- Fixed permissions on the Docker volume
|
||||
- Fixed emails not using user timezone
|
||||
- Fixed draft status not being shown on group events & posts inside admin section
|
||||
- Fixed cancelled status not being shown on cancelled events cards
|
||||
- Fixed membership notification emails not being sent with the user's language
|
||||
- Fixed group posts ActivityPub endpoint
|
||||
- Fixed unlisted groups being available in search
|
||||
- Fixed inline media pictures being unattached when editing an event or a post
|
||||
- Fixed adding an instance to follow with spaces
|
||||
- Fixed past groups showing up on group's page
|
||||
- Fixed error message not showing up when you are already an anonymous participant for an event
|
||||
- Fixed error message not showing up when you pick an username already in user for a new profile or a group
|
||||
- Fixed translations not fallbacking properly to english when not found
|
||||
-
|
||||
|
||||
### Security
|
||||
|
||||
- Stop logging user JWT tokens in Websocket Mobilizon logs
|
||||
|
||||
### Translations
|
||||
|
||||
Updated translations:
|
||||
|
||||
- Catalan
|
||||
- Dutch
|
||||
- English
|
||||
- Finnish
|
||||
- French
|
||||
- Galician
|
||||
- German
|
||||
- Hungarian
|
||||
- Italian
|
||||
- Norwegian
|
||||
- Occitan
|
||||
- Polish
|
||||
- Spanish
|
||||
- Swedish
|
||||
|
||||
## 1.0.2 - 2020-11-15
|
||||
|
||||
@ -163,20 +254,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### 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:
|
||||
```
|
||||
MIX_ENV=prod mix deps.clean mime --build
|
||||
```
|
||||
- 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:
|
||||
|
||||
* 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
|
||||
|
||||
- Possibility to login using LDAP
|
||||
- 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)
|
||||
- resources (collections of links, with folders, accessible to your group members)
|
||||
- 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
|
||||
|
||||
- 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
|
||||
|
||||
### Special operations
|
||||
|
||||
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).
|
||||
@ -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).
|
||||
|
||||
### Added
|
||||
|
||||
- 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 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
|
||||
|
||||
### Changed
|
||||
|
||||
- Configuration handling (see above)
|
||||
- Improved a bit color theme
|
||||
- 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
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed URL search
|
||||
- Fixed content accessed through URL search being public
|
||||
- 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
|
||||
|
||||
### Special operations
|
||||
|
||||
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.
|
||||
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):
|
||||
* `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):
|
||||
* `mix mobilizon.move_participant_stats`
|
||||
|
||||
- `mix mobilizon.move_participant_stats`
|
||||
|
||||
### Added
|
||||
|
||||
- Federation is active
|
||||
- Added an interface for admins to view and manage instance followers and followings
|
||||
- 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
|
||||
|
||||
### Changed
|
||||
|
||||
- Move participant stats to event table **(read special instructions above)**
|
||||
- Limit length (20 characters) and number (10) of tags allowed
|
||||
- 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
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix event URL validation and check if hostname is correct before showing it
|
||||
- Fix participations stats on the MyEvents page
|
||||
- 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
|
||||
|
||||
### Security
|
||||
|
||||
- Sanitize event title to avoid XSS
|
||||
|
||||
## [1.0.0-beta.1] - 2019-10-15
|
||||
|
||||
### Added
|
||||
|
||||
- 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.
|
||||
|
||||
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
|
||||
|
||||
@ -33,7 +33,7 @@ You will have the power to create multiple identities from the same account, lik
|
||||
|
||||
### 📅 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.
|
||||
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
|
||||
|
||||
### Learn more
|
||||
* 🌐 Official website: [https://joinmobilizon.org](https://joinmobilizon.org)
|
||||
* 🔢 Pick an instance [https://mobilizon.org](https://mobilizon.org)
|
||||
* 💻 Source: [https://framagit.org/framasoft/mobilizon](https://framagit.org/framasoft/mobilizon)
|
||||
* 📜 Documentation [https://docs.joinmobilizon.org](https://docs.joinmobilizon.org)
|
||||
|
||||
|
||||
- 🌐 Official website: [https://joinmobilizon.org](https://joinmobilizon.org)
|
||||
- 🔢 Pick an instance [https://mobilizon.org](https://mobilizon.org)
|
||||
- 💻 Source: [https://framagit.org/framasoft/mobilizon](https://framagit.org/framasoft/mobilizon)
|
||||
- 📜 Documentation [https://docs.joinmobilizon.org](https://docs.joinmobilizon.org)
|
||||
|
||||
### Discuss
|
||||
* 💬 Riot/Matrix: [https://riot.im/app/#/room/#Mobilizon:matrix.org](https://riot.im/app/#/room/#Mobilizon:matrix.org)
|
||||
* 🗣️ 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
|
||||
* 🐘 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`.
|
||||
|
||||
|
||||
## ❤️ 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).
|
||||
|
32
SECURITY.md
@ -5,15 +5,15 @@ Framasoft, the Mobilizon maintainer team and community take all security bugs in
|
||||
|
||||
### 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.
|
||||
|
||||
@ -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:
|
||||
|
||||
* 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.
|
||||
|
||||
@ -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:
|
||||
|
||||
* 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
|
||||
* 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;
|
||||
* 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.
|
||||
* 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
|
||||
- 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
|
||||
- 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;
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
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,
|
||||
avatar_upload_limit: 2_000_000,
|
||||
banner_upload_limit: 4_000_000,
|
||||
remove_orphan_uploads: true,
|
||||
orphan_upload_grace_period_hours: 48,
|
||||
remove_unconfirmed_users: true,
|
||||
unconfirmed_user_grace_period_hours: 48,
|
||||
email_from: "noreply@localhost",
|
||||
email_reply_to: "noreply@localhost"
|
||||
|
||||
@ -77,17 +81,6 @@ config :mobilizon, Mobilizon.Web.Upload,
|
||||
|
||||
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, uploads: "uploads"
|
||||
|
||||
config :mobilizon, :media_proxy,
|
||||
enabled: true,
|
||||
proxy_opts: [
|
||||
redirect_on_failure: false,
|
||||
max_body_length: 25 * 1_048_576,
|
||||
http: [
|
||||
follow_redirect: true,
|
||||
pool: :media
|
||||
]
|
||||
]
|
||||
|
||||
config :mobilizon, Mobilizon.Web.Email.Mailer,
|
||||
adapter: Bamboo.SMTPAdapter,
|
||||
server: "localhost",
|
||||
@ -142,6 +135,10 @@ config :mobilizon, :ldap,
|
||||
base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
|
||||
uid: System.get_env("LDAP_UID") || "cn",
|
||||
require_bind_for_search: !(System.get_env("LDAP_REQUIRE_BIND_FOR_SEARCH") == "false"),
|
||||
# The full CN to filter by `memberOf`, or `false` if disabled
|
||||
group: false,
|
||||
# Either the admin UID matching the field in `uid`,
|
||||
# Either a tuple with the fully qualified DN: {:full, uid=admin,dc=example.com,dc=local}
|
||||
bind_uid: System.get_env("LDAP_BIND_UID"),
|
||||
bind_password: System.get_env("LDAP_BIND_PASSWORD")
|
||||
|
||||
@ -154,22 +151,20 @@ config :geolix,
|
||||
}
|
||||
]
|
||||
|
||||
config :auto_linker,
|
||||
opts: [
|
||||
scheme: true,
|
||||
extra: true,
|
||||
# TODO: Set to :no_scheme when it works properly
|
||||
validate_tld: true,
|
||||
class: false,
|
||||
strip_prefix: false,
|
||||
new_window: true,
|
||||
rel: "noopener noreferrer ugc"
|
||||
]
|
||||
config :mobilizon, Mobilizon.Service.Formatter,
|
||||
class: false,
|
||||
rel: "noopener noreferrer ugc",
|
||||
new_window: true,
|
||||
truncate: false,
|
||||
strip_prefix: false,
|
||||
extra: true,
|
||||
validate_tld: :no_scheme
|
||||
|
||||
config :tesla, adapter: Tesla.Adapter.Hackney
|
||||
|
||||
config :phoenix, :format_encoders, json: Jason, "activity-json": Jason
|
||||
config :phoenix, :json_library, Jason
|
||||
config :phoenix, :filter_parameters, ["password", "token"]
|
||||
|
||||
config :ex_cldr,
|
||||
default_locale: "en",
|
||||
@ -180,26 +175,8 @@ config :http_signatures,
|
||||
|
||||
config :mobilizon, :cldr,
|
||||
locales: [
|
||||
"ar",
|
||||
"be",
|
||||
"ca",
|
||||
"cs",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fi",
|
||||
"fr",
|
||||
"gl",
|
||||
"hu",
|
||||
"it",
|
||||
"ja",
|
||||
"nl",
|
||||
"nn",
|
||||
"oc",
|
||||
"pl",
|
||||
"pt",
|
||||
"ru",
|
||||
"sv"
|
||||
"en"
|
||||
]
|
||||
|
||||
config :mobilizon, :activitypub,
|
||||
@ -233,6 +210,9 @@ config :mobilizon, :maps,
|
||||
tiles: [
|
||||
endpoint: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
attribution: "© The OpenStreetMap Contributors"
|
||||
],
|
||||
routing: [
|
||||
type: :openstreetmap
|
||||
]
|
||||
|
||||
config :mobilizon, :anonymous,
|
||||
@ -266,7 +246,10 @@ config :mobilizon, Oban,
|
||||
queues: [default: 10, search: 5, mailers: 10, background: 5],
|
||||
crontab: [
|
||||
{"@hourly", Mobilizon.Service.Workers.BuildSiteMap, queue: :background},
|
||||
{"17 * * * *", Mobilizon.Service.Workers.RefreshGroups, queue: :background}
|
||||
{"17 * * * *", Mobilizon.Service.Workers.RefreshGroups, queue: :background},
|
||||
# To be activated in Mobilizon 1.2
|
||||
# {"@hourly", Mobilizon.Service.Workers.CleanOrphanMediaWorker, queue: :background},
|
||||
{"@hourly", Mobilizon.Service.Workers.CleanUnconfirmedUsersWorker, queue: :background}
|
||||
]
|
||||
|
||||
config :mobilizon, :rich_media,
|
||||
|
@ -19,7 +19,6 @@ config :mobilizon, Mobilizon.Web.Endpoint,
|
||||
code_reloader: true,
|
||||
check_origin: false,
|
||||
watchers: [
|
||||
# yarn: ["run", "dev", cd: Path.expand("../js", __DIR__)]
|
||||
node: [
|
||||
"node_modules/webpack/bin/webpack.js",
|
||||
"--mode",
|
||||
@ -53,8 +52,8 @@ config :mobilizon, Mobilizon.Web.Endpoint,
|
||||
patterns: [
|
||||
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
|
||||
~r{priv/gettext/.*(po)$},
|
||||
~r{lib/mobilizon_web/views/.*(ex)$},
|
||||
~r{lib/mobilizon_web/templates/.*(eex)$}
|
||||
~r{lib/web/(live|views)/.*(ex)$},
|
||||
~r{lib/web/templates/.*(eex)$}
|
||||
]
|
||||
]
|
||||
|
||||
@ -92,13 +91,6 @@ config :mobilizon, :instance,
|
||||
|
||||
# config :mobilizon, :activitypub, sign_object_fetches: false
|
||||
|
||||
# No need to compile every locale in development environment
|
||||
config :mobilizon, :cldr,
|
||||
locales: [
|
||||
"fr",
|
||||
"en"
|
||||
]
|
||||
|
||||
config :mobilizon, :anonymous,
|
||||
reports: [
|
||||
allowed: true
|
||||
|
@ -13,6 +13,31 @@ config :mobilizon, Mobilizon.Web.Endpoint,
|
||||
# Do not print debug messages in production
|
||||
config :logger, level: :info
|
||||
|
||||
# Load all locales in production
|
||||
config :mobilizon, :cldr,
|
||||
locales: [
|
||||
"ar",
|
||||
"be",
|
||||
"ca",
|
||||
"cs",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fi",
|
||||
"fr",
|
||||
"gl",
|
||||
"hu",
|
||||
"it",
|
||||
"ja",
|
||||
"nl",
|
||||
"nn",
|
||||
"oc",
|
||||
"pl",
|
||||
"pt",
|
||||
"ru",
|
||||
"sv"
|
||||
]
|
||||
|
||||
cond do
|
||||
System.get_env("INSTANCE_CONFIG") &&
|
||||
File.exists?("./config/#{System.get_env("INSTANCE_CONFIG")}") ->
|
||||
|
@ -1,4 +1,4 @@
|
||||
version: '3'
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
@ -1,4 +1,4 @@
|
||||
version: '3'
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
@ -15,7 +15,7 @@ services:
|
||||
restart: unless-stopped
|
||||
build: .
|
||||
volumes:
|
||||
- '.:/app'
|
||||
- ".:/app"
|
||||
ports:
|
||||
- "4000:4000"
|
||||
depends_on:
|
||||
|
@ -1,7 +1,7 @@
|
||||
# First build the application assets
|
||||
FROM node:alpine as assets
|
||||
|
||||
RUN apk add --no-cache python build-base
|
||||
RUN apk add --no-cache python build-base libwebp-tools bash imagemagick ncurses
|
||||
|
||||
COPY js .
|
||||
RUN yarn install \
|
||||
@ -33,6 +33,8 @@ FROM alpine
|
||||
|
||||
RUN apk add --no-cache openssl ncurses-libs file postgresql-client
|
||||
|
||||
RUN mkdir -p /app/uploads && chown nobody:nobody /app/uploads
|
||||
|
||||
USER nobody
|
||||
EXPOSE 4000
|
||||
|
||||
|
@ -4,4 +4,4 @@ indent_size = 2
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
max_line_length = 80
|
||||
|
@ -7,13 +7,9 @@ module.exports = {
|
||||
|
||||
extends: [
|
||||
"plugin:vue/essential",
|
||||
"@vue/airbnb",
|
||||
"@vue/typescript/recommended",
|
||||
"plugin:cypress/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier",
|
||||
"eslint:recommended",
|
||||
"@vue/prettier",
|
||||
"@vue/typescript/recommended",
|
||||
"@vue/prettier/@typescript-eslint",
|
||||
],
|
||||
|
||||
@ -21,6 +17,7 @@ module.exports = {
|
||||
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
parser: "@typescript-eslint/parser",
|
||||
},
|
||||
|
||||
rules: {
|
||||
@ -35,29 +32,24 @@ module.exports = {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"cypress/no-unnecessary-waiting": "off",
|
||||
"vue/max-len": [
|
||||
"error",
|
||||
"off",
|
||||
{
|
||||
ignoreStrings: true,
|
||||
ignoreHTMLTextContents: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
ignoreComments: true,
|
||||
template: 170,
|
||||
code: 100,
|
||||
code: 80,
|
||||
},
|
||||
],
|
||||
"prettier/prettier": "error",
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"import/prefer-default-export": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
},
|
||||
|
||||
ignorePatterns: ["src/typings/*.d.ts", "vue.config.js"],
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/__tests__/*.{j,t}s?(x)", "**/tests/unit/**/*.spec.{j,t}s?(x)"],
|
||||
env: {
|
||||
mocha: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
1
js/.gitignore
vendored
@ -4,6 +4,7 @@ node_modules
|
||||
|
||||
/tests/e2e/videos/
|
||||
/tests/e2e/screenshots/
|
||||
/coverage
|
||||
|
||||
# local env files
|
||||
.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) => {
|
||||
// here we're filtering out any type information unrelated to unions or interfaces
|
||||
const filteredData = result.data.__schema.types.filter((type) => type.possibleTypes !== null);
|
||||
const filteredData = result.data.__schema.types.filter(
|
||||
(type) => type.possibleTypes !== null
|
||||
);
|
||||
result.data.__schema.types = filteredData;
|
||||
fs.writeFile("./fragmentTypes.json", JSON.stringify(result.data), (err) => {
|
||||
if (err) {
|
||||
|
19
js/jest.config.js
Normal file
@ -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",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build --modern",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"build:assets": "vue-cli-service build --modern",
|
||||
"build:pictures": "bash ./scripts/build/pictures.sh",
|
||||
"build": "yarn run build:assets && yarn run build:pictures",
|
||||
"test:unit": "LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 vue-cli-service test:unit",
|
||||
"test:e2e": "vue-cli-service test:e2e",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
@ -29,7 +31,7 @@
|
||||
"eslint-plugin-cypress": "^2.10.3",
|
||||
"graphql": "^15.0.0",
|
||||
"graphql-tag": "^2.10.3",
|
||||
"intersection-observer": "^0.11.0",
|
||||
"intersection-observer": "^0.12.0",
|
||||
"leaflet": "^1.4.0",
|
||||
"leaflet.locatecontrol": "^0.72.0",
|
||||
"lodash": "^4.17.11",
|
||||
@ -39,6 +41,7 @@
|
||||
"tippy.js": "^6.2.3",
|
||||
"tiptap": "^1.26.0",
|
||||
"tiptap-extensions": "^1.29.1",
|
||||
"unfetch": "^4.2.0",
|
||||
"v-tooltip": "2.0.2",
|
||||
"vue": "^2.6.11",
|
||||
"vue-apollo": "^3.0.3",
|
||||
@ -52,6 +55,7 @@
|
||||
"vuedraggable": "2.23.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.18",
|
||||
"@types/leaflet": "^1.5.2",
|
||||
"@types/leaflet.locatecontrol": "^0.60.7",
|
||||
"@types/lodash": "^4.14.141",
|
||||
@ -63,27 +67,27 @@
|
||||
"@types/vuedraggable": "^2.23.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.1",
|
||||
"@typescript-eslint/parser": "^4.0.1",
|
||||
"@vue/cli-plugin-babel": "~4.5.8",
|
||||
"@vue/cli-plugin-e2e-cypress": "~4.5.8",
|
||||
"@vue/cli-plugin-eslint": "~4.5.8",
|
||||
"@vue/cli-plugin-pwa": "~4.5.8",
|
||||
"@vue/cli-plugin-router": "~4.5.8",
|
||||
"@vue/cli-plugin-typescript": "~4.5.8",
|
||||
"@vue/cli-service": "~4.5.8",
|
||||
"@vue/eslint-config-airbnb": "^5.0.2",
|
||||
"@vue/cli-plugin-babel": "~4.5.9",
|
||||
"@vue/cli-plugin-e2e-cypress": "~4.5.9",
|
||||
"@vue/cli-plugin-eslint": "~4.5.9",
|
||||
"@vue/cli-plugin-pwa": "~4.5.9",
|
||||
"@vue/cli-plugin-router": "~4.5.9",
|
||||
"@vue/cli-plugin-typescript": "~4.5.9",
|
||||
"@vue/cli-plugin-unit-jest": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.9",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"@vue/eslint-config-typescript": "^7.0.0",
|
||||
"@vue/test-utils": "^1.1.0",
|
||||
"eslint": "^7.7.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-config-prettier": "^7.0.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-vue": "^7.0.0",
|
||||
"prettier": "2.1.2",
|
||||
"prettier-eslint": "^11.0.0",
|
||||
"mock-apollo-client": "^0.4",
|
||||
"prettier": "2.2.1",
|
||||
"prettier-eslint": "^12.0.0",
|
||||
"sass": "^1.29.0",
|
||||
"sass-loader": "^10.0.1",
|
||||
"typescript": "~4.0.2",
|
||||
"typescript": "~4.1.2",
|
||||
"vue-cli-plugin-svg": "~0.1.3",
|
||||
"vue-i18n-extract": "^1.0.2",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
|
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">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import NavBar from "./components/NavBar.vue";
|
||||
import { AUTH_ACCESS_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID, AUTH_USER_ROLE } from "./constants";
|
||||
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from "./graphql/user";
|
||||
import {
|
||||
AUTH_ACCESS_TOKEN,
|
||||
AUTH_USER_EMAIL,
|
||||
AUTH_USER_ID,
|
||||
AUTH_USER_ROLE,
|
||||
} from "./constants";
|
||||
import {
|
||||
CURRENT_USER_CLIENT,
|
||||
UPDATE_CURRENT_USER_CLIENT,
|
||||
} from "./graphql/user";
|
||||
import Footer from "./components/Footer.vue";
|
||||
import Logo from "./components/Logo.vue";
|
||||
import { initializeCurrentActor } from "./utils/auth";
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { ICurrentUserRole } from "@/types/enums";
|
||||
import { ApolloCache } from "apollo-cache";
|
||||
import { NormalizedCacheObject } from "apollo-cache-inmemory";
|
||||
import { ICurrentUserRole } from "@/types/current-user.model";
|
||||
import { Resolvers } from "apollo-client/core/types";
|
||||
|
||||
export default function buildCurrentUserResolver(cache: ApolloCache<NormalizedCacheObject>) {
|
||||
export default function buildCurrentUserResolver(
|
||||
cache: ApolloCache<NormalizedCacheObject>
|
||||
): Resolvers {
|
||||
cache.writeData({
|
||||
data: {
|
||||
currentUser: {
|
||||
@ -53,7 +56,12 @@ export default function buildCurrentUserResolver(cache: ApolloCache<NormalizedCa
|
||||
preferredUsername,
|
||||
avatar,
|
||||
name,
|
||||
}: { id: string; preferredUsername: string; avatar: string; name: string },
|
||||
}: {
|
||||
id: string;
|
||||
preferredUsername: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
},
|
||||
{ cache: localCache }: { cache: ApolloCache<NormalizedCacheObject> }
|
||||
) => {
|
||||
const data = {
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { IntrospectionFragmentMatcher, NormalizedCacheObject } from "apollo-cache-inmemory";
|
||||
import {
|
||||
IntrospectionFragmentMatcher,
|
||||
NormalizedCacheObject,
|
||||
} from "apollo-cache-inmemory";
|
||||
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from "@/constants";
|
||||
import { REFRESH_TOKEN } from "@/graphql/auth";
|
||||
import { saveTokenData } from "@/utils/auth";
|
||||
@ -11,7 +14,11 @@ export const fragmentMatcher = new IntrospectionFragmentMatcher({
|
||||
{
|
||||
kind: "UNION",
|
||||
name: "SearchResult",
|
||||
possibleTypes: [{ name: "Event" }, { name: "Person" }, { name: "Group" }],
|
||||
possibleTypes: [
|
||||
{ name: "Event" },
|
||||
{ name: "Person" },
|
||||
{ name: "Group" },
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "INTERFACE",
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
.section {
|
||||
padding: 1rem 1% 4rem;
|
||||
}
|
||||
|
@ -13,7 +13,12 @@
|
||||
<template slot-scope="props">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<img width="32" :src="props.option.avatar.url" v-if="props.option.avatar" alt="" />
|
||||
<img
|
||||
width="32"
|
||||
:src="props.option.avatar.url"
|
||||
v-if="props.option.avatar"
|
||||
alt=""
|
||||
/>
|
||||
<b-icon v-else icon="account-circle" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
@ -21,7 +26,9 @@
|
||||
{{ props.option.name }}
|
||||
<br />
|
||||
<small>{{ `@${props.option.preferredUsername}` }}</small>
|
||||
<small v-if="props.option.domain">{{ `@${props.option.domain}` }}</small>
|
||||
<small v-if="props.option.domain">{{
|
||||
`@${props.option.domain}`
|
||||
}}</small>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ `@${props.option.preferredUsername}` }}
|
||||
@ -53,7 +60,9 @@ export default class ActorAutoComplete extends Vue {
|
||||
|
||||
selected: IPerson | null = this.defaultSelected;
|
||||
|
||||
name: string = this.defaultSelected ? this.defaultSelected.preferredUsername : "";
|
||||
name: string = this.defaultSelected
|
||||
? this.defaultSelected.preferredUsername
|
||||
: "";
|
||||
|
||||
page = 1;
|
||||
|
||||
|
@ -12,8 +12,15 @@
|
||||
<p>
|
||||
{{ actor.name || `@${usernameWithDomain(actor)}` }}
|
||||
</p>
|
||||
<p class="has-text-grey" v-if="actor.name">@{{ usernameWithDomain(actor) }}</p>
|
||||
<div v-if="full" class="summary" :class="{ limit: limit }" v-html="actor.summary" />
|
||||
<p class="has-text-grey" v-if="actor.name">
|
||||
@{{ usernameWithDomain(actor) }}
|
||||
</p>
|
||||
<div
|
||||
v-if="full"
|
||||
class="summary"
|
||||
:class="{ limit: limit }"
|
||||
v-html="actor.summary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,7 +7,10 @@
|
||||
<ul class="identities">
|
||||
<li v-for="identity in identities" :key="identity.id">
|
||||
<router-link
|
||||
:to="{ name: 'UpdateIdentity', params: { identityName: identity.preferredUsername } }"
|
||||
:to="{
|
||||
name: 'UpdateIdentity',
|
||||
params: { identityName: identity.preferredUsername },
|
||||
}"
|
||||
class="media identity"
|
||||
v-bind:class="{ 'is-current-identity': isCurrentIdentity(identity) }"
|
||||
>
|
||||
@ -24,7 +27,10 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<router-link :to="{ name: 'CreateIdentity' }" class="button create-identity is-primary">
|
||||
<router-link
|
||||
:to="{ name: 'CreateIdentity' }"
|
||||
class="button create-identity is-primary"
|
||||
>
|
||||
{{ $t("Create a new identity") }}
|
||||
</router-link>
|
||||
</section>
|
||||
@ -53,7 +59,7 @@ export default class Identities extends Vue {
|
||||
|
||||
errors: string[] = [];
|
||||
|
||||
isCurrentIdentity(identity: IPerson) {
|
||||
isCurrentIdentity(identity: IPerson): boolean {
|
||||
return identity.preferredUsername === this.currentIdentityName;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { ActorType } from "@/types/enums";
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import { IActor, ActorType } from "../../types/actor";
|
||||
import { IActor } from "../../types/actor";
|
||||
import ActorCard from "./ActorCard.vue";
|
||||
|
||||
@Component({
|
||||
|
@ -16,11 +16,21 @@
|
||||
checkable
|
||||
checkbox-position="left"
|
||||
>
|
||||
<b-table-column field="actor.id" label="ID" width="40" numeric v-slot="props">{{
|
||||
props.row.actor.id
|
||||
}}</b-table-column>
|
||||
<b-table-column
|
||||
field="actor.id"
|
||||
label="ID"
|
||||
width="40"
|
||||
numeric
|
||||
v-slot="props"
|
||||
>{{ props.row.actor.id }}</b-table-column
|
||||
>
|
||||
|
||||
<b-table-column field="actor.type" :label="$t('Type')" width="80" v-slot="props">
|
||||
<b-table-column
|
||||
field="actor.type"
|
||||
:label="$t('Type')"
|
||||
width="80"
|
||||
v-slot="props"
|
||||
>
|
||||
<b-icon icon="lan" v-if="RelayMixin.isInstance(props.row.actor)" />
|
||||
<b-icon icon="account-circle" v-else />
|
||||
</b-table-column>
|
||||
@ -33,26 +43,39 @@
|
||||
centered
|
||||
v-slot="props"
|
||||
>
|
||||
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`">{{
|
||||
props.row.approved ? $t("Accepted") : $t("Pending")
|
||||
}}</span>
|
||||
<span
|
||||
:class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
|
||||
>{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
|
||||
>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="actor.domain" :label="$t('Domain')" sortable>
|
||||
<template v-slot:default="props">
|
||||
<a @click="toggle(props.row)" v-if="RelayMixin.isInstance(props.row.actor)">{{
|
||||
props.row.actor.domain
|
||||
}}</a>
|
||||
<a
|
||||
@click="toggle(props.row)"
|
||||
v-if="RelayMixin.isInstance(props.row.actor)"
|
||||
>{{ props.row.actor.domain }}</a
|
||||
>
|
||||
<a @click="toggle(props.row)" v-else>{{
|
||||
`${props.row.actor.preferredUsername}@${props.row.actor.domain}`
|
||||
}}</a>
|
||||
</template>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable v-slot="props">
|
||||
<span :title="$options.filters.formatDateTimeString(props.row.updatedAt)">{{
|
||||
formatDistanceToNow(new Date(props.row.updatedAt), { locale: $dateFnsLocale })
|
||||
}}</span></b-table-column
|
||||
<b-table-column
|
||||
field="targetActor.updatedAt"
|
||||
:label="$t('Date')"
|
||||
sortable
|
||||
v-slot="props"
|
||||
>
|
||||
<span
|
||||
:title="$options.filters.formatDateTimeString(props.row.updatedAt)"
|
||||
>{{
|
||||
formatDistanceToNow(new Date(props.row.updatedAt), {
|
||||
locale: $dateFnsLocale,
|
||||
})
|
||||
}}</span
|
||||
></b-table-column
|
||||
>
|
||||
|
||||
<template slot="detail" slot-scope="props">
|
||||
@ -143,7 +166,11 @@ export default class Followers extends Mixins(RelayMixin) {
|
||||
await this.$apollo.queries.relayFollowers.refetch();
|
||||
this.checkedRows = [];
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,7 +185,11 @@ export default class Followers extends Mixins(RelayMixin) {
|
||||
await this.$apollo.queries.relayFollowers.refetch();
|
||||
this.checkedRows = [];
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
<form @submit="followRelay">
|
||||
<b-field :label="$t('Add an instance')" custom-class="add-relay" horizontal>
|
||||
<b-field
|
||||
:label="$t('Add an instance')"
|
||||
custom-class="add-relay"
|
||||
horizontal
|
||||
>
|
||||
<b-field grouped expanded size="is-large">
|
||||
<p class="control">
|
||||
<b-input v-model="newRelayAddress" :placeholder="$t('Ex: mobilizon.fr')" />
|
||||
<b-input
|
||||
v-model="newRelayAddress"
|
||||
:placeholder="$t('Ex: mobilizon.fr')"
|
||||
/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<b-button type="is-primary" native-type="submit">{{ $t("Add an instance") }}</b-button>
|
||||
<b-button type="is-primary" native-type="submit">{{
|
||||
$t("Add an instance")
|
||||
}}</b-button>
|
||||
</p>
|
||||
</b-field>
|
||||
</b-field>
|
||||
@ -29,12 +38,25 @@
|
||||
checkable
|
||||
checkbox-position="left"
|
||||
>
|
||||
<b-table-column field="targetActor.id" label="ID" width="40" numeric v-slot="props">{{
|
||||
props.row.targetActor.id
|
||||
}}</b-table-column>
|
||||
<b-table-column
|
||||
field="targetActor.id"
|
||||
label="ID"
|
||||
width="40"
|
||||
numeric
|
||||
v-slot="props"
|
||||
>{{ props.row.targetActor.id }}</b-table-column
|
||||
>
|
||||
|
||||
<b-table-column field="targetActor.type" :label="$t('Type')" width="80" v-slot="props">
|
||||
<b-icon icon="lan" v-if="RelayMixin.isInstance(props.row.targetActor)" />
|
||||
<b-table-column
|
||||
field="targetActor.type"
|
||||
:label="$t('Type')"
|
||||
width="80"
|
||||
v-slot="props"
|
||||
>
|
||||
<b-icon
|
||||
icon="lan"
|
||||
v-if="RelayMixin.isInstance(props.row.targetActor)"
|
||||
/>
|
||||
<b-icon icon="account-circle" v-else />
|
||||
</b-table-column>
|
||||
|
||||
@ -46,26 +68,39 @@
|
||||
centered
|
||||
v-slot="props"
|
||||
>
|
||||
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`">{{
|
||||
props.row.approved ? $t("Accepted") : $t("Pending")
|
||||
}}</span>
|
||||
<span
|
||||
:class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
|
||||
>{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
|
||||
>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="targetActor.domain" :label="$t('Domain')" sortable>
|
||||
<template v-slot:default="props">
|
||||
<a @click="toggle(props.row)" v-if="RelayMixin.isInstance(props.row.targetActor)">{{
|
||||
props.row.targetActor.domain
|
||||
}}</a>
|
||||
<a
|
||||
@click="toggle(props.row)"
|
||||
v-if="RelayMixin.isInstance(props.row.targetActor)"
|
||||
>{{ props.row.targetActor.domain }}</a
|
||||
>
|
||||
<a @click="toggle(props.row)" v-else>{{
|
||||
`${props.row.targetActor.preferredUsername}@${props.row.targetActor.domain}`
|
||||
}}</a>
|
||||
</template>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable v-slot="props">
|
||||
<span :title="$options.filters.formatDateTimeString(props.row.updatedAt)">{{
|
||||
formatDistanceToNow(new Date(props.row.updatedAt), { locale: $dateFnsLocale })
|
||||
}}</span></b-table-column
|
||||
<b-table-column
|
||||
field="targetActor.updatedAt"
|
||||
:label="$t('Date')"
|
||||
sortable
|
||||
v-slot="props"
|
||||
>
|
||||
<span
|
||||
:title="$options.filters.formatDateTimeString(props.row.updatedAt)"
|
||||
>{{
|
||||
formatDistanceToNow(new Date(props.row.updatedAt), {
|
||||
locale: $dateFnsLocale,
|
||||
})
|
||||
}}</span
|
||||
></b-table-column
|
||||
>
|
||||
|
||||
<template slot="detail" slot-scope="props">
|
||||
@ -103,7 +138,6 @@ import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { ADD_RELAY, REMOVE_RELAY } from "../../graphql/admin";
|
||||
import { IFollower } from "../../types/actor/follower.model";
|
||||
import { Paginate } from "../../types/paginate";
|
||||
import RelayMixin from "../../mixins/relay";
|
||||
|
||||
@Component({
|
||||
@ -127,19 +161,25 @@ export default class Followings extends Mixins(RelayMixin) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: ADD_RELAY,
|
||||
variables: {
|
||||
address: this.newRelayAddress,
|
||||
address: this.newRelayAddress.trim(), // trim to fix copy and paste domain name spaces and tabs
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowings.refetch();
|
||||
this.newRelayAddress = "";
|
||||
} catch (err) {
|
||||
Snackbar.open({ message: err.message, type: "is-danger", position: "is-bottom" });
|
||||
Snackbar.open({
|
||||
message: err.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async removeRelays(): Promise<void> {
|
||||
await this.checkedRows.forEach((row: IFollower) => {
|
||||
this.removeRelay(`${row.targetActor.preferredUsername}@${row.targetActor.domain}`);
|
||||
this.removeRelay(
|
||||
`${row.targetActor.preferredUsername}@${row.targetActor.domain}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -154,7 +194,11 @@ export default class Followings extends Mixins(RelayMixin) {
|
||||
await this.$apollo.queries.relayFollowings.refetch();
|
||||
this.checkedRows = [];
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,34 @@
|
||||
<template>
|
||||
<li :class="{ reply: comment.inReplyToComment }">
|
||||
<article class="media" :class="{ selected: commentSelected }" :id="commentId">
|
||||
<article
|
||||
class="media"
|
||||
:class="{ selected: commentSelected }"
|
||||
:id="commentId"
|
||||
>
|
||||
<popover-actor-card
|
||||
class="media-left"
|
||||
:actor="comment.actor"
|
||||
:inline="true"
|
||||
v-if="comment.actor"
|
||||
>
|
||||
<figure class="image is-48x48" v-if="!comment.deletedAt && comment.actor.avatar">
|
||||
<figure
|
||||
class="image is-48x48"
|
||||
v-if="!comment.deletedAt && comment.actor.avatar"
|
||||
>
|
||||
<img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
<b-icon
|
||||
class="media-left"
|
||||
v-else
|
||||
size="is-large"
|
||||
icon="account-circle"
|
||||
/>
|
||||
</popover-actor-card>
|
||||
<div v-else class="media-left">
|
||||
<figure class="image is-48x48" v-if="!comment.deletedAt && comment.actor.avatar">
|
||||
<figure
|
||||
class="image is-48x48"
|
||||
v-if="!comment.deletedAt && comment.actor.avatar"
|
||||
>
|
||||
<img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
@ -21,7 +36,9 @@
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<span class="first-line" v-if="!comment.deletedAt">
|
||||
<strong :class="{ organizer: commentFromOrganizer }">{{ comment.actor.name }}</strong>
|
||||
<strong :class="{ organizer: commentFromOrganizer }">{{
|
||||
comment.actor.name
|
||||
}}</strong>
|
||||
<small>@{{ usernameWithDomain(comment.actor) }}</small>
|
||||
<a class="comment-link has-text-grey" :href="commentURL">
|
||||
<small>{{
|
||||
@ -54,10 +71,15 @@
|
||||
<div class="load-replies" v-if="comment.totalReplies">
|
||||
<p v-if="!showReplies" @click="fetchReplies">
|
||||
<b-icon icon="chevron-down" /><span>{{
|
||||
$tc("View a reply", comment.totalReplies, { totalReplies: comment.totalReplies })
|
||||
$tc("View a reply", comment.totalReplies, {
|
||||
totalReplies: comment.totalReplies,
|
||||
})
|
||||
}}</span>
|
||||
</p>
|
||||
<p v-else-if="comment.totalReplies && showReplies" @click="showReplies = false">
|
||||
<p
|
||||
v-else-if="comment.totalReplies && showReplies"
|
||||
@click="showReplies = false"
|
||||
>
|
||||
<b-icon icon="chevron-up" />
|
||||
<span>{{ $t("Hide replies") }}</span>
|
||||
</p>
|
||||
@ -86,14 +108,24 @@
|
||||
</nav>
|
||||
</div>
|
||||
</article>
|
||||
<form class="reply" @submit.prevent="replyToComment" v-if="currentActor.id" v-show="replyTo">
|
||||
<form
|
||||
class="reply"
|
||||
@submit.prevent="replyToComment"
|
||||
v-if="currentActor.id"
|
||||
v-show="replyTo"
|
||||
>
|
||||
<article class="media reply">
|
||||
<figure class="media-left" v-if="currentActor.avatar">
|
||||
<p class="image is-48x48">
|
||||
<img :src="currentActor.avatar.url" alt="" />
|
||||
</p>
|
||||
</figure>
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
<b-icon
|
||||
class="media-left"
|
||||
v-else
|
||||
size="is-large"
|
||||
icon="account-circle"
|
||||
/>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<span class="first-line">
|
||||
@ -102,7 +134,12 @@
|
||||
</span>
|
||||
<br />
|
||||
<span class="editor-line">
|
||||
<editor class="editor" ref="commentEditor" v-model="newComment.text" mode="comment" />
|
||||
<editor
|
||||
class="editor"
|
||||
ref="commentEditor"
|
||||
v-model="newComment.text"
|
||||
mode="comment"
|
||||
/>
|
||||
<b-button
|
||||
:disabled="newComment.text.trim().length === 0"
|
||||
native-type="submit"
|
||||
@ -118,7 +155,12 @@
|
||||
<div class="left">
|
||||
<div class="vertical-border" @click="showReplies = false" />
|
||||
</div>
|
||||
<transition-group name="comment-replies" v-if="showReplies" class="comment-replies" tag="ul">
|
||||
<transition-group
|
||||
name="comment-replies"
|
||||
v-if="showReplies"
|
||||
class="comment-replies"
|
||||
tag="ul"
|
||||
>
|
||||
<comment
|
||||
class="reply"
|
||||
v-for="reply in comment.replies"
|
||||
@ -137,7 +179,7 @@ import { Component, Prop, Vue, Ref } from "vue-property-decorator";
|
||||
import EditorComponent from "@/components/Editor.vue";
|
||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { CommentModeration } from "../../types/event-options.model";
|
||||
import { CommentModeration } from "@/types/enums";
|
||||
import { CommentModel, IComment } from "../../types/comment.model";
|
||||
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
||||
import { IPerson, usernameWithDomain } from "../../types/actor";
|
||||
@ -155,7 +197,8 @@ import PopoverActorCard from "../Account/PopoverActorCard.vue";
|
||||
},
|
||||
},
|
||||
components: {
|
||||
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
||||
editor: () =>
|
||||
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
||||
comment: () => import(/* webpackChunkName: "comment" */ "./Comment.vue"),
|
||||
PopoverActorCard,
|
||||
},
|
||||
@ -167,7 +210,9 @@ export default class Comment extends Vue {
|
||||
|
||||
// Hack because Vue only exports it's own interface.
|
||||
// See https://github.com/kaorun343/vue-property-decorator/issues/257
|
||||
@Ref() readonly commentEditor!: EditorComponent & { replyToComment: (comment: IComment) => void };
|
||||
@Ref() readonly commentEditor!: EditorComponent & {
|
||||
replyToComment: (comment: IComment) => void;
|
||||
};
|
||||
|
||||
currentActor!: IPerson;
|
||||
|
||||
@ -231,7 +276,9 @@ export default class Comment extends Vue {
|
||||
if (!eventData) return;
|
||||
const { event } = eventData;
|
||||
const { comments } = event;
|
||||
const parentCommentIndex = comments.findIndex((oldComment) => oldComment.id === parentId);
|
||||
const parentCommentIndex = comments.findIndex(
|
||||
(oldComment) => oldComment.id === parentId
|
||||
);
|
||||
const parentComment = comments[parentCommentIndex];
|
||||
if (!parentComment) return;
|
||||
parentComment.replies = thread;
|
||||
@ -288,7 +335,6 @@ export default class Comment extends Vue {
|
||||
mutation: CREATE_REPORT,
|
||||
variables: {
|
||||
eventId: this.event.id,
|
||||
reporterId: this.currentActor.id,
|
||||
reportedId: this.comment.actor.id,
|
||||
commentsIds: [this.comment.id],
|
||||
content,
|
||||
@ -304,7 +350,11 @@ export default class Comment extends Vue {
|
||||
duration: 5000,
|
||||
});
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,11 @@
|
||||
@submit.prevent="createCommentForEvent(newComment)"
|
||||
@keyup.ctrl.enter="createCommentForEvent(newComment)"
|
||||
>
|
||||
<b-notification v-if="isEventOrganiser && !areCommentsClosed" :closable="false">{{
|
||||
$t("Comments are closed for everybody else.")
|
||||
}}</b-notification>
|
||||
<b-notification
|
||||
v-if="isEventOrganiser && !areCommentsClosed"
|
||||
:closable="false"
|
||||
>{{ $t("Comments are closed for everybody else.") }}</b-notification
|
||||
>
|
||||
<article class="media">
|
||||
<figure class="media-left">
|
||||
<identity-picker-wrapper :inline="false" v-model="newComment.actor" />
|
||||
@ -16,11 +18,20 @@
|
||||
<div class="media-content">
|
||||
<div class="field">
|
||||
<p class="control">
|
||||
<editor ref="commenteditor" mode="comment" v-model="newComment.text" />
|
||||
<editor
|
||||
ref="commenteditor"
|
||||
mode="comment"
|
||||
v-model="newComment.text"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="send-comment">
|
||||
<b-button native-type="submit" type="is-primary">{{ $t("Post a comment") }}</b-button>
|
||||
<b-button
|
||||
native-type="submit"
|
||||
type="is-primary"
|
||||
class="comment-button-submit"
|
||||
>{{ $t("Post a comment") }}</b-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@ -28,8 +39,19 @@
|
||||
<b-notification v-else :closable="false">{{
|
||||
$t("The organiser has chosen to close comments.")
|
||||
}}</b-notification>
|
||||
<transition name="comment-empty-list" mode="out-in">
|
||||
<transition-group name="comment-list" v-if="comments.length" class="comment-list" tag="ul">
|
||||
<p
|
||||
v-if="$apollo.queries.comments.loading"
|
||||
class="loading has-text-centered"
|
||||
>
|
||||
{{ $t("Loading comments…") }}
|
||||
</p>
|
||||
<transition name="comment-empty-list" mode="out-in" v-else>
|
||||
<transition-group
|
||||
name="comment-list"
|
||||
v-if="comments.length"
|
||||
class="comment-list"
|
||||
tag="ul"
|
||||
>
|
||||
<comment
|
||||
class="root-comment"
|
||||
:comment="comment"
|
||||
@ -51,7 +73,7 @@
|
||||
import { Prop, Vue, Component, Watch } from "vue-property-decorator";
|
||||
import Comment from "@/components/Comment/Comment.vue";
|
||||
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
|
||||
import { CommentModeration } from "../../types/event-options.model";
|
||||
import { CommentModeration } from "@/types/enums";
|
||||
import { CommentModel, IComment } from "../../types/comment.model";
|
||||
import {
|
||||
CREATE_COMMENT_FROM_EVENT,
|
||||
@ -76,7 +98,9 @@ import { IEvent } from "../../types/event.model";
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data.event.comments.map((comment: IComment) => new CommentModel(comment));
|
||||
return data.event.comments.map(
|
||||
(comment: IComment) => new CommentModel(comment)
|
||||
);
|
||||
},
|
||||
skip() {
|
||||
return !this.event.uuid;
|
||||
@ -86,7 +110,8 @@ import { IEvent } from "../../types/event.model";
|
||||
components: {
|
||||
Comment,
|
||||
IdentityPickerWrapper,
|
||||
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
||||
editor: () =>
|
||||
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
||||
},
|
||||
})
|
||||
export default class CommentTree extends Vue {
|
||||
@ -112,9 +137,10 @@ export default class CommentTree extends Vue {
|
||||
mutation: CREATE_COMMENT_FROM_EVENT,
|
||||
variables: {
|
||||
eventId: this.event.id,
|
||||
actorId: comment.actor.id,
|
||||
text: comment.text,
|
||||
inReplyToCommentId: comment.inReplyToComment ? comment.inReplyToComment.id : null,
|
||||
inReplyToCommentId: comment.inReplyToComment
|
||||
? comment.inReplyToComment.id
|
||||
: null,
|
||||
},
|
||||
update: (store, { data }) => {
|
||||
if (data == null) return;
|
||||
@ -204,7 +230,6 @@ export default class CommentTree extends Vue {
|
||||
mutation: DELETE_COMMENT,
|
||||
variables: {
|
||||
commentId: comment.id,
|
||||
actorId: this.currentActor.id,
|
||||
},
|
||||
update: (store, { data }) => {
|
||||
if (data == null) return;
|
||||
@ -230,7 +255,9 @@ export default class CommentTree extends Vue {
|
||||
});
|
||||
if (!localData) return;
|
||||
const { thread: oldReplyList } = localData;
|
||||
const replies = oldReplyList.filter((reply) => reply.id !== deletedCommentId);
|
||||
const replies = oldReplyList.filter(
|
||||
(reply) => reply.id !== deletedCommentId
|
||||
);
|
||||
store.writeQuery({
|
||||
query: FETCH_THREAD_REPLIES,
|
||||
variables: {
|
||||
@ -251,7 +278,9 @@ export default class CommentTree extends Vue {
|
||||
event.comments = oldComments;
|
||||
} else {
|
||||
// we have deleted a thread itself
|
||||
event.comments = oldComments.filter((reply) => reply.id !== deletedCommentId);
|
||||
event.comments = oldComments.filter(
|
||||
(reply) => reply.id !== deletedCommentId
|
||||
);
|
||||
}
|
||||
store.writeQuery({
|
||||
query: COMMENTS_THREADS,
|
||||
@ -276,14 +305,18 @@ export default class CommentTree extends Vue {
|
||||
.filter((comment) => comment.inReplyToComment == null)
|
||||
.sort((a, b) => {
|
||||
if (a.updatedAt && b.updatedAt) {
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
||||
return (
|
||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
get filteredOrderedComments(): IComment[] {
|
||||
return this.orderedComments.filter((comment) => !comment.deletedAt || comment.totalReplies > 0);
|
||||
return this.orderedComments.filter(
|
||||
(comment) => !comment.deletedAt || comment.totalReplies > 0
|
||||
);
|
||||
}
|
||||
|
||||
get isEventOrganiser(): boolean {
|
||||
@ -302,7 +335,7 @@ export default class CommentTree extends Vue {
|
||||
}
|
||||
|
||||
get isAbleToComment(): boolean {
|
||||
if (this.currentActor.id) {
|
||||
if (this.currentActor && this.currentActor.id) {
|
||||
return this.areCommentsClosed || this.isEventOrganiser;
|
||||
}
|
||||
return false;
|
||||
|
@ -15,7 +15,10 @@
|
||||
<span v-else class="name comment-link has-text-grey">
|
||||
{{ $t("[deleted]") }}
|
||||
</span>
|
||||
<span class="icons" v-if="!comment.deletedAt && comment.actor.id === currentActor.id">
|
||||
<span
|
||||
class="icons"
|
||||
v-if="!comment.deletedAt && comment.actor.id === currentActor.id"
|
||||
>
|
||||
<b-dropdown aria-role="list">
|
||||
<b-icon slot="trigger" role="button" icon="dots-horizontal" />
|
||||
|
||||
@ -44,8 +47,9 @@
|
||||
<div class="post-infos">
|
||||
<span :title="comment.insertedAt | formatDateTimeString">
|
||||
{{
|
||||
formatDistanceToNow(new Date(comment.updatedAt), { locale: $dateFnsLocale }) ||
|
||||
$t("Right now")
|
||||
formatDistanceToNow(new Date(comment.updatedAt), {
|
||||
locale: $dateFnsLocale,
|
||||
}) || $t("Right now")
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
@ -77,7 +81,9 @@
|
||||
type="is-primary"
|
||||
>{{ $t("Update") }}</b-button
|
||||
>
|
||||
<b-button native-type="button" @click="toggleEditMode">{{ $t("Cancel") }}</b-button>
|
||||
<b-button native-type="button" @click="toggleEditMode">{{
|
||||
$t("Cancel")
|
||||
}}</b-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -95,7 +101,8 @@ import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
},
|
||||
components: {
|
||||
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
||||
editor: () =>
|
||||
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
||||
},
|
||||
})
|
||||
export default class DiscussionComment extends Vue {
|
||||
|
@ -1,14 +1,23 @@
|
||||
<template>
|
||||
<router-link
|
||||
class="discussion-minimalist-card-wrapper"
|
||||
:to="{ name: RouteName.DISCUSSION, params: { slug: discussion.slug, id: discussion.id } }"
|
||||
:to="{
|
||||
name: RouteName.DISCUSSION,
|
||||
params: { slug: discussion.slug, id: discussion.id },
|
||||
}"
|
||||
>
|
||||
<div class="media-left">
|
||||
<figure
|
||||
class="image is-32x32"
|
||||
v-if="discussion.lastComment.actor && discussion.lastComment.actor.avatar"
|
||||
v-if="
|
||||
discussion.lastComment.actor && discussion.lastComment.actor.avatar
|
||||
"
|
||||
>
|
||||
<img class="is-rounded" :src="discussion.lastComment.actor.avatar.url" alt />
|
||||
<img
|
||||
class="is-rounded"
|
||||
:src="discussion.lastComment.actor.avatar.url"
|
||||
alt
|
||||
/>
|
||||
</figure>
|
||||
<b-icon v-else size="is-medium" icon="account-circle" />
|
||||
</div>
|
||||
@ -17,15 +26,18 @@
|
||||
<p class="discussion-minimalist-title">{{ discussion.title }}</p>
|
||||
<span :title="actualDate | formatDateTimeString">
|
||||
{{
|
||||
formatDistanceToNowStrict(new Date(actualDate), { locale: $dateFnsLocale }) ||
|
||||
$t("Right now")
|
||||
formatDistanceToNowStrict(new Date(actualDate), {
|
||||
locale: $dateFnsLocale,
|
||||
}) || $t("Right now")
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="has-text-grey" v-if="!discussion.lastComment.deletedAt">
|
||||
{{ htmlTextEllipsis }}
|
||||
</div>
|
||||
<div v-else class="has-text-grey">{{ $t("[This comment has been deleted]") }}</div>
|
||||
<div v-else class="has-text-grey">
|
||||
{{ $t("[This comment has been deleted]") }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
@ -54,7 +66,10 @@ export default class DiscussionListItem extends Vue {
|
||||
}
|
||||
|
||||
get actualDate(): string | Date | undefined {
|
||||
if (this.discussion.updatedAt === this.discussion.insertedAt && this.discussion.lastComment) {
|
||||
if (
|
||||
this.discussion.updatedAt === this.discussion.insertedAt &&
|
||||
this.discussion.lastComment
|
||||
) {
|
||||
return this.discussion.lastComment.publishedAt;
|
||||
}
|
||||
return this.discussion.updatedAt;
|
||||
@ -83,7 +98,8 @@ export default class DiscussionListItem extends Vue {
|
||||
|
||||
.discussion-minimalist-title {
|
||||
color: #3c376e;
|
||||
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
|
||||
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica,
|
||||
Arial, serif;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
flex: 1;
|
||||
|
@ -117,11 +117,21 @@
|
||||
<b-icon icon="format-quote-close" />
|
||||
</button>
|
||||
|
||||
<button v-if="!isBasicMode" class="menubar__button" @click="commands.undo" type="button">
|
||||
<button
|
||||
v-if="!isBasicMode"
|
||||
class="menubar__button"
|
||||
@click="commands.undo"
|
||||
type="button"
|
||||
>
|
||||
<b-icon icon="undo" />
|
||||
</button>
|
||||
|
||||
<button v-if="!isBasicMode" class="menubar__button" @click="commands.redo" type="button">
|
||||
<button
|
||||
v-if="!isBasicMode"
|
||||
class="menubar__button"
|
||||
@click="commands.redo"
|
||||
type="button"
|
||||
>
|
||||
<b-icon icon="redo" />
|
||||
</button>
|
||||
</div>
|
||||
@ -181,7 +191,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="suggestion-list__item is-empty">{{ $t("No profiles found") }}</div>
|
||||
<div v-else class="suggestion-list__item is-empty">
|
||||
{{ $t("No profiles found") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -212,7 +224,7 @@ import { SEARCH_PERSONS } from "../graphql/search";
|
||||
import { Actor, IActor, IPerson } from "../types/actor";
|
||||
import Image from "./Editor/Image";
|
||||
import MaxSize from "./Editor/MaxSize";
|
||||
import { UPLOAD_PICTURE } from "../graphql/upload";
|
||||
import { UPLOAD_MEDIA } from "../graphql/upload";
|
||||
import { listenFileUpload } from "../utils/upload";
|
||||
import { CURRENT_ACTOR_CLIENT } from "../graphql/actor";
|
||||
import { IComment } from "../types/comment.model";
|
||||
@ -372,7 +384,7 @@ export default class EditorComponent extends Vue {
|
||||
searchText: query,
|
||||
},
|
||||
});
|
||||
// TODO: TipTap doesn't handle async for onFilter, hence the following line.
|
||||
// TipTap doesn't handle async for onFilter, hence the following line.
|
||||
this.filteredActors = result.data.searchPersons.elements;
|
||||
return this.filteredActors;
|
||||
},
|
||||
@ -395,6 +407,7 @@ export default class EditorComponent extends Vue {
|
||||
new Image(),
|
||||
new MaxSize({ maxSize: this.maxSize }),
|
||||
],
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
onUpdate: ({ getHTML }: { getHTML: Function }) => {
|
||||
this.$emit("input", getHTML());
|
||||
},
|
||||
@ -410,6 +423,7 @@ export default class EditorComponent extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
showLinkMenu(command: Function, active: boolean): Function | undefined {
|
||||
if (!this.editor) return undefined;
|
||||
if (active) return command({ href: null });
|
||||
@ -430,7 +444,8 @@ export default class EditorComponent extends Vue {
|
||||
|
||||
upHandler(): void {
|
||||
this.navigatedActorIndex =
|
||||
(this.navigatedActorIndex + this.filteredActors.length - 1) % this.filteredActors.length;
|
||||
(this.navigatedActorIndex + this.filteredActors.length - 1) %
|
||||
this.filteredActors.length;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -438,7 +453,8 @@ export default class EditorComponent extends Vue {
|
||||
* if it's the last item, navigate to the first one
|
||||
*/
|
||||
downHandler(): void {
|
||||
this.navigatedActorIndex = (this.navigatedActorIndex + 1) % this.filteredActors.length;
|
||||
this.navigatedActorIndex =
|
||||
(this.navigatedActorIndex + 1) % this.filteredActors.length;
|
||||
}
|
||||
|
||||
enterHandler(): void {
|
||||
@ -522,19 +538,22 @@ export default class EditorComponent extends Vue {
|
||||
* Show a file prompt, upload picture and insert it into editor
|
||||
* @param command
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
async showImagePrompt(command: Function): Promise<void> {
|
||||
const image = await listenFileUpload();
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: UPLOAD_PICTURE,
|
||||
mutation: UPLOAD_MEDIA,
|
||||
variables: {
|
||||
file: image,
|
||||
name: image.name,
|
||||
actorId: this.currentActor.id,
|
||||
},
|
||||
});
|
||||
if (data.uploadPicture && data.uploadPicture.url) {
|
||||
command({ src: data.uploadPicture.url });
|
||||
if (data.uploadMedia && data.uploadMedia.url) {
|
||||
command({
|
||||
src: data.uploadMedia.url,
|
||||
"data-media-id": data.uploadMedia.id,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
@ -1,6 +1,7 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
import { Node } from "tiptap";
|
||||
import { UPLOAD_PICTURE } from "@/graphql/upload";
|
||||
import { UPLOAD_MEDIA } from "@/graphql/upload";
|
||||
import apolloProvider from "@/vue-apollo";
|
||||
import ApolloClient from "apollo-client";
|
||||
import { NormalizedCacheObject } from "apollo-cache-inmemory";
|
||||
@ -12,6 +13,7 @@ import { EditorView } from "prosemirror-view";
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
export default class Image extends Node {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
get name() {
|
||||
return "image";
|
||||
}
|
||||
@ -27,16 +29,18 @@ export default class Image extends Node {
|
||||
title: {
|
||||
default: null,
|
||||
},
|
||||
"data-media-id": {},
|
||||
},
|
||||
group: "inline",
|
||||
draggable: true,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: "img[src]",
|
||||
tag: "img",
|
||||
getAttrs: (dom: any) => ({
|
||||
src: dom.getAttribute("src"),
|
||||
title: dom.getAttribute("title"),
|
||||
alt: dom.getAttribute("alt"),
|
||||
"data-media-id": dom.getAttribute("data-media-id"),
|
||||
}),
|
||||
},
|
||||
],
|
||||
@ -45,15 +49,21 @@ export default class Image extends Node {
|
||||
}
|
||||
|
||||
commands({ type }: { type: NodeType }): any {
|
||||
return (attrs: { [key: string]: string }) => (state: EditorState, dispatch: DispatchFn) => {
|
||||
return (attrs: { [key: string]: string }) => (
|
||||
state: EditorState,
|
||||
dispatch: DispatchFn
|
||||
) => {
|
||||
const { selection }: { selection: TextSelection } = state;
|
||||
const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos;
|
||||
const position = selection.$cursor
|
||||
? selection.$cursor.pos
|
||||
: selection.$to.pos;
|
||||
const node = type.create(attrs);
|
||||
const transaction = state.tr.insert(position, node);
|
||||
dispatch(transaction);
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
@ -72,7 +82,8 @@ export default class Image extends Node {
|
||||
}
|
||||
|
||||
const images = Array.from(realEvent.dataTransfer.files).filter(
|
||||
(file: any) => /image/i.test(file.type) && !/svg/i.test(file.type)
|
||||
(file: any) =>
|
||||
/image/i.test(file.type) && !/svg/i.test(file.type)
|
||||
);
|
||||
|
||||
if (images.length === 0) {
|
||||
@ -88,21 +99,24 @@ export default class Image extends Node {
|
||||
});
|
||||
if (!coordinates) return false;
|
||||
const client = apolloProvider.defaultClient as ApolloClient<NormalizedCacheObject>;
|
||||
const editorElem = document.getElementById("tiptab-editor");
|
||||
const actorId = editorElem && editorElem.dataset.actorId;
|
||||
|
||||
try {
|
||||
images.forEach(async (image) => {
|
||||
const { data } = await client.mutate({
|
||||
mutation: UPLOAD_PICTURE,
|
||||
mutation: UPLOAD_MEDIA,
|
||||
variables: {
|
||||
actorId,
|
||||
file: image,
|
||||
name: image.name,
|
||||
},
|
||||
});
|
||||
const node = schema.nodes.image.create({ src: data.uploadPicture.url });
|
||||
const transaction = view.state.tr.insert(coordinates.pos, node);
|
||||
const node = schema.nodes.image.create({
|
||||
src: data.uploadMedia.url,
|
||||
"data-media-id": data.uploadMedia.id,
|
||||
});
|
||||
const transaction = view.state.tr.insert(
|
||||
coordinates.pos,
|
||||
node
|
||||
);
|
||||
view.dispatch(transaction);
|
||||
});
|
||||
return true;
|
||||
|
@ -1,17 +1,23 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
import { Extension, Plugin } from "tiptap";
|
||||
|
||||
export default class MaxSize extends Extension {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
get name() {
|
||||
return "maxSize";
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
get defaultOptions() {
|
||||
return {
|
||||
maxSize: null,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
@ -21,7 +27,7 @@ export default class MaxSize extends Extension {
|
||||
const newLength = newState.doc.content.size;
|
||||
|
||||
if (newLength > max && newLength > oldLength) {
|
||||
let newTr = newState.tr;
|
||||
const newTr = newState.tr;
|
||||
newTr.insertText("", max + 1, newLength);
|
||||
|
||||
return newTr;
|
||||
|
@ -21,9 +21,13 @@
|
||||
</b-autocomplete>
|
||||
</b-field>
|
||||
<b-field v-if="isSecureContext()">
|
||||
<b-button type="is-text" v-if="!gettingLocation" icon-right="target" @click="locateMe">{{
|
||||
$t("Use my location")
|
||||
}}</b-button>
|
||||
<b-button
|
||||
type="is-text"
|
||||
v-if="!gettingLocation"
|
||||
icon-right="target"
|
||||
@click="locateMe"
|
||||
>{{ $t("Use my location") }}</b-button
|
||||
>
|
||||
<span v-else>{{ $t("Getting location") }}</span>
|
||||
</b-field>
|
||||
<!--
|
||||
@ -50,7 +54,7 @@
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { LatLng } from "leaflet";
|
||||
import { debounce } from "lodash";
|
||||
import { debounce, DebouncedFunc } from "lodash";
|
||||
import { Address, IAddress } from "../../types/address.model";
|
||||
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
@ -58,7 +62,8 @@ import { IConfig } from "../../types/config.model";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
"map-leaflet": () => import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
|
||||
"map-leaflet": () =>
|
||||
import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
|
||||
},
|
||||
apollo: {
|
||||
config: CONFIG,
|
||||
@ -81,7 +86,8 @@ export default class AddressAutoComplete extends Vue {
|
||||
|
||||
private gettingLocation = false;
|
||||
|
||||
private location!: Position;
|
||||
// eslint-disable-next-line no-undef
|
||||
private location!: GeolocationPosition;
|
||||
|
||||
private gettingLocationError: any;
|
||||
|
||||
@ -89,7 +95,7 @@ export default class AddressAutoComplete extends Vue {
|
||||
|
||||
config!: IConfig;
|
||||
|
||||
fetchAsyncData!: Function;
|
||||
fetchAsyncData!: DebouncedFunc<(query: string) => Promise<void>>;
|
||||
|
||||
// We put this in data because of issues like
|
||||
// https://github.com/vuejs/vue-class-component/issues/263
|
||||
@ -121,7 +127,9 @@ export default class AddressAutoComplete extends Vue {
|
||||
},
|
||||
});
|
||||
|
||||
this.addressData = result.data.searchAddress.map((address: IAddress) => new Address(address));
|
||||
this.addressData = result.data.searchAddress.map(
|
||||
(address: IAddress) => new Address(address)
|
||||
);
|
||||
this.isFetching = false;
|
||||
}
|
||||
|
||||
@ -174,7 +182,9 @@ export default class AddressAutoComplete extends Vue {
|
||||
},
|
||||
});
|
||||
|
||||
this.addressData = result.data.reverseGeocode.map((address: IAddress) => new Address(address));
|
||||
this.addressData = result.data.reverseGeocode.map(
|
||||
(address: IAddress) => new Address(address)
|
||||
);
|
||||
if (this.addressData.length > 0) {
|
||||
const defaultAddress = new Address(this.addressData[0]);
|
||||
this.selected = defaultAddress;
|
||||
@ -197,7 +207,10 @@ export default class AddressAutoComplete extends Vue {
|
||||
this.location = await AddressAutoComplete.getLocation();
|
||||
this.mapDefaultZoom = 12;
|
||||
this.reverseGeoCode(
|
||||
new LatLng(this.location.coords.latitude, this.location.coords.longitude),
|
||||
new LatLng(
|
||||
this.location.coords.latitude,
|
||||
this.location.coords.longitude
|
||||
),
|
||||
12
|
||||
);
|
||||
} catch (e) {
|
||||
@ -207,7 +220,8 @@ export default class AddressAutoComplete extends Vue {
|
||||
this.gettingLocation = false;
|
||||
}
|
||||
|
||||
static async getLocation(): Promise<Position> {
|
||||
// eslint-disable-next-line no-undef
|
||||
static async getLocation(): Promise<GeolocationPosition> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!("geolocation" in navigator)) {
|
||||
reject(new Error("Geolocation is not available."));
|
||||
|
@ -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>
|
||||
<router-link class="card" :to="{ name: 'Event', params: { uuid: event.uuid } }">
|
||||
<router-link
|
||||
class="card"
|
||||
:to="{ name: 'Event', params: { uuid: event.uuid } }"
|
||||
>
|
||||
<div class="card-image">
|
||||
<figure
|
||||
class="image is-16by9"
|
||||
@ -7,10 +10,19 @@
|
||||
event.picture ? event.picture.url : '/img/mobilizon_default_card.png'
|
||||
}')`"
|
||||
>
|
||||
<div class="tag-container" v-if="event.tags">
|
||||
<div
|
||||
class="tag-container"
|
||||
v-if="event.tags || event.status !== EventStatus.CONFIRMED"
|
||||
>
|
||||
<b-tag type="is-info" v-if="event.status === EventStatus.TENTATIVE">
|
||||
{{ $t("Tentative") }}
|
||||
</b-tag>
|
||||
<b-tag type="is-danger" v-if="event.status === EventStatus.CANCELLED">
|
||||
{{ $t("Cancelled") }}
|
||||
</b-tag>
|
||||
<router-link
|
||||
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
|
||||
v-for="tag in event.tags.slice(0, 3)"
|
||||
v-for="tag in (event.tags || []).slice(0, 3)"
|
||||
:key="tag.slug"
|
||||
>
|
||||
<b-tag type="is-light">{{ tag.title }}</b-tag>
|
||||
@ -21,14 +33,18 @@
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<date-calendar-icon v-if="!mergedOptions.hideDate" :date="event.beginsOn" />
|
||||
<date-calendar-icon
|
||||
v-if="!mergedOptions.hideDate"
|
||||
:date="event.beginsOn"
|
||||
/>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="event-title">{{ event.title }}</p>
|
||||
<div class="event-subtitle" v-if="event.physicalAddress">
|
||||
<!-- <p>{{ $t('By @{username}', { username: actor.preferredUsername }) }}</p>-->
|
||||
<span>
|
||||
{{ event.physicalAddress.description }}, {{ event.physicalAddress.locality }}
|
||||
{{ event.physicalAddress.description }},
|
||||
{{ event.physicalAddress.locality }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -77,7 +93,7 @@ import { IEvent, IEventCardOptions } from "@/types/event.model";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||
import { Actor, Person } from "@/types/actor";
|
||||
import { ParticipantRole } from "../../types/participant.model";
|
||||
import { EventStatus, ParticipantRole } from "@/types/enums";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
@ -92,6 +108,8 @@ export default class EventCard extends Vue {
|
||||
|
||||
ParticipantRole = ParticipantRole;
|
||||
|
||||
EventStatus = EventStatus;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
defaultOptions: IEventCardOptions = {
|
||||
|
@ -18,7 +18,9 @@
|
||||
</docs>
|
||||
|
||||
<template>
|
||||
<span v-if="!endsOn">{{ beginsOn | formatDateTimeString(showStartTime) }}</span>
|
||||
<span v-if="!endsOn">{{
|
||||
beginsOn | formatDateTimeString(showStartTime)
|
||||
}}</span>
|
||||
<span v-else-if="isSameDay() && showStartTime && showEndTime">
|
||||
{{
|
||||
$t("On {date} from {startTime} to {endTime}", {
|
||||
@ -44,7 +46,9 @@
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else-if="isSameDay()">{{ $t("On {date}", { date: formatDate(beginsOn) }) }}</span>
|
||||
<span v-else-if="isSameDay()">{{
|
||||
$t("On {date}", { date: formatDate(beginsOn) })
|
||||
}}</span>
|
||||
<span v-else-if="endsOn && showStartTime && showEndTime">
|
||||
{{
|
||||
$t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
|
||||
@ -97,7 +101,9 @@ export default class EventFullDate extends Vue {
|
||||
}
|
||||
|
||||
isSameDay(): boolean {
|
||||
const sameDay = new Date(this.beginsOn).toDateString() === new Date(this.endsOn).toDateString();
|
||||
const sameDay =
|
||||
new Date(this.beginsOn).toDateString() ===
|
||||
new Date(this.endsOn).toDateString();
|
||||
return this.endsOn !== undefined && sameDay;
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,49 @@
|
||||
<template>
|
||||
<article class="box">
|
||||
<div class="columns">
|
||||
<div class="content column">
|
||||
<div class="identity-header">
|
||||
<figure class="image is-24x24" v-if="participation.actor.avatar">
|
||||
<img class="is-rounded" :src="participation.actor.avatar.url" alt="" />
|
||||
</figure>
|
||||
{{ displayNameAndUsername(participation.actor) }}
|
||||
</div>
|
||||
<div class="list-card">
|
||||
<div class="content">
|
||||
<div class="title-wrapper">
|
||||
<div class="date-component">
|
||||
<date-calendar-icon :date="participation.event.beginsOn" />
|
||||
</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>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="participation-actor has-text-grey">
|
||||
<span>
|
||||
<b-icon icon="earth" v-if="participation.event.visibility === EventVisibility.PUBLIC" />
|
||||
<b-icon
|
||||
icon="earth"
|
||||
v-if="participation.event.visibility === EventVisibility.PUBLIC"
|
||||
/>
|
||||
<b-icon
|
||||
icon="link"
|
||||
v-else-if="participation.event.visibility === EventVisibility.UNLISTED"
|
||||
v-else-if="
|
||||
participation.event.visibility === EventVisibility.UNLISTED
|
||||
"
|
||||
/>
|
||||
<b-icon
|
||||
icon="lock"
|
||||
v-else-if="participation.event.visibility === EventVisibility.PRIVATE"
|
||||
v-else-if="
|
||||
participation.event.visibility === EventVisibility.PRIVATE
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
participation.event.physicalAddress && participation.event.physicalAddress.locality
|
||||
participation.event.physicalAddress &&
|
||||
participation.event.physicalAddress.locality
|
||||
"
|
||||
>{{ participation.event.physicalAddress.locality }} -</span
|
||||
>
|
||||
@ -32,19 +51,10 @@
|
||||
<i18n tag="span" path="Organized by {name}">
|
||||
<popover-actor-card
|
||||
slot="name"
|
||||
:actor="participation.event.organizerActor"
|
||||
:actor="organizerActor"
|
||||
:inline="true"
|
||||
>
|
||||
{{ participation.event.organizerActor.displayName() }}
|
||||
</popover-actor-card>
|
||||
</i18n>
|
||||
<i18n
|
||||
v-if="participation.role === ParticipantRole.PARTICIPANT"
|
||||
path="Going as {name}"
|
||||
tag="span"
|
||||
>
|
||||
<popover-actor-card slot="name" :actor="participation.actor" :inline="true">
|
||||
{{ participation.actor.displayName() }}
|
||||
{{ organizerActor.displayName() }}
|
||||
</popover-actor-card>
|
||||
</i18n>
|
||||
</span>
|
||||
@ -53,12 +63,15 @@
|
||||
<span
|
||||
class="participant-stats"
|
||||
v-if="
|
||||
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
|
||||
participation.role
|
||||
)
|
||||
![
|
||||
ParticipantRole.PARTICIPANT,
|
||||
ParticipantRole.NOT_APPROVED,
|
||||
].includes(participation.role)
|
||||
"
|
||||
>
|
||||
<span v-if="participation.event.options.maximumAttendeeCapacity !== 0">
|
||||
<span
|
||||
v-if="participation.event.options.maximumAttendeeCapacity !== 0"
|
||||
>
|
||||
{{
|
||||
$tc(
|
||||
"{available}/{capacity} available places",
|
||||
@ -68,16 +81,21 @@
|
||||
available:
|
||||
participation.event.options.maximumAttendeeCapacity -
|
||||
participation.event.participantStats.participant,
|
||||
capacity: participation.event.options.maximumAttendeeCapacity,
|
||||
capacity:
|
||||
participation.event.options.maximumAttendeeCapacity,
|
||||
}
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{
|
||||
$tc("{count} participants", participation.event.participantStats.participant, {
|
||||
count: participation.event.participantStats.participant,
|
||||
})
|
||||
$tc(
|
||||
"{count} participants",
|
||||
participation.event.participantStats.participant,
|
||||
{
|
||||
count: participation.event.participantStats.participant,
|
||||
}
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-if="participation.event.participantStats.notApproved > 0">
|
||||
@ -103,80 +121,90 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions column is-narrow">
|
||||
<ul>
|
||||
<li
|
||||
<div class="actions">
|
||||
<b-dropdown aria-role="list" position="is-bottom-left">
|
||||
<b-button slot="trigger" role="button" icon-right="dots-horizontal">
|
||||
{{ $t("Actions") }}
|
||||
</b-button>
|
||||
|
||||
<b-dropdown-item
|
||||
v-if="
|
||||
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
|
||||
participation.role
|
||||
)
|
||||
![
|
||||
ParticipantRole.PARTICIPANT,
|
||||
ParticipantRole.NOT_APPROVED,
|
||||
].includes(participation.role)
|
||||
"
|
||||
aria-role="listitem"
|
||||
@click="
|
||||
gotToWithCheck(participation, {
|
||||
name: RouteName.EDIT_EVENT,
|
||||
params: { eventId: participation.event.uuid },
|
||||
})
|
||||
"
|
||||
>
|
||||
<b-button
|
||||
type="is-text"
|
||||
@click="
|
||||
gotToWithCheck(participation, {
|
||||
name: RouteName.EDIT_EVENT,
|
||||
params: { eventId: participation.event.uuid },
|
||||
})
|
||||
"
|
||||
icon-left="pencil"
|
||||
>{{ $t("Edit") }}</b-button
|
||||
>
|
||||
</li>
|
||||
<li v-if="participation.role === ParticipantRole.CREATOR">
|
||||
<b-button
|
||||
type="is-text"
|
||||
@click="
|
||||
gotToWithCheck(participation, {
|
||||
name: RouteName.DUPLICATE_EVENT,
|
||||
params: { eventId: participation.event.uuid },
|
||||
})
|
||||
"
|
||||
icon-left="content-duplicate"
|
||||
>
|
||||
{{ $t("Duplicate") }}
|
||||
</b-button>
|
||||
</li>
|
||||
<li
|
||||
v-if="
|
||||
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
|
||||
participation.role
|
||||
)
|
||||
<b-icon icon="pencil" />
|
||||
{{ $t("Edit") }}
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
v-if="participation.role === ParticipantRole.CREATOR"
|
||||
aria-role="listitem"
|
||||
@click="
|
||||
gotToWithCheck(participation, {
|
||||
name: RouteName.DUPLICATE_EVENT,
|
||||
params: { eventId: participation.event.uuid },
|
||||
})
|
||||
"
|
||||
>
|
||||
<b-icon icon="content-duplicate" />
|
||||
{{ $t("Duplicate") }}
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
v-if="
|
||||
![
|
||||
ParticipantRole.PARTICIPANT,
|
||||
ParticipantRole.NOT_APPROVED,
|
||||
].includes(participation.role)
|
||||
"
|
||||
aria-role="listitem"
|
||||
@click="openDeleteEventModalWrapper"
|
||||
>
|
||||
<b-button type="is-text" icon-left="delete">{{ $t("Delete") }}</b-button>
|
||||
</li>
|
||||
<li
|
||||
<b-icon icon="delete" />
|
||||
{{ $t("Delete") }}
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
v-if="
|
||||
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
|
||||
participation.role
|
||||
)
|
||||
![
|
||||
ParticipantRole.PARTICIPANT,
|
||||
ParticipantRole.NOT_APPROVED,
|
||||
].includes(participation.role)
|
||||
"
|
||||
aria-role="listitem"
|
||||
@click="
|
||||
gotToWithCheck(participation, {
|
||||
name: RouteName.PARTICIPATIONS,
|
||||
params: { eventId: participation.event.uuid },
|
||||
})
|
||||
"
|
||||
>
|
||||
<b-button
|
||||
type="is-text"
|
||||
@click="
|
||||
gotToWithCheck(participation, {
|
||||
name: RouteName.PARTICIPATIONS,
|
||||
params: { eventId: participation.event.uuid },
|
||||
})
|
||||
"
|
||||
icon-left="account-multiple-plus"
|
||||
>{{ $t("Manage participations") }}</b-button
|
||||
<b-icon icon="account-multiple-plus" />
|
||||
{{ $t("Manage participations") }}
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item aria-role="listitem" has-link>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: participation.event.uuid },
|
||||
}"
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<b-button
|
||||
tag="router-link"
|
||||
icon-left="view-compact"
|
||||
type="is-text"
|
||||
:to="{ name: RouteName.EVENT, params: { uuid: participation.event.uuid } }"
|
||||
>{{ $t("View event page") }}</b-button
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<b-icon icon="view-compact" />
|
||||
{{ $t("View event page") }}
|
||||
</router-link>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@ -187,9 +215,10 @@ import { Component, Prop } from "vue-property-decorator";
|
||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||
import { mixins } from "vue-class-component";
|
||||
import { RawLocation, Route } from "vue-router";
|
||||
import { IParticipant, ParticipantRole } from "../../types/participant.model";
|
||||
import { EventVisibility, IEventCardOptions } from "../../types/event.model";
|
||||
import { IPerson } from "../../types/actor";
|
||||
import { EventVisibility, ParticipantRole } from "@/types/enums";
|
||||
import { IParticipant } from "../../types/participant.model";
|
||||
import { IEventCardOptions } from "../../types/event.model";
|
||||
import { displayNameAndUsername, IActor, IPerson } from "../../types/actor";
|
||||
import ActorMixin from "../../mixins/actor";
|
||||
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
||||
import EventMixin from "../../mixins/event";
|
||||
@ -234,6 +263,8 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
|
||||
|
||||
EventVisibility = EventVisibility;
|
||||
|
||||
displayNameAndUsername = displayNameAndUsername;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
get mergedOptions(): IEventCardOptions {
|
||||
@ -246,11 +277,17 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
|
||||
async openDeleteEventModalWrapper(): Promise<void> {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
await this.openDeleteEventModal(this.participation.event, this.currentActor);
|
||||
await this.openDeleteEventModal(this.participation.event);
|
||||
}
|
||||
|
||||
async gotToWithCheck(participation: IParticipant, route: RawLocation): Promise<Route> {
|
||||
if (participation.actor.id !== this.currentActor.id && participation.event.organizerActor) {
|
||||
async gotToWithCheck(
|
||||
participation: IParticipant,
|
||||
route: RawLocation
|
||||
): Promise<Route> {
|
||||
if (
|
||||
participation.actor.id !== this.currentActor.id &&
|
||||
participation.event.organizerActor
|
||||
) {
|
||||
const organizer = participation.event.organizerActor as IPerson;
|
||||
await changeIdentity(this.$apollo.provider.defaultClient, organizer);
|
||||
this.$buefy.notification.open({
|
||||
@ -267,6 +304,16 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
|
||||
}
|
||||
return this.$router.push(route);
|
||||
}
|
||||
|
||||
get organizerActor(): IActor | undefined {
|
||||
if (
|
||||
this.participation.event.attributedTo &&
|
||||
this.participation.event.attributedTo.id
|
||||
) {
|
||||
return this.participation.event.attributedTo;
|
||||
}
|
||||
return this.participation.event.organizerActor;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -293,74 +340,71 @@ article.box {
|
||||
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;
|
||||
|
||||
.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;
|
||||
}
|
||||
figure {
|
||||
padding-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
ul li {
|
||||
margin: 0 auto;
|
||||
.is-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button.is-text {
|
||||
text-decoration: none;
|
||||
|
||||
::v-deep span:first-child i.mdi::before {
|
||||
font-size: 24px !important;
|
||||
}
|
||||
|
||||
::v-deep span:last-child {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
font-size: 0.8rem;
|
||||
color: $background-color;
|
||||
}
|
||||
}
|
||||
& > .columns {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
@ -6,7 +6,9 @@
|
||||
<div class="date-component">
|
||||
<date-calendar-icon :date="event.beginsOn" />
|
||||
</div>
|
||||
<router-link :to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }">
|
||||
<router-link
|
||||
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
||||
>
|
||||
<h2 class="title">{{ event.title }}</h2>
|
||||
</router-link>
|
||||
</div>
|
||||
@ -15,17 +17,34 @@
|
||||
{{ event.physicalAddress.locality }}
|
||||
</span>
|
||||
<span v-if="event.attributedTo && options.memberofGroup">
|
||||
{{ $t("Created by {name}", { name: usernameWithDomain(event.organizerActor) }) }}
|
||||
{{
|
||||
$t("Created by {name}", {
|
||||
name: usernameWithDomain(event.organizerActor),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else-if="options.memberofGroup">
|
||||
{{ $t("Organized by {name}", { name: usernameWithDomain(event.organizerActor) }) }}
|
||||
{{
|
||||
$t("Organized by {name}", {
|
||||
name: usernameWithDomain(event.organizerActor),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<span class="column is-narrow">
|
||||
<b-icon icon="earth" v-if="event.visibility === EventVisibility.PUBLIC" />
|
||||
<b-icon icon="link" v-if="event.visibility === EventVisibility.UNLISTED" />
|
||||
<b-icon icon="lock" v-if="event.visibility === EventVisibility.PRIVATE" />
|
||||
<b-icon
|
||||
icon="earth"
|
||||
v-if="event.visibility === EventVisibility.PUBLIC"
|
||||
/>
|
||||
<b-icon
|
||||
icon="link"
|
||||
v-if="event.visibility === EventVisibility.UNLISTED"
|
||||
/>
|
||||
<b-icon
|
||||
icon="lock"
|
||||
v-if="event.visibility === EventVisibility.PRIVATE"
|
||||
/>
|
||||
</span>
|
||||
<span class="column is-narrow participant-stats">
|
||||
<span v-if="event.options.maximumAttendeeCapacity !== 0">
|
||||
@ -38,9 +57,13 @@
|
||||
</span>
|
||||
<span v-else>
|
||||
{{
|
||||
$tc("{count} participants", event.participantStats.participant, {
|
||||
count: event.participantStats.participant,
|
||||
})
|
||||
$tc(
|
||||
"{count} participants",
|
||||
event.participantStats.participant,
|
||||
{
|
||||
count: event.participantStats.participant,
|
||||
}
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
@ -51,7 +74,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { EventVisibility, IEventCardOptions, IEvent } from "@/types/event.model";
|
||||
import { IEventCardOptions, IEvent } from "@/types/event.model";
|
||||
import { Component, Prop } from "vue-property-decorator";
|
||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||
import { IPerson, usernameWithDomain } from "@/types/actor";
|
||||
@ -59,7 +82,7 @@ import { mixins } from "vue-class-component";
|
||||
import ActorMixin from "@/mixins/actor";
|
||||
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
|
||||
import EventMixin from "@/mixins/event";
|
||||
import { ParticipantRole } from "../../types/participant.model";
|
||||
import { EventVisibility, ParticipantRole } from "@/types/enums";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
const defaultOptions: IEventCardOptions = {
|
||||
|
@ -14,10 +14,12 @@
|
||||
{{
|
||||
$tc(
|
||||
"{available}/{capacity} available places",
|
||||
event.options.maximumAttendeeCapacity - event.participantStats.participant,
|
||||
event.options.maximumAttendeeCapacity -
|
||||
event.participantStats.participant,
|
||||
{
|
||||
available:
|
||||
event.options.maximumAttendeeCapacity - event.participantStats.participant,
|
||||
event.options.maximumAttendeeCapacity -
|
||||
event.participantStats.participant,
|
||||
capacity: event.options.maximumAttendeeCapacity,
|
||||
}
|
||||
)
|
||||
@ -42,9 +44,13 @@
|
||||
"
|
||||
>
|
||||
{{
|
||||
$tc("{count} requests waiting", event.participantStats.notApproved, {
|
||||
count: event.participantStats.notApproved,
|
||||
})
|
||||
$tc(
|
||||
"{count} requests waiting",
|
||||
event.participantStats.notApproved,
|
||||
{
|
||||
count: event.participantStats.notApproved,
|
||||
}
|
||||
)
|
||||
}}
|
||||
</b-button>
|
||||
</span>
|
||||
@ -56,7 +62,7 @@
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||
import { ParticipantRole } from "../../types/participant.model";
|
||||
import { ParticipantRole } from "@/types/enums";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
@ -88,7 +94,8 @@ export default class EventMinimalistCard extends Vue {
|
||||
|
||||
.event-minimalist-title {
|
||||
color: #3c376e;
|
||||
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
|
||||
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
|
||||
serif;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
@ -33,9 +33,12 @@
|
||||
<div v-else-if="queryText.length >= 3" class="is-enabled">
|
||||
<span>{{ $t('No results for "{queryText}"') }}</span>
|
||||
<span>{{
|
||||
$t("You can try another search term or drag and drop the marker on the map", {
|
||||
queryText,
|
||||
})
|
||||
$t(
|
||||
"You can try another search term or drag and drop the marker on the map",
|
||||
{
|
||||
queryText,
|
||||
}
|
||||
)
|
||||
}}</span>
|
||||
<!-- <p class="control" @click="openNewAddressModal">-->
|
||||
<!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
|
||||
@ -102,7 +105,7 @@
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { LatLng } from "leaflet";
|
||||
import { debounce } from "lodash";
|
||||
import { debounce, DebouncedFunc } from "lodash";
|
||||
import { Address, IAddress } from "../../types/address.model";
|
||||
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
@ -110,7 +113,8 @@ import { IConfig } from "../../types/config.model";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
"map-leaflet": () => import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
|
||||
"map-leaflet": () =>
|
||||
import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
|
||||
},
|
||||
apollo: {
|
||||
config: CONFIG,
|
||||
@ -133,7 +137,8 @@ export default class FullAddressAutoComplete extends Vue {
|
||||
|
||||
private gettingLocation = false;
|
||||
|
||||
private location!: Position;
|
||||
// eslint-disable-next-line no-undef
|
||||
private location!: GeolocationPosition;
|
||||
|
||||
private gettingLocationError: any;
|
||||
|
||||
@ -141,11 +146,11 @@ export default class FullAddressAutoComplete extends Vue {
|
||||
|
||||
config!: IConfig;
|
||||
|
||||
fetchAsyncData!: Function;
|
||||
fetchAsyncData!: DebouncedFunc<(query: string) => Promise<void>>;
|
||||
|
||||
// We put this in data because of issues like
|
||||
// https://github.com/vuejs/vue-class-component/issues/263
|
||||
data() {
|
||||
data(): Record<string, unknown> {
|
||||
return {
|
||||
fetchAsyncData: debounce(this.asyncData, 200),
|
||||
};
|
||||
@ -173,7 +178,9 @@ export default class FullAddressAutoComplete extends Vue {
|
||||
},
|
||||
});
|
||||
|
||||
this.addressData = result.data.searchAddress.map((address: IAddress) => new Address(address));
|
||||
this.addressData = result.data.searchAddress.map(
|
||||
(address: IAddress) => new Address(address)
|
||||
);
|
||||
this.isFetching = false;
|
||||
}
|
||||
|
||||
@ -224,7 +231,9 @@ export default class FullAddressAutoComplete extends Vue {
|
||||
},
|
||||
});
|
||||
|
||||
this.addressData = result.data.reverseGeocode.map((address: IAddress) => new Address(address));
|
||||
this.addressData = result.data.reverseGeocode.map(
|
||||
(address: IAddress) => new Address(address)
|
||||
);
|
||||
if (this.addressData.length > 0) {
|
||||
const defaultAddress = new Address(this.addressData[0]);
|
||||
this.selected = defaultAddress;
|
||||
@ -248,7 +257,10 @@ export default class FullAddressAutoComplete extends Vue {
|
||||
this.location = await FullAddressAutoComplete.getLocation();
|
||||
this.mapDefaultZoom = 12;
|
||||
this.reverseGeoCode(
|
||||
new LatLng(this.location.coords.latitude, this.location.coords.longitude),
|
||||
new LatLng(
|
||||
this.location.coords.latitude,
|
||||
this.location.coords.longitude
|
||||
),
|
||||
12
|
||||
);
|
||||
} catch (e) {
|
||||
@ -266,7 +278,8 @@ export default class FullAddressAutoComplete extends Vue {
|
||||
return window.isSecureContext;
|
||||
}
|
||||
|
||||
static async getLocation(): Promise<Position> {
|
||||
// eslint-disable-next-line no-undef
|
||||
static async getLocation(): Promise<GeolocationPosition> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!("geolocation" in navigator)) {
|
||||
reject(new Error("Geolocation is not available."));
|
||||
|
@ -10,9 +10,18 @@
|
||||
>
|
||||
<div class="media">
|
||||
<figure class="image is-48x48" v-if="availableActor.avatar">
|
||||
<img class="media-left is-rounded" :src="availableActor.avatar.url" alt="" />
|
||||
<img
|
||||
class="media-left is-rounded"
|
||||
:src="availableActor.avatar.url"
|
||||
alt=""
|
||||
/>
|
||||
</figure>
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
<b-icon
|
||||
class="media-left"
|
||||
v-else
|
||||
size="is-large"
|
||||
icon="account-circle"
|
||||
/>
|
||||
<div class="media-content">
|
||||
<h3>{{ availableActor.name }}</h3>
|
||||
<small>{{ `@${availableActor.preferredUsername}` }}</small>
|
||||
@ -23,9 +32,11 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { IMember, IPerson, MemberRole, IActor, Actor } from "@/types/actor";
|
||||
import { IPerson, IActor, Actor } from "@/types/actor";
|
||||
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@ -59,16 +70,21 @@ export default class OrganizerPicker extends Vue {
|
||||
get actualMemberships(): IMember[] {
|
||||
if (this.restrictModeratorLevel) {
|
||||
return this.groupMemberships.elements.filter((membership: IMember) =>
|
||||
[MemberRole.ADMINISTRATOR, MemberRole.MODERATOR, MemberRole.CREATOR].includes(
|
||||
membership.role
|
||||
)
|
||||
[
|
||||
MemberRole.ADMINISTRATOR,
|
||||
MemberRole.MODERATOR,
|
||||
MemberRole.CREATOR,
|
||||
].includes(membership.role)
|
||||
);
|
||||
}
|
||||
return this.groupMemberships.elements;
|
||||
}
|
||||
|
||||
get actualAvailableActors(): IActor[] {
|
||||
return [this.identity, ...this.actualMemberships.map((member) => member.parent)];
|
||||
return [
|
||||
this.identity,
|
||||
...this.actualMemberships.map((member) => member.parent),
|
||||
];
|
||||
}
|
||||
|
||||
@Watch("currentActor")
|
||||
|
@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="organizer-picker">
|
||||
<!-- If we have a current actor (inline) -->
|
||||
<div v-if="inline && currentActor.id" class="inline box" @click="isComponentModalActive = true">
|
||||
<div
|
||||
v-if="inline && currentActor.id"
|
||||
class="inline box"
|
||||
@click="isComponentModalActive = true"
|
||||
>
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="currentActor.avatar">
|
||||
@ -15,7 +19,9 @@
|
||||
</div>
|
||||
<div class="media-content" v-if="currentActor.name">
|
||||
<p class="is-4">{{ currentActor.name }}</p>
|
||||
<p class="is-6 has-text-grey">{{ `@${currentActor.preferredUsername}` }}</p>
|
||||
<p class="is-6 has-text-grey">
|
||||
{{ `@${currentActor.preferredUsername}` }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="media-content" v-else>
|
||||
{{ `@${currentActor.preferredUsername}` }}
|
||||
@ -26,7 +32,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- If we have a current actor -->
|
||||
<span v-else-if="currentActor.id" class="block" @click="isComponentModalActive = true">
|
||||
<span
|
||||
v-else-if="currentActor.id"
|
||||
class="block"
|
||||
@click="isComponentModalActive = true"
|
||||
>
|
||||
<img
|
||||
class="image is-48x48"
|
||||
v-if="currentActor.avatar"
|
||||
@ -40,13 +50,19 @@
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="identity.avatar">
|
||||
<img class="image is-rounded" :src="identity.avatar.url" :alt="identity.avatar.alt" />
|
||||
<img
|
||||
class="image is-rounded"
|
||||
:src="identity.avatar.url"
|
||||
:alt="identity.avatar.alt"
|
||||
/>
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
</div>
|
||||
<div class="media-content" v-if="identity.name">
|
||||
<p class="is-4">{{ identity.name }}</p>
|
||||
<p class="is-6 has-text-grey">{{ `@${identity.preferredUsername}` }}</p>
|
||||
<p class="is-6 has-text-grey">
|
||||
{{ `@${identity.preferredUsername}` }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="media-content" v-else>
|
||||
{{ `@${identity.preferredUsername}` }}
|
||||
@ -74,7 +90,11 @@
|
||||
<div class="column">
|
||||
<div v-if="actorMembersForCurrentActor.length > 0">
|
||||
<p>{{ $t("Add a contact") }}</p>
|
||||
<p class="field" v-for="actor in actorMembersForCurrentActor" :key="actor.id">
|
||||
<p
|
||||
class="field"
|
||||
v-for="actor in actorMembersForCurrentActor"
|
||||
:key="actor.id"
|
||||
>
|
||||
<b-checkbox v-model="actualContacts" :native-value="actor.id">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
@ -89,7 +109,9 @@
|
||||
</div>
|
||||
<div class="media-content" v-if="actor.name">
|
||||
<p class="is-4">{{ actor.name }}</p>
|
||||
<p class="is-6 has-text-grey">{{ `@${actor.preferredUsername}` }}</p>
|
||||
<p class="is-6 has-text-grey">
|
||||
{{ `@${actor.preferredUsername}` }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="media-content" v-else>
|
||||
{{ `@${actor.preferredUsername}` }}
|
||||
@ -115,7 +137,8 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { IActor, IGroup, IMember, IPerson } from "../../types/actor";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
import { IActor, IGroup, IPerson } from "../../types/actor";
|
||||
import OrganizerPicker from "./OrganizerPicker.vue";
|
||||
import { PERSON_MEMBERSHIPS_WITH_MEMBERS } from "../../graphql/actor";
|
||||
import { Paginate } from "../../types/paginate";
|
||||
@ -150,7 +173,8 @@ export default class OrganizerPickerWrapper extends Vue {
|
||||
|
||||
groupMemberships: Paginate<IMember> = { elements: [], total: 0 };
|
||||
|
||||
@Prop({ type: Array, required: false, default: () => [] }) contacts!: IActor[];
|
||||
@Prop({ type: Array, required: false, default: () => [] })
|
||||
contacts!: IActor[];
|
||||
|
||||
actualContacts: (string | undefined)[] = this.contacts.map(({ id }) => id);
|
||||
|
||||
@ -171,7 +195,9 @@ export default class OrganizerPickerWrapper extends Vue {
|
||||
pickActor(): void {
|
||||
this.$emit(
|
||||
"update:contacts",
|
||||
this.actorMembersForCurrentActor.filter(({ id }) => this.actualContacts.includes(id))
|
||||
this.actorMembersForCurrentActor.filter(({ id }) =>
|
||||
this.actualContacts.includes(id)
|
||||
)
|
||||
);
|
||||
this.$emit("input", this.currentActor);
|
||||
this.isComponentModalActive = false;
|
||||
@ -182,7 +208,9 @@ export default class OrganizerPickerWrapper extends Vue {
|
||||
({ parent: { id } }) => id === this.currentActor.id
|
||||
);
|
||||
if (currentMembership) {
|
||||
return currentMembership.parent.members.elements.map(({ actor }) => actor);
|
||||
return currentMembership.parent.members.elements.map(
|
||||
({ actor }: { actor: IActor }) => actor
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
@ -47,8 +47,16 @@ A button to set your participation
|
||||
>
|
||||
</b-dropdown>
|
||||
|
||||
<div v-else-if="participation && participation.role === ParticipantRole.NOT_APPROVED">
|
||||
<b-dropdown aria-role="list" position="is-bottom-left" class="dropdown-disabled">
|
||||
<div
|
||||
v-else-if="
|
||||
participation && participation.role === ParticipantRole.NOT_APPROVED
|
||||
"
|
||||
>
|
||||
<b-dropdown
|
||||
aria-role="list"
|
||||
position="is-bottom-left"
|
||||
class="dropdown-disabled"
|
||||
>
|
||||
<button class="button is-success is-large" type="button" slot="trigger">
|
||||
<b-icon icon="timer-sand-empty" />
|
||||
<template>
|
||||
@ -74,9 +82,17 @@ A button to set your participation
|
||||
<small>{{ $t("Waiting for organization team approval.") }}</small>
|
||||
</div>
|
||||
|
||||
<div v-else-if="participation && participation.role === ParticipantRole.REJECTED">
|
||||
<div
|
||||
v-else-if="
|
||||
participation && participation.role === ParticipantRole.REJECTED
|
||||
"
|
||||
>
|
||||
<span>
|
||||
{{ $t("Unfortunately, your participation request was rejected by the organizers.") }}
|
||||
{{
|
||||
$t(
|
||||
"Unfortunately, your participation request was rejected by the organizers."
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -92,7 +108,11 @@ A button to set your participation
|
||||
<b-icon icon="menu-down" />
|
||||
</button>
|
||||
|
||||
<b-dropdown-item :value="true" aria-role="listitem" @click="joinEvent(currentActor)">
|
||||
<b-dropdown-item
|
||||
:value="true"
|
||||
aria-role="listitem"
|
||||
@click="joinEvent(currentActor)"
|
||||
>
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-32x32" v-if="currentActor.avatar">
|
||||
@ -103,7 +123,8 @@ A button to set your participation
|
||||
<span>
|
||||
{{
|
||||
$t("as {identity}", {
|
||||
identity: currentActor.name || `@${currentActor.preferredUsername}`,
|
||||
identity:
|
||||
currentActor.name || `@${currentActor.preferredUsername}`,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
@ -121,7 +142,10 @@ A button to set your participation
|
||||
</b-dropdown>
|
||||
<b-button
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.EVENT_PARTICIPATE_LOGGED_OUT, params: { uuid: event.uuid } }"
|
||||
:to="{
|
||||
name: RouteName.EVENT_PARTICIPATE_LOGGED_OUT,
|
||||
params: { uuid: event.uuid },
|
||||
}"
|
||||
v-else-if="!participation && hasAnonymousParticipationMethods"
|
||||
type="is-primary"
|
||||
size="is-large"
|
||||
@ -130,7 +154,10 @@ A button to set your participation
|
||||
>
|
||||
<b-button
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT, params: { uuid: event.uuid } }"
|
||||
:to="{
|
||||
name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT,
|
||||
params: { uuid: event.uuid },
|
||||
}"
|
||||
v-else-if="!currentActor.id"
|
||||
type="is-primary"
|
||||
size="is-large"
|
||||
@ -142,8 +169,9 @@ A button to set your participation
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { IParticipant, ParticipantRole } from "../../types/participant.model";
|
||||
import { EventJoinOptions, IEvent } from "../../types/event.model";
|
||||
import { EventJoinOptions, ParticipantRole } from "@/types/enums";
|
||||
import { IParticipant } from "../../types/participant.model";
|
||||
import { IEvent } from "../../types/event.model";
|
||||
import { IPerson, Person } from "../../types/actor";
|
||||
import { CURRENT_ACTOR_CLIENT, IDENTITIES } from "../../graphql/actor";
|
||||
import { CURRENT_USER_CLIENT } from "../../graphql/user";
|
||||
@ -161,7 +189,9 @@ import RouteName from "../../router/name";
|
||||
identities: {
|
||||
query: IDENTITIES,
|
||||
update: ({ identities }) =>
|
||||
identities ? identities.map((identity: IPerson) => new Person(identity)) : [],
|
||||
identities
|
||||
? identities.map((identity: IPerson) => new Person(identity))
|
||||
: [],
|
||||
skip() {
|
||||
return this.currentUser.isLoggedIn === false;
|
||||
},
|
||||
|
@ -59,7 +59,12 @@
|
||||
<a :href="linkedInShareUrl" target="_blank" rel="nofollow noopener"
|
||||
><b-icon icon="linkedin" size="is-large" type="is-primary"
|
||||
/></a>
|
||||
<a :href="diasporaShareUrl" class="diaspora" target="_blank" rel="nofollow noopener">
|
||||
<a
|
||||
:href="diasporaShareUrl"
|
||||
class="diaspora"
|
||||
target="_blank"
|
||||
rel="nofollow noopener"
|
||||
>
|
||||
<span data-v-5e15e80a="" class="icon has-text-primary is-large">
|
||||
<DiasporaLogo alt="diaspora-logo" />
|
||||
</span>
|
||||
@ -67,7 +72,6 @@
|
||||
<a :href="emailShareUrl" target="_blank" rel="nofollow noopener"
|
||||
><b-icon icon="email" size="is-large" type="is-primary"
|
||||
/></a>
|
||||
<!-- TODO: mailto: links are not used anymore, we should provide a popup to redact a message instead -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -76,7 +80,9 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Ref } from "vue-property-decorator";
|
||||
import { IEvent, EventVisibility, EventStatus } from "../../types/event.model";
|
||||
import { EventStatus, EventVisibility } from "@/types/enums";
|
||||
import { IEvent } from "../../types/event.model";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import DiasporaLogo from "../../assets/diaspora-icon.svg?inline";
|
||||
|
||||
@ -88,7 +94,8 @@ import DiasporaLogo from "../../assets/diaspora-icon.svg?inline";
|
||||
export default class ShareEventModal extends Vue {
|
||||
@Prop({ type: Object, required: true }) event!: IEvent;
|
||||
|
||||
@Prop({ type: Boolean, required: false, default: true }) eventCapacityOK!: boolean;
|
||||
@Prop({ type: Boolean, required: false, default: true })
|
||||
eventCapacityOK!: boolean;
|
||||
|
||||
@Ref("eventURLInput") readonly eventURLInput!: any;
|
||||
|
||||
@ -99,13 +106,15 @@ export default class ShareEventModal extends Vue {
|
||||
showCopiedTooltip = false;
|
||||
|
||||
get twitterShareUrl(): string {
|
||||
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(this.event.url)}&text=${
|
||||
this.event.title
|
||||
}`;
|
||||
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(
|
||||
this.event.url
|
||||
)}&text=${this.event.title}`;
|
||||
}
|
||||
|
||||
get facebookShareUrl(): string {
|
||||
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(this.event.url)}`;
|
||||
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
|
||||
this.event.url
|
||||
)}`;
|
||||
}
|
||||
|
||||
get linkedInShareUrl(): string {
|
||||
@ -124,7 +133,7 @@ export default class ShareEventModal extends Vue {
|
||||
)}&url=${encodeURIComponent(this.event.url)}`;
|
||||
}
|
||||
|
||||
copyURL() {
|
||||
copyURL(): void {
|
||||
this.eventURLInput.$refs.input.select();
|
||||
document.execCommand("copy");
|
||||
this.showCopiedTooltip = true;
|
||||
|
@ -4,7 +4,9 @@
|
||||
{{ $t("Add some tags") }}
|
||||
<b-tooltip
|
||||
type="is-dark"
|
||||
:label="$t('You can add tags by hitting the Enter key or by adding a comma')"
|
||||
:label="
|
||||
$t('You can add tags by hitting the Enter key or by adding a comma')
|
||||
"
|
||||
>
|
||||
<b-icon size="is-small" icon="help-circle-outline"></b-icon>
|
||||
</b-tooltip>
|
||||
@ -40,7 +42,6 @@ import { ITag } from "../../types/tag.model";
|
||||
if (typeof tag !== "string") {
|
||||
return tag;
|
||||
}
|
||||
// @ts-ignore
|
||||
return { title: tag, slug: tag } as ITag;
|
||||
});
|
||||
this.$emit("input", tagEntities);
|
||||
@ -57,14 +58,14 @@ export default class TagInput extends Vue {
|
||||
|
||||
filteredTags: ITag[] = [];
|
||||
|
||||
getFilteredTags(text: string) {
|
||||
getFilteredTags(text: string): void {
|
||||
this.filteredTags = differenceBy(this.data, this.value, "id").filter(
|
||||
(option) => get(option, this.path).toString().toLowerCase().indexOf(text.toLowerCase()) >= 0
|
||||
(option) =>
|
||||
get(option, this.path)
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.indexOf(text.toLowerCase()) >= 0
|
||||
);
|
||||
}
|
||||
|
||||
static isTag(x: any): x is ITag {
|
||||
return x.slug !== undefined;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,22 +1,53 @@
|
||||
<template>
|
||||
<footer class="footer" ref="footer">
|
||||
|
||||
<!-- <img :src="`/img/pics/footer_${random}.jpg`" alt="" />-->
|
||||
<a href="https://openstreetmap.fr">
|
||||
<h1 class="title is-1">
|
||||
|
||||
OpenStreetMap.fr
|
||||
</h1>
|
||||
<h1 class="title is-1">OpenStreetMap.fr</h1>
|
||||
</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>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.ABOUT }">{{ $t("About") }}</router-link>
|
||||
<b-select
|
||||
v-if="$i18n"
|
||||
v-model="locale"
|
||||
:placeholder="$t('Select a language')"
|
||||
>
|
||||
<option v-for="(language, lang) in langs" :value="lang" :key="lang">
|
||||
{{ language }}
|
||||
</option>
|
||||
</b-select>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.TERMS }">{{ $t("Terms") }}</router-link>
|
||||
<router-link :to="{ name: RouteName.ABOUT }">{{
|
||||
$t("About")
|
||||
}}</router-link>
|
||||
</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") }}
|
||||
</a>
|
||||
</li>
|
||||
@ -26,33 +57,57 @@
|
||||
tag="span"
|
||||
path="Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}."
|
||||
>
|
||||
<a slot="mobilizon" href="https://joinmobilizon.org">{{ $t("Mobilizon") }}</a>
|
||||
<a slot="mobilizon" href="https://joinmobilizon.org">{{
|
||||
$t("Mobilizon")
|
||||
}}</a>
|
||||
<span slot="date">{{ new Date().getFullYear() }}</span>
|
||||
<a href="https://joinmobilizon.org/hall-of-fame" slot="contributors">{{
|
||||
$t("more than 1360 contributors")
|
||||
}}</a>
|
||||
$t("more than 1360 contributors")
|
||||
}}</a>
|
||||
</i18n>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {Component, Vue} from "vue-property-decorator";
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import { saveLocaleData } from "@/utils/auth";
|
||||
import { loadLanguageAsync } from "@/utils/i18n";
|
||||
import RouteName from "../router/name";
|
||||
import langs from "../i18n/langs.json";
|
||||
|
||||
@Component
|
||||
export default class Footer extends Vue {
|
||||
RouteName = RouteName;
|
||||
|
||||
locale: string | null = this.$i18n.locale;
|
||||
|
||||
langs: Record<string, string> = langs;
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get random(): number {
|
||||
return Math.floor(Math.random() * 4) + 1;
|
||||
}
|
||||
|
||||
@Watch("locale")
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async updateLocale(locale: string): Promise<void> {
|
||||
if (locale) {
|
||||
await loadLanguageAsync(locale);
|
||||
saveLocaleData(locale);
|
||||
}
|
||||
}
|
||||
|
||||
@Watch("$i18n.locale", { deep: true })
|
||||
updateLocaleFromI18n(locale: string): void {
|
||||
if (locale) {
|
||||
this.locale = locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "~bulma/sass/utilities/mixins.sass";
|
||||
|
||||
|
||||
footer.footer {
|
||||
color: white;
|
||||
background: $primary;
|
||||
@ -95,5 +150,10 @@ footer.footer {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: $secondary;
|
||||
}
|
||||
|
||||
::v-deep span.select select {
|
||||
background: $background-color;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -17,7 +17,9 @@
|
||||
>
|
||||
<h3>{{ group.name }}</h3>
|
||||
<p class="is-6 has-text-grey">
|
||||
<span v-if="group.domain">{{ `@${group.preferredUsername}@${group.domain}` }}</span>
|
||||
<span v-if="group.domain">{{
|
||||
`@${group.preferredUsername}@${group.domain}`
|
||||
}}</span>
|
||||
<span v-else>{{ `@${group.preferredUsername}` }}</span>
|
||||
</p>
|
||||
</router-link>
|
||||
|
@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="identity-header">
|
||||
<figure class="image is-24x24" v-if="member.actor.avatar">
|
||||
<img class="is-rounded" :src="member.actor.avatar.url" alt="" />
|
||||
</figure>
|
||||
{{ displayNameAndUsername(member.actor) }}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
<div class="media">
|
||||
@ -13,7 +19,9 @@
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(member.parent) },
|
||||
params: {
|
||||
preferredUsername: usernameWithDomain(member.parent),
|
||||
},
|
||||
}"
|
||||
>
|
||||
<h3>{{ member.parent.name }}</h3>
|
||||
@ -23,12 +31,16 @@
|
||||
}}</span>
|
||||
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
|
||||
<b-taglist>
|
||||
<b-tag type="is-info" v-if="member.role === MemberRole.ADMINISTRATOR">{{
|
||||
$t("Administrator")
|
||||
}}</b-tag>
|
||||
<b-tag type="is-info" v-else-if="member.role === MemberRole.MODERATOR">{{
|
||||
$t("Moderator")
|
||||
}}</b-tag>
|
||||
<b-tag
|
||||
type="is-info"
|
||||
v-if="member.role === MemberRole.ADMINISTRATOR"
|
||||
>{{ $t("Administrator") }}</b-tag
|
||||
>
|
||||
<b-tag
|
||||
type="is-info"
|
||||
v-else-if="member.role === MemberRole.MODERATOR"
|
||||
>{{ $t("Moderator") }}</b-tag
|
||||
>
|
||||
</b-taglist>
|
||||
</p>
|
||||
</router-link>
|
||||
@ -54,7 +66,9 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { IMember, MemberRole, usernameWithDomain } from "@/types/actor";
|
||||
import { displayNameAndUsername, usernameWithDomain } from "@/types/actor";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component
|
||||
@ -65,20 +79,34 @@ export default class GroupMemberCard extends Vue {
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
displayNameAndUsername = displayNameAndUsername;
|
||||
|
||||
MemberRole = MemberRole;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.card {
|
||||
.card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > div:first-child {
|
||||
flex: 1;
|
||||
& > div:first-child {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
& > div:last-child {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
& > div:last-child {
|
||||
cursor: pointer;
|
||||
.identity-header {
|
||||
background: $yellow-2;
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
|
||||
figure {
|
||||
padding-right: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -8,7 +8,9 @@
|
||||
<a
|
||||
class="list-item"
|
||||
v-for="groupMembership in actualMemberships"
|
||||
:class="{ 'is-active': groupMembership.parent.id === currentGroup.id }"
|
||||
:class="{
|
||||
'is-active': groupMembership.parent.id === currentGroup.id,
|
||||
}"
|
||||
@click="changeCurrentGroup(groupMembership.parent)"
|
||||
:key="groupMembership.id"
|
||||
>
|
||||
@ -19,14 +21,25 @@
|
||||
:src="groupMembership.parent.avatar.url"
|
||||
alt=""
|
||||
/>
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
<b-icon
|
||||
class="media-left"
|
||||
v-else
|
||||
size="is-large"
|
||||
icon="account-circle"
|
||||
/>
|
||||
<div class="media-content">
|
||||
<h3>@{{ groupMembership.parent.name }}</h3>
|
||||
<small>{{ `@${groupMembership.parent.preferredUsername}` }}</small>
|
||||
<small>{{
|
||||
`@${groupMembership.parent.preferredUsername}`
|
||||
}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="list-item" @click="changeCurrentGroup(new Group())" v-if="currentGroup.id">
|
||||
<a
|
||||
class="list-item"
|
||||
@click="changeCurrentGroup(new Group())"
|
||||
v-if="currentGroup.id"
|
||||
>
|
||||
<h3>{{ $t("Unset group") }}</h3>
|
||||
</a>
|
||||
</div>
|
||||
@ -36,9 +49,11 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { IGroup, IMember, IPerson, Group, MemberRole } from "@/types/actor";
|
||||
import { IGroup, IPerson, Group } from "@/types/actor";
|
||||
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@ -77,9 +92,11 @@ export default class GroupPicker extends Vue {
|
||||
get actualMemberships(): IMember[] {
|
||||
if (this.restrictModeratorLevel) {
|
||||
return this.groupMemberships.elements.filter((membership: IMember) =>
|
||||
[MemberRole.ADMINISTRATOR, MemberRole.MODERATOR, MemberRole.CREATOR].includes(
|
||||
membership.role
|
||||
)
|
||||
[
|
||||
MemberRole.ADMINISTRATOR,
|
||||
MemberRole.MODERATOR,
|
||||
MemberRole.CREATOR,
|
||||
].includes(membership.role)
|
||||
);
|
||||
}
|
||||
return this.groupMemberships.elements;
|
||||
|
@ -10,7 +10,11 @@
|
||||
{{ $t("The event will show the group as organizer.") }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="inline && currentGroup.id" class="inline box" @click="isComponentModalActive = true">
|
||||
<div
|
||||
v-if="inline && currentGroup.id"
|
||||
class="inline box"
|
||||
@click="isComponentModalActive = true"
|
||||
>
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="currentGroup.avatar">
|
||||
@ -24,7 +28,9 @@
|
||||
</div>
|
||||
<div class="media-content" v-if="currentGroup.name">
|
||||
<p class="is-4">{{ currentGroup.name }}</p>
|
||||
<p class="is-6 has-text-grey">{{ `@${currentGroup.preferredUsername}` }}</p>
|
||||
<p class="is-6 has-text-grey">
|
||||
{{ `@${currentGroup.preferredUsername}` }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="media-content" v-else>
|
||||
{{ `@${currentGroup.preferredUsername}` }}
|
||||
@ -34,7 +40,11 @@
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else-if="currentGroup.id" class="block" @click="isComponentModalActive = true">
|
||||
<span
|
||||
v-else-if="currentGroup.id"
|
||||
class="block"
|
||||
@click="isComponentModalActive = true"
|
||||
>
|
||||
<img
|
||||
class="image is-48x48"
|
||||
v-if="currentGroup.avatar"
|
||||
@ -44,7 +54,9 @@
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
</span>
|
||||
<div v-if="groupMemberships.total === 0" class="box">
|
||||
<p class="is-4">{{ $t("This identity is not a member of any group.") }}</p>
|
||||
<p class="is-4">
|
||||
{{ $t("This identity is not a member of any group.") }}
|
||||
</p>
|
||||
<p class="is-6 is-size-6 has-text-grey">
|
||||
{{ $t("You need to create the group before you create an event.") }}
|
||||
</p>
|
||||
@ -61,7 +73,8 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { IGroup, IMember, IPerson } from "../../types/actor";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
import { IGroup, IPerson } from "../../types/actor";
|
||||
import GroupPicker from "./GroupPicker.vue";
|
||||
import { PERSON_MEMBERSHIPS } from "../../graphql/actor";
|
||||
import { Paginate } from "../../types/paginate";
|
||||
|
@ -26,7 +26,8 @@ export default class GroupSection extends Vue {
|
||||
|
||||
@Prop({ required: true, type: String }) icon!: string;
|
||||
|
||||
@Prop({ required: false, type: Boolean, default: true }) privateSection!: boolean;
|
||||
@Prop({ required: false, type: Boolean, default: true })
|
||||
privateSection!: boolean;
|
||||
|
||||
@Prop({ required: true, type: Object }) route!: Route;
|
||||
}
|
||||
@ -76,7 +77,8 @@ div.group-section-title {
|
||||
::v-deep span {
|
||||
display: inline;
|
||||
padding: 3px 8px;
|
||||
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
|
||||
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
|
||||
serif;
|
||||
font-weight: 500;
|
||||
font-size: 30px;
|
||||
flex: 1;
|
||||
|
@ -2,7 +2,10 @@
|
||||
<div class="media">
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<i18n tag="p" path="You have been invited by {invitedBy} to the following group:">
|
||||
<i18n
|
||||
tag="p"
|
||||
path="You have been invited by {invitedBy} to the following group:"
|
||||
>
|
||||
<b slot="invitedBy">{{ member.invitedBy.name }}</b>
|
||||
</i18n>
|
||||
</div>
|
||||
@ -20,15 +23,21 @@
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(member.parent) },
|
||||
params: {
|
||||
preferredUsername: usernameWithDomain(member.parent),
|
||||
},
|
||||
}"
|
||||
>
|
||||
<h3>{{ member.parent.name }}</h3>
|
||||
<p class="is-6 has-text-grey">
|
||||
<span v-if="member.parent.domain">
|
||||
{{ `@${member.parent.preferredUsername}@${member.parent.domain}` }}
|
||||
{{
|
||||
`@${member.parent.preferredUsername}@${member.parent.domain}`
|
||||
}}
|
||||
</span>
|
||||
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
|
||||
<span v-else>{{
|
||||
`@${member.parent.preferredUsername}`
|
||||
}}</span>
|
||||
</p>
|
||||
</router-link>
|
||||
</div>
|
||||
@ -54,7 +63,8 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { IMember, usernameWithDomain } from "@/types/actor";
|
||||
import { usernameWithDomain } from "@/types/actor";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component
|
||||
|
@ -11,10 +11,10 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
|
||||
import { IMember } from "@/types/actor";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import InvitationCard from "@/components/Group/InvitationCard.vue";
|
||||
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@ -26,13 +26,15 @@ export default class Invitations extends Vue {
|
||||
|
||||
async acceptInvitation(id: string): Promise<void> {
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>({
|
||||
mutation: ACCEPT_INVITATION,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
|
||||
});
|
||||
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>(
|
||||
{
|
||||
mutation: ACCEPT_INVITATION,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
|
||||
}
|
||||
);
|
||||
if (data) {
|
||||
this.$emit("accept-invitation", data.acceptInvitation);
|
||||
}
|
||||
@ -46,13 +48,15 @@ export default class Invitations extends Vue {
|
||||
|
||||
async rejectInvitation(id: string): Promise<void> {
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>({
|
||||
mutation: REJECT_INVITATION,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
|
||||
});
|
||||
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>(
|
||||
{
|
||||
mutation: REJECT_INVITATION,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
|
||||
}
|
||||
);
|
||||
if (data) {
|
||||
this.$emit("reject-invitation", data.rejectInvitation);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -1,18 +1,36 @@
|
||||
<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>
|
||||
|
||||
<script lang="ts">
|
||||
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({
|
||||
components: {
|
||||
MobilizonLogo,
|
||||
},
|
||||
})
|
||||
@Component
|
||||
export default class Logo extends Vue {
|
||||
@Prop({ type: Boolean, required: false, default: false }) invert!: boolean;
|
||||
}
|
||||
|
@ -8,7 +8,11 @@
|
||||
@click="clickMap"
|
||||
@update:zoom="updateZoom"
|
||||
>
|
||||
<l-tile-layer :url="config.maps.tiles.endpoint" :attribution="attribution"> </l-tile-layer>
|
||||
<l-tile-layer
|
||||
:url="config.maps.tiles.endpoint"
|
||||
:attribution="attribution"
|
||||
>
|
||||
</l-tile-layer>
|
||||
<v-locatecontrol :options="{ icon: 'mdi mdi-map-marker' }" />
|
||||
<l-marker
|
||||
:lat-lng="[lat, lon]"
|
||||
@ -17,7 +21,9 @@
|
||||
:draggable="!readOnly"
|
||||
>
|
||||
<l-popup v-if="popupMultiLine">
|
||||
<span v-for="line in popupMultiLine" :key="line">{{ line }}<br /></span>
|
||||
<span v-for="line in popupMultiLine" :key="line"
|
||||
>{{ line }}<br
|
||||
/></span>
|
||||
</l-popup>
|
||||
</l-marker>
|
||||
</l-map>
|
||||
@ -51,12 +57,15 @@ export default class Map extends Vue {
|
||||
|
||||
@Prop({ type: String, required: true }) coords!: string;
|
||||
|
||||
@Prop({ type: Object, required: false }) marker!: { text: string | string[]; icon: string };
|
||||
@Prop({ type: Object, required: false }) marker!: {
|
||||
text: string | string[];
|
||||
icon: string;
|
||||
};
|
||||
|
||||
@Prop({ type: Object, required: false }) options!: object;
|
||||
@Prop({ type: Object, required: false }) options!: Record<string, unknown>;
|
||||
|
||||
@Prop({ type: Function, required: false })
|
||||
updateDraggableMarkerCallback!: Function;
|
||||
updateDraggableMarkerCallback!: (latlng: LatLng, zoom: number) => void;
|
||||
|
||||
defaultOptions: {
|
||||
zoom: number;
|
||||
@ -86,45 +95,48 @@ export default class Map extends Vue {
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
openPopup(event: LeafletEvent) {
|
||||
openPopup(event: LeafletEvent): void {
|
||||
this.$nextTick(() => {
|
||||
event.target.openPopup();
|
||||
});
|
||||
}
|
||||
|
||||
get mergedOptions(): object {
|
||||
get mergedOptions(): Record<string, unknown> {
|
||||
return { ...this.defaultOptions, ...this.options };
|
||||
}
|
||||
|
||||
get lat() {
|
||||
get lat(): number {
|
||||
return this.$props.coords.split(";")[1];
|
||||
}
|
||||
|
||||
get lon() {
|
||||
get lon(): number {
|
||||
return this.$props.coords.split(";")[0];
|
||||
}
|
||||
|
||||
get popupMultiLine() {
|
||||
get popupMultiLine(): Array<string> {
|
||||
if (Array.isArray(this.marker.text)) {
|
||||
return this.marker.text;
|
||||
}
|
||||
return [this.marker.text];
|
||||
}
|
||||
|
||||
clickMap(event: LeafletMouseEvent) {
|
||||
clickMap(event: LeafletMouseEvent): void {
|
||||
this.updateDraggableMarkerPosition(event.latlng);
|
||||
}
|
||||
|
||||
updateDraggableMarkerPosition(e: LatLng) {
|
||||
updateDraggableMarkerPosition(e: LatLng): void {
|
||||
this.updateDraggableMarkerCallback(e, this.zoom);
|
||||
}
|
||||
|
||||
updateZoom(zoom: number) {
|
||||
updateZoom(zoom: number): void {
|
||||
this.zoom = zoom;
|
||||
}
|
||||
|
||||
get attribution() {
|
||||
return this.config.maps.tiles.attribution || this.$t("© The OpenStreetMap Contributors");
|
||||
get attribution(): string {
|
||||
return (
|
||||
this.config.maps.tiles.attribution ||
|
||||
(this.$t("© The OpenStreetMap Contributors") as string)
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -17,13 +17,16 @@ import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
|
||||
@Component({
|
||||
beforeDestroy() {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.parentContainer.removeLayer(this);
|
||||
},
|
||||
})
|
||||
export default class Vue2LeafletLocateControl extends Vue {
|
||||
@Prop({ type: Object, default: () => ({}) }) options!: object;
|
||||
@Prop({ type: Object, default: () => ({}) }) options!: Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
@Prop({ type: Boolean, default: true }) visible = true;
|
||||
|
||||
@ -33,7 +36,7 @@ export default class Vue2LeafletLocateControl extends Vue {
|
||||
|
||||
parentContainer: any;
|
||||
|
||||
mounted() {
|
||||
mounted(): void {
|
||||
this.mapObject = L.control.locate(this.options);
|
||||
DomEvent.on(this.mapObject, this.$listeners as any);
|
||||
propsBinder(this, this.mapObject, this.$props);
|
||||
@ -42,7 +45,7 @@ export default class Vue2LeafletLocateControl extends Vue {
|
||||
this.mapObject.addTo(this.parentContainer.mapObject, !this.visible);
|
||||
}
|
||||
|
||||
public locate() {
|
||||
public locate(): void {
|
||||
this.mapObject.start();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,15 @@
|
||||
<template>
|
||||
<b-navbar type="is-secondary" wrapper-class="container" :active.sync="mobileNavbarActive">
|
||||
<b-navbar
|
||||
type="is-secondary"
|
||||
wrapper-class="container"
|
||||
:active.sync="mobileNavbarActive"
|
||||
>
|
||||
<template slot="brand">
|
||||
<b-navbar-item tag="router-link" :to="{ name: RouteName.HOME }" :aria-label="$t('Home')">
|
||||
<b-navbar-item
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.HOME }"
|
||||
:aria-label="$t('Home')"
|
||||
>
|
||||
<logo />
|
||||
</b-navbar-item>
|
||||
</template>
|
||||
@ -19,9 +27,12 @@
|
||||
>{{ $t("My groups") }}
|
||||
</b-navbar-item>
|
||||
<b-navbar-item tag="span" v-if="config && config.features.eventCreation">
|
||||
<b-button tag="router-link" :to="{ name: RouteName.CREATE_EVENT }" type="is-primary"
|
||||
>{{ $t("Create") }}
|
||||
</b-button>
|
||||
<b-button
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.CREATE_EVENT }"
|
||||
type="is-primary"
|
||||
>{{ $t("Create") }}</b-button
|
||||
>
|
||||
</b-navbar-item>
|
||||
</template>
|
||||
<template slot="end">
|
||||
@ -30,9 +41,17 @@
|
||||
</b-navbar-item>
|
||||
|
||||
<b-navbar-dropdown v-if="currentActor.id && currentUser.isLoggedIn" right>
|
||||
<template slot="label" v-if="currentActor" class="navbar-dropdown-profile">
|
||||
<template
|
||||
slot="label"
|
||||
v-if="currentActor"
|
||||
class="navbar-dropdown-profile"
|
||||
>
|
||||
<figure class="image is-32x32" v-if="currentActor.avatar">
|
||||
<img class="is-rounded" alt="avatarUrl" :src="currentActor.avatar.url" />
|
||||
<img
|
||||
class="is-rounded"
|
||||
alt="avatarUrl"
|
||||
:src="currentActor.avatar.url"
|
||||
/>
|
||||
</figure>
|
||||
<b-icon v-else icon="account-circle" />
|
||||
</template>
|
||||
@ -65,9 +84,11 @@
|
||||
<hr class="navbar-divider" />
|
||||
</b-navbar-item>
|
||||
|
||||
<b-navbar-item tag="router-link" :to="{ name: RouteName.UPDATE_IDENTITY }"
|
||||
>{{ $t("My account") }}
|
||||
</b-navbar-item>
|
||||
<b-navbar-item
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.UPDATE_IDENTITY }"
|
||||
>{{ $t("My account") }}</b-navbar-item
|
||||
>
|
||||
|
||||
<!-- <b-navbar-item tag="router-link" :to="{ name: RouteName.CREATE_GROUP }">-->
|
||||
<!-- {{ $t('Create group') }}-->
|
||||
@ -95,9 +116,11 @@
|
||||
<strong>{{ $t("Sign up") }}</strong>
|
||||
</router-link>
|
||||
|
||||
<router-link class="button is-light" :to="{ name: RouteName.LOGIN }"
|
||||
>{{ $t("Log in") }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="button is-light"
|
||||
:to="{ name: RouteName.LOGIN }"
|
||||
>{{ $t("Log in") }}</router-link
|
||||
>
|
||||
</div>
|
||||
</b-navbar-item>
|
||||
</template>
|
||||
@ -109,13 +132,18 @@ import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import Logo from "@/components/Logo.vue";
|
||||
import { GraphQLError } from "graphql";
|
||||
import { loadLanguageAsync } from "@/utils/i18n";
|
||||
import { ICurrentUserRole } from "@/types/enums";
|
||||
import { CURRENT_USER_CLIENT, USER_SETTINGS } from "../graphql/user";
|
||||
import { changeIdentity, logout } from "../utils/auth";
|
||||
import { CURRENT_ACTOR_CLIENT, IDENTITIES, UPDATE_DEFAULT_ACTOR } from "../graphql/actor";
|
||||
import {
|
||||
CURRENT_ACTOR_CLIENT,
|
||||
IDENTITIES,
|
||||
UPDATE_DEFAULT_ACTOR,
|
||||
} from "../graphql/actor";
|
||||
import { IPerson, Person } from "../types/actor";
|
||||
import { CONFIG } from "../graphql/config";
|
||||
import { IConfig } from "../types/config.model";
|
||||
import { ICurrentUser, ICurrentUserRole, IUser } from "../types/current-user.model";
|
||||
import { ICurrentUser, IUser } from "../types/current-user.model";
|
||||
import SearchField from "./SearchField.vue";
|
||||
import RouteName from "../router/name";
|
||||
|
||||
@ -130,7 +158,9 @@ import RouteName from "../router/name";
|
||||
identities: {
|
||||
query: IDENTITIES,
|
||||
update: ({ identities }) =>
|
||||
identities ? identities.map((identity: IPerson) => new Person(identity)) : [],
|
||||
identities
|
||||
? identities.map((identity: IPerson) => new Person(identity))
|
||||
: [],
|
||||
skip() {
|
||||
return this.currentUser.isLoggedIn === false;
|
||||
},
|
||||
@ -201,7 +231,8 @@ export default class NavBar extends Vue {
|
||||
async handleErrors(errors: GraphQLError[]): Promise<void> {
|
||||
if (
|
||||
errors.length > 0 &&
|
||||
errors[0].message === "You need to be logged-in to view your list of identities"
|
||||
errors[0].message ===
|
||||
"You need to be logged-in to view your list of identities"
|
||||
) {
|
||||
await this.logout();
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
<template>
|
||||
<section class="section container">
|
||||
<h1 class="title" v-if="loading">{{ $t("Your participation request is being validated") }}</h1>
|
||||
<h1 class="title" v-if="loading">
|
||||
{{ $t("Your participation request is being validated") }}
|
||||
</h1>
|
||||
<div v-else>
|
||||
<div v-if="failed">
|
||||
<b-message :title="$t('Error while validating participation request')" type="is-danger">
|
||||
<b-message
|
||||
:title="$t('Error while validating participation request')"
|
||||
type="is-danger"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
"Either the participation request has already been validated, either the validation token is incorrect."
|
||||
@ -12,9 +17,16 @@
|
||||
</b-message>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 class="title">{{ $t("Your participation request has been validated") }}</h1>
|
||||
<p class="content" v-if="participation.event.joinOptions == EventJoinOptions.RESTRICTED">
|
||||
{{ $t("Your participation still has to be approved by the organisers.") }}
|
||||
<h1 class="title">
|
||||
{{ $t("Your participation request has been validated") }}
|
||||
</h1>
|
||||
<p
|
||||
class="content"
|
||||
v-if="participation.event.joinOptions == EventJoinOptions.RESTRICTED"
|
||||
>
|
||||
{{
|
||||
$t("Your participation still has to be approved by the organisers.")
|
||||
}}
|
||||
</p>
|
||||
<div class="columns has-text-centered">
|
||||
<div class="column">
|
||||
@ -38,9 +50,9 @@
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { confirmLocalAnonymousParticipation } from "@/services/AnonymousParticipationStorage";
|
||||
import { EventJoinOptions } from "@/types/enums";
|
||||
import { IParticipant } from "../../types/participant.model";
|
||||
import RouteName from "../../router/name";
|
||||
import { EventJoinOptions } from "../../types/event.model";
|
||||
import { CONFIRM_PARTICIPATION } from "../../graphql/event";
|
||||
|
||||
@Component
|
||||
|
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>
|
||||
<redirect-with-account :uri="uri" :pathAfterLogin="`/events/${uuid}`" :sentence="sentence" />
|
||||
<redirect-with-account
|
||||
:uri="uri"
|
||||
:pathAfterLogin="`/events/${uuid}`"
|
||||
:sentence="sentence"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
@ -21,6 +25,8 @@ export default class ParticipationWithAccount extends Vue {
|
||||
}`;
|
||||
}
|
||||
|
||||
sentence = this.$t("We will redirect you to your instance in order to interact with this event");
|
||||
sentence = this.$t(
|
||||
"We will redirect you to your instance in order to interact with this event"
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
@ -33,7 +33,13 @@
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p v-else>{{ $t("If you want, you may send a message to the event organizer here.") }}</p>
|
||||
<p v-else>
|
||||
{{
|
||||
$t(
|
||||
"If you want, you may send a message to the event organizer here."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<b-field :label="$t('Message')">
|
||||
<b-input
|
||||
type="textarea"
|
||||
@ -54,42 +60,93 @@
|
||||
</p>
|
||||
</b-checkbox>
|
||||
</b-field>
|
||||
<b-button :disabled="sendingForm" type="is-primary" native-type="submit">{{
|
||||
$t("Send email")
|
||||
}}</b-button>
|
||||
<b-button
|
||||
:disabled="sendingForm"
|
||||
type="is-primary"
|
||||
native-type="submit"
|
||||
>{{ $t("Send email") }}</b-button
|
||||
>
|
||||
<div class="has-text-centered">
|
||||
<b-button native-type="button" tag="a" type="is-text" @click="$router.go(-1)">{{
|
||||
$t("Back to previous page")
|
||||
}}</b-button>
|
||||
<b-button
|
||||
native-type="button"
|
||||
tag="a"
|
||||
type="is-text"
|
||||
@click="$router.go(-1)"
|
||||
>{{ $t("Back to previous page") }}</b-button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
<h1 class="title">{{ $t("Request for participation confirmation sent") }}</h1>
|
||||
<p class="content">{{ $t("Check your inbox (and your junk mail folder).") }}</p>
|
||||
<p class="content">{{ $t("You may now close this window.") }}</p>
|
||||
<h1 class="title">
|
||||
{{ $t("Request for participation confirmation sent") }}
|
||||
</h1>
|
||||
<p class="content">
|
||||
<span>{{
|
||||
$t("Check your inbox (and your junk mail folder).")
|
||||
}}</span>
|
||||
<span
|
||||
class="details"
|
||||
v-if="event.joinOptions === EventJoinOptions.RESTRICTED"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
"Your participation will be validated once you click the confirmation link into the email, and after the organizer manually validates your participation."
|
||||
)
|
||||
}} </span
|
||||
><span class="details" v-else>{{
|
||||
$t(
|
||||
"Your participation will be validated once you click the confirmation link into the email."
|
||||
)
|
||||
}}</span>
|
||||
</p>
|
||||
<b-message type="is-warning" v-if="error">{{ error }}</b-message>
|
||||
<p class="content">
|
||||
<i18n path="You may now close this window, or {return_to_event}.">
|
||||
<router-link
|
||||
slot="return_to_event"
|
||||
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
||||
>{{ $t("return to the event's page") }}</router-link
|
||||
>
|
||||
</i18n>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<b-message type="is-danger" v-else-if="!$apollo.loading"
|
||||
>{{
|
||||
$t(
|
||||
"Unable to load event for participation. The error details are provided below:"
|
||||
)
|
||||
}}
|
||||
<details>
|
||||
<pre>{{ error }}</pre>
|
||||
</details>
|
||||
</b-message>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { EventModel, IEvent, EventJoinOptions } from "@/types/event.model";
|
||||
import { FETCH_EVENT, JOIN_EVENT } from "@/graphql/event";
|
||||
import { EventModel, IEvent } from "@/types/event.model";
|
||||
import { FETCH_EVENT_BASIC, JOIN_EVENT } from "@/graphql/event";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import { CONFIG } from "@/graphql/config";
|
||||
import { addLocalUnconfirmedAnonymousParticipation } from "@/services/AnonymousParticipationStorage";
|
||||
import { IParticipant, ParticipantRole } from "../../types/participant.model";
|
||||
import { EventJoinOptions, ParticipantRole } from "@/types/enums";
|
||||
import RouteName from "@/router/name";
|
||||
import { IParticipant } from "../../types/participant.model";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
event: {
|
||||
query: FETCH_EVENT,
|
||||
query: FETCH_EVENT_BASIC,
|
||||
variables() {
|
||||
return {
|
||||
uuid: this.uuid,
|
||||
};
|
||||
},
|
||||
error(e) {
|
||||
this.error = e;
|
||||
},
|
||||
skip() {
|
||||
return !this.uuid;
|
||||
},
|
||||
@ -101,7 +158,11 @@ import { IParticipant, ParticipantRole } from "../../types/participant.model";
|
||||
export default class ParticipationWithoutAccount extends Vue {
|
||||
@Prop({ type: String, required: true }) uuid!: string;
|
||||
|
||||
anonymousParticipation: { email: string; message: string; saveParticipation: boolean } = {
|
||||
anonymousParticipation: {
|
||||
email: string;
|
||||
message: string;
|
||||
saveParticipation: boolean;
|
||||
} = {
|
||||
email: "",
|
||||
message: "",
|
||||
saveParticipation: true,
|
||||
@ -119,6 +180,8 @@ export default class ParticipationWithoutAccount extends Vue {
|
||||
|
||||
EventJoinOptions = EventJoinOptions;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
async joinEvent(): Promise<void> {
|
||||
this.error = false;
|
||||
this.sendingForm = true;
|
||||
@ -134,21 +197,27 @@ export default class ParticipationWithoutAccount extends Vue {
|
||||
},
|
||||
update: (store, { data: updateData }) => {
|
||||
if (updateData == null) {
|
||||
console.error("Cannot update event participant cache, because of data null value.");
|
||||
console.error(
|
||||
"Cannot update event participant cache, because of data null value."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedData = store.readQuery<{ event: IEvent }>({
|
||||
query: FETCH_EVENT,
|
||||
query: FETCH_EVENT_BASIC,
|
||||
variables: { uuid: this.event.uuid },
|
||||
});
|
||||
if (cachedData == null) {
|
||||
console.error("Cannot update event participant cache, because of cached null value.");
|
||||
console.error(
|
||||
"Cannot update event participant cache, because of cached null value."
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { event } = cachedData;
|
||||
if (event === null) {
|
||||
console.error("Cannot update event participant cache, because of null value.");
|
||||
console.error(
|
||||
"Cannot update event participant cache, because of null value."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -158,16 +227,15 @@ export default class ParticipationWithoutAccount extends Vue {
|
||||
event.participantStats.going += 1;
|
||||
event.participantStats.participant += 1;
|
||||
}
|
||||
console.log("just before writequery");
|
||||
|
||||
store.writeQuery({
|
||||
query: FETCH_EVENT,
|
||||
query: FETCH_EVENT_BASIC,
|
||||
variables: { uuid: this.event.uuid },
|
||||
data: { event },
|
||||
});
|
||||
},
|
||||
});
|
||||
console.log("finished with store", data);
|
||||
this.formSent = true;
|
||||
if (
|
||||
data &&
|
||||
data.joinEvent.metadata.cancellationToken &&
|
||||
@ -177,13 +245,28 @@ export default class ParticipationWithoutAccount extends Vue {
|
||||
this.event,
|
||||
data.joinEvent.metadata.cancellationToken
|
||||
);
|
||||
console.log("done with crypto stuff");
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
if (
|
||||
["TextEncoder is not defined", "crypto.subtle is undefined"].includes(
|
||||
e.message
|
||||
)
|
||||
) {
|
||||
this.error = this.$t(
|
||||
"Unable to save your participation in this browser."
|
||||
) as string;
|
||||
} else if (e.graphQLErrors && e.graphQLErrors.length > 0) {
|
||||
this.error = e.graphQLErrors[0].message;
|
||||
} else if (e.networkError) {
|
||||
this.error = e.networkError.message;
|
||||
}
|
||||
}
|
||||
this.sendingForm = false;
|
||||
this.formSent = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
section.container.section {
|
||||
background: $white;
|
||||
}
|
||||
</style>
|
||||
|
@ -2,22 +2,34 @@
|
||||
<section class="section container hero">
|
||||
<div class="hero-body" v-if="event">
|
||||
<div class="container">
|
||||
<subtitle>{{ $t("You wish to participate to the following event") }}</subtitle>
|
||||
<subtitle>{{
|
||||
$t("You wish to participate to the following event")
|
||||
}}</subtitle>
|
||||
<EventListViewCard v-if="event" :event="event" />
|
||||
<div class="columns has-text-centered">
|
||||
<div class="column">
|
||||
<router-link :to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT }">
|
||||
<router-link
|
||||
:to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT }"
|
||||
>
|
||||
<figure class="image is-128x128">
|
||||
<img src="../../assets/undraw_profile.svg" alt="Profile illustration" />
|
||||
<img
|
||||
src="../../assets/undraw_profile.svg"
|
||||
alt="Profile illustration"
|
||||
/>
|
||||
</figure>
|
||||
<b-button type="is-primary">{{ $t("I have a Mobilizon account") }}</b-button>
|
||||
<b-button type="is-primary">{{
|
||||
$t("I have a Mobilizon account")
|
||||
}}</b-button>
|
||||
</router-link>
|
||||
<p>
|
||||
<small>
|
||||
{{
|
||||
$t("Either on the {instance} instance or on another instance.", {
|
||||
instance: host,
|
||||
})
|
||||
$t(
|
||||
"Either on the {instance} instance or on another instance.",
|
||||
{
|
||||
instance: host,
|
||||
}
|
||||
)
|
||||
}}
|
||||
</small>
|
||||
<b-tooltip
|
||||
@ -32,25 +44,41 @@
|
||||
</b-tooltip>
|
||||
</p>
|
||||
</div>
|
||||
<vertical-divider :content="$t('Or')" v-if="anonymousParticipationAllowed" />
|
||||
<vertical-divider
|
||||
:content="$t('Or')"
|
||||
v-if="anonymousParticipationAllowed"
|
||||
/>
|
||||
<div
|
||||
class="column"
|
||||
v-if="anonymousParticipationAllowed && hasAnonymousEmailParticipationMethod"
|
||||
v-if="
|
||||
anonymousParticipationAllowed &&
|
||||
hasAnonymousEmailParticipationMethod
|
||||
"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: RouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT }"
|
||||
v-if="event.local"
|
||||
>
|
||||
<figure class="image is-128x128">
|
||||
<img src="../../assets/undraw_mail_2.svg" alt="Privacy illustration" />
|
||||
<img
|
||||
src="../../assets/undraw_mail_2.svg"
|
||||
alt="Privacy illustration"
|
||||
/>
|
||||
</figure>
|
||||
<b-button type="is-primary">{{ $t("I don't have a Mobilizon account") }}</b-button>
|
||||
<b-button type="is-primary">{{
|
||||
$t("I don't have a Mobilizon account")
|
||||
}}</b-button>
|
||||
</router-link>
|
||||
<a :href="`${event.url}/participate/without-account`" v-else>
|
||||
<figure class="image is-128x128">
|
||||
<img src="../../assets/undraw_mail_2.svg" alt="Privacy illustration" />
|
||||
<img
|
||||
src="../../assets/undraw_mail_2.svg"
|
||||
alt="Privacy illustration"
|
||||
/>
|
||||
</figure>
|
||||
<b-button type="is-primary">{{ $t("I don't have a Mobilizon account") }}</b-button>
|
||||
<b-button type="is-primary">{{
|
||||
$t("I don't have a Mobilizon account")
|
||||
}}</b-button>
|
||||
</a>
|
||||
<p>
|
||||
<small>{{ $t("Participate using your email address") }}</small>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="root">
|
||||
<figure class="image" v-if="actualImageSrc">
|
||||
<img :src="actualImageSrc" />
|
||||
<figure class="image" v-if="imageSrc">
|
||||
<img :src="imageSrc" />
|
||||
</figure>
|
||||
<figure class="image is-128x128" v-else>
|
||||
<div class="image-placeholder">
|
||||
@ -9,12 +9,19 @@
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<b-upload @input="onFileChanged" :accept="accept">
|
||||
<a class="button is-primary">
|
||||
<b-icon icon="upload"></b-icon>
|
||||
<span>{{ $t("Click to upload") }}</span>
|
||||
</a>
|
||||
</b-upload>
|
||||
<div class="action-buttons">
|
||||
<b-field class="file is-primary">
|
||||
<b-upload @input="onFileChanged" :accept="accept" class="file-label">
|
||||
<span class="file-cta">
|
||||
<b-icon class="file-icon" icon="upload" />
|
||||
<span>{{ $t("Click to upload") }}</span>
|
||||
</span>
|
||||
</b-upload>
|
||||
</b-field>
|
||||
<b-button type="is-text" v-if="imageSrc" @click="removeOrClearPicture">
|
||||
{{ $t("Clear") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -45,18 +52,28 @@ figure.image {
|
||||
color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { IMedia } from "@/types/media.model";
|
||||
import { Component, Model, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
|
||||
@Component
|
||||
export default class PictureUpload extends Vue {
|
||||
@Model("change", { type: File }) readonly pictureFile!: File;
|
||||
|
||||
@Prop({ type: String, required: false }) defaultImageSrc!: string;
|
||||
@Prop({ type: Object, required: false }) defaultImage!: IMedia;
|
||||
|
||||
@Prop({ type: String, required: false, default: "image/gif,image/png,image/jpeg,image/webp" })
|
||||
@Prop({
|
||||
type: String,
|
||||
required: false,
|
||||
default: "image/gif,image/png,image/jpeg,image/webp",
|
||||
})
|
||||
accept!: string;
|
||||
|
||||
@Prop({
|
||||
@ -70,10 +87,14 @@ export default class PictureUpload extends Vue {
|
||||
})
|
||||
textFallback!: string;
|
||||
|
||||
imageSrc: string | null = null;
|
||||
imageSrc: string | null = this.defaultImage ? this.defaultImage.url : null;
|
||||
|
||||
file!: File | null;
|
||||
|
||||
mounted(): void {
|
||||
this.updatePreview(this.pictureFile);
|
||||
if (this.pictureFile) {
|
||||
this.updatePreview(this.pictureFile);
|
||||
}
|
||||
}
|
||||
|
||||
@Watch("pictureFile")
|
||||
@ -81,13 +102,23 @@ export default class PictureUpload extends Vue {
|
||||
this.updatePreview(val);
|
||||
}
|
||||
|
||||
onFileChanged(file: File): void {
|
||||
@Watch("defaultImage")
|
||||
onDefaultImageChange(defaultImage: IMedia): void {
|
||||
this.imageSrc = defaultImage ? defaultImage.url : null;
|
||||
}
|
||||
|
||||
onFileChanged(file: File | null): void {
|
||||
this.$emit("change", file);
|
||||
|
||||
this.updatePreview(file);
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
private updatePreview(file?: File) {
|
||||
async removeOrClearPicture(): Promise<void> {
|
||||
this.onFileChanged(null);
|
||||
}
|
||||
|
||||
private updatePreview(file?: File | null) {
|
||||
if (file) {
|
||||
this.imageSrc = URL.createObjectURL(file);
|
||||
return;
|
||||
@ -95,9 +126,5 @@ export default class PictureUpload extends Vue {
|
||||
|
||||
this.imageSrc = null;
|
||||
}
|
||||
|
||||
get actualImageSrc(): string | null {
|
||||
return this.imageSrc || this.defaultImageSrc;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -14,26 +14,46 @@
|
||||
<div class="media-content">
|
||||
<p class="post-minimalist-title">{{ post.title }}</p>
|
||||
<div class="metadata">
|
||||
<b-tag type="is-warning" size="is-small" v-if="post.draft">{{ $t("Draft") }}</b-tag>
|
||||
<b-tag type="is-warning" size="is-small" v-if="post.draft">{{
|
||||
$t("Draft")
|
||||
}}</b-tag>
|
||||
<small
|
||||
v-if="post.visibility === PostVisibility.PUBLIC && isCurrentActorMember"
|
||||
v-if="
|
||||
post.visibility === PostVisibility.PUBLIC &&
|
||||
isCurrentActorMember
|
||||
"
|
||||
class="has-text-grey"
|
||||
>
|
||||
<b-icon icon="earth" size="is-small" />{{ $t("Public") }}</small
|
||||
>
|
||||
<small v-else-if="post.visibility === PostVisibility.UNLISTED" class="has-text-grey">
|
||||
<b-icon icon="link" size="is-small" />{{ $t("Accessible through link") }}</small
|
||||
<small
|
||||
v-else-if="post.visibility === PostVisibility.UNLISTED"
|
||||
class="has-text-grey"
|
||||
>
|
||||
<b-icon icon="link" size="is-small" />{{
|
||||
$t("Accessible through link")
|
||||
}}</small
|
||||
>
|
||||
<small
|
||||
v-else-if="post.visibility === PostVisibility.PRIVATE"
|
||||
class="has-text-grey"
|
||||
>
|
||||
<small v-else-if="post.visibility === PostVisibility.PRIVATE" class="has-text-grey">
|
||||
<b-icon icon="lock" size="is-small" />{{
|
||||
$t("Accessible only to members", { group: post.attributedTo.name })
|
||||
$t("Accessible only to members", {
|
||||
group: post.attributedTo.name,
|
||||
})
|
||||
}}</small
|
||||
>
|
||||
<small class="has-text-grey">{{
|
||||
$options.filters.formatDateTimeString(new Date(post.insertedAt), false)
|
||||
$options.filters.formatDateTimeString(
|
||||
new Date(post.insertedAt),
|
||||
false
|
||||
)
|
||||
}}</small>
|
||||
<small class="has-text-grey" v-if="isCurrentActorMember">{{
|
||||
$t("Created by {username}", { username: `@${usernameWithDomain(post.author)}` })
|
||||
$t("Created by {username}", {
|
||||
username: `@${usernameWithDomain(post.author)}`,
|
||||
})
|
||||
}}</small>
|
||||
</div>
|
||||
</div>
|
||||
@ -43,15 +63,17 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { usernameWithDomain } from "@/types/actor";
|
||||
import { PostVisibility } from "@/types/enums";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import RouteName from "../../router/name";
|
||||
import { IPost, PostVisibility } from "../../types/post.model";
|
||||
import { IPost } from "../../types/post.model";
|
||||
|
||||
@Component
|
||||
export default class PostElementItem extends Vue {
|
||||
@Prop({ required: true, type: Object }) post!: IPost;
|
||||
|
||||
@Prop({ required: false, type: Boolean, default: false }) isCurrentActorMember!: boolean;
|
||||
@Prop({ required: false, type: Boolean, default: false })
|
||||
isCurrentActorMember!: boolean;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
@ -74,7 +96,8 @@ export default class PostElementItem extends Vue {
|
||||
|
||||
.post-minimalist-title {
|
||||
color: #3c376e;
|
||||
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
|
||||
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
|
||||
serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
|
@ -43,7 +43,8 @@ export default class PostListItem extends Vue {
|
||||
|
||||
.post-minimalist-title {
|
||||
color: #3c376e;
|
||||
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
|
||||
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
|
||||
serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
|
@ -22,10 +22,18 @@
|
||||
<div class="content columns">
|
||||
<div class="column is-one-quarter-desktop">
|
||||
<span v-if="report.reporter.type === ActorType.APPLICATION">
|
||||
{{ $t("Reported by someone on {domain}", { domain: report.reporter.domain }) }}
|
||||
{{
|
||||
$t("Reported by someone on {domain}", {
|
||||
domain: report.reporter.domain,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t("Reported by {reporter}", { reporter: report.reporter.preferredUsername }) }}
|
||||
{{
|
||||
$t("Reported by {reporter}", {
|
||||
reporter: report.reporter.preferredUsername,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="column" v-if="report.content" v-html="report.content" />
|
||||
@ -36,7 +44,7 @@
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { IReport } from "@/types/report.model";
|
||||
import { ActorType } from "@/types/actor";
|
||||
import { ActorType } from "@/types/enums";
|
||||
|
||||
@Component
|
||||
export default class ReportCard extends Vue {
|
||||
|
@ -4,7 +4,10 @@
|
||||
<p class="modal-card-title">{{ title }}</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body is-flex" :class="{ 'is-titleless': !title }">
|
||||
<section
|
||||
class="modal-card-body is-flex"
|
||||
:class="{ 'is-titleless': !title }"
|
||||
>
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<b-icon icon="alert" type="is-warning" size="is-large" />
|
||||
@ -16,7 +19,12 @@
|
||||
<figure class="image is-48x48" v-if="comment.actor.avatar">
|
||||
<img :src="comment.actor.avatar.url" alt="Image" />
|
||||
</figure>
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
<b-icon
|
||||
class="media-left"
|
||||
v-else
|
||||
size="is-large"
|
||||
icon="account-circle"
|
||||
/>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
@ -82,7 +90,10 @@ import { IComment } from "../../types/comment.model";
|
||||
},
|
||||
})
|
||||
export default class ReportModal extends Vue {
|
||||
@Prop({ type: Function }) onConfirm!: Function;
|
||||
@Prop({ type: Function }) onConfirm!: (
|
||||
content: string,
|
||||
forward: boolean
|
||||
) => void;
|
||||
|
||||
@Prop({ type: String }) title!: string;
|
||||
|
||||
|
@ -14,7 +14,9 @@
|
||||
</div>
|
||||
<div class="body">
|
||||
<h3>{{ resource.title }}</h3>
|
||||
<span class="host" v-if="inline">{{ resource.updatedAt | formatDateTimeString }}</span>
|
||||
<span class="host" v-if="inline">{{
|
||||
resource.updatedAt | formatDateTimeString
|
||||
}}</span>
|
||||
</div>
|
||||
<draggable
|
||||
v-if="!inline"
|
||||
@ -93,21 +95,27 @@ export default class FolderItem extends Mixins(ResourceMixin) {
|
||||
|
||||
async moveResource(resource: IResource): Promise<IResource | undefined> {
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate<{ updateResource: IResource }>({
|
||||
mutation: UPDATE_RESOURCE,
|
||||
variables: {
|
||||
id: resource.id,
|
||||
path: `${this.resource.path}/${resource.title}`,
|
||||
parentId: this.resource.id,
|
||||
},
|
||||
});
|
||||
const { data } = await this.$apollo.mutate<{ updateResource: IResource }>(
|
||||
{
|
||||
mutation: UPDATE_RESOURCE,
|
||||
variables: {
|
||||
id: resource.id,
|
||||
path: `${this.resource.path}/${resource.title}`,
|
||||
parentId: this.resource.id,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!data) {
|
||||
console.error("Error while updating resource");
|
||||
return undefined;
|
||||
}
|
||||
return data.updateResource;
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
</b-dropdown>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
|
||||
@Component
|
||||
export default class ResourceDropdown extends Vue {}
|
||||
|
@ -2,7 +2,12 @@
|
||||
<div class="resource-wrapper">
|
||||
<a :href="resource.resourceUrl" target="_blank">
|
||||
<div class="preview">
|
||||
<div v-if="resource.type && Object.keys(mapServiceTypeToIcon).includes(resource.type)">
|
||||
<div
|
||||
v-if="
|
||||
resource.type &&
|
||||
Object.keys(mapServiceTypeToIcon).includes(resource.type)
|
||||
"
|
||||
>
|
||||
<b-icon :icon="mapServiceTypeToIcon[resource.type]" size="is-large" />
|
||||
</div>
|
||||
<div
|
||||
@ -21,7 +26,9 @@
|
||||
:src="resource.metadata.faviconUrl"
|
||||
/>
|
||||
<h3>{{ resource.title }}</h3>
|
||||
<span class="host" v-if="inline">{{ resource.updatedAt | formatDateTimeString }}</span>
|
||||
<span class="host" v-if="inline">{{
|
||||
resource.updatedAt | formatDateTimeString
|
||||
}}</span>
|
||||
<span class="host" v-else>{{ urlHostname }}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -2,9 +2,15 @@
|
||||
<div v-if="resource">
|
||||
<article class="panel is-primary">
|
||||
<p class="panel-heading">
|
||||
{{ $t('Move "{resourceName}"', { resourceName: initialResource.title }) }}
|
||||
{{
|
||||
$t('Move "{resourceName}"', { resourceName: initialResource.title })
|
||||
}}
|
||||
</p>
|
||||
<a class="panel-block clickable" @click="resource = resource.parent" v-if="resource.parent">
|
||||
<a
|
||||
class="panel-block clickable"
|
||||
@click="resource = resource.parent"
|
||||
v-if="resource.parent"
|
||||
>
|
||||
<span class="panel-icon">
|
||||
<b-icon icon="chevron-up" size="is-small" />
|
||||
</span>
|
||||
@ -23,12 +29,19 @@
|
||||
<a
|
||||
class="panel-block"
|
||||
v-for="element in resource.children.elements"
|
||||
:class="{ clickable: element.type === 'folder' && element.id !== initialResource.id }"
|
||||
:class="{
|
||||
clickable:
|
||||
element.type === 'folder' && element.id !== initialResource.id,
|
||||
}"
|
||||
:key="element.id"
|
||||
@click="goDown(element)"
|
||||
>
|
||||
<span class="panel-icon">
|
||||
<b-icon icon="folder" size="is-small" v-if="element.type === 'folder'" />
|
||||
<b-icon
|
||||
icon="folder"
|
||||
size="is-small"
|
||||
v-if="element.type === 'folder'"
|
||||
/>
|
||||
<b-icon icon="link" size="is-small" v-else />
|
||||
</span>
|
||||
{{ element.title }}
|
||||
@ -44,10 +57,17 @@
|
||||
{{ $t("No resources in this folder") }}
|
||||
</p>
|
||||
</article>
|
||||
<b-button type="is-primary" @click="updateResource" :disabled="moveDisabled">{{
|
||||
$t("Move resource to {folder}", { folder: resource.title })
|
||||
<b-button
|
||||
type="is-primary"
|
||||
@click="updateResource"
|
||||
:disabled="moveDisabled"
|
||||
>{{
|
||||
$t("Move resource to {folder}", { folder: resource.title })
|
||||
}}</b-button
|
||||
>
|
||||
<b-button type="is-text" @click="$emit('close-move-modal')">{{
|
||||
$t("Cancel")
|
||||
}}</b-button>
|
||||
<b-button type="is-text" @click="$emit('closeMoveModal')">{{ $t("Cancel") }}</b-button>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
@ -81,31 +101,34 @@ export default class ResourceSelector extends Vue {
|
||||
|
||||
resource: IResource | undefined = this.initialResource.parent;
|
||||
|
||||
goDown(element: IResource) {
|
||||
goDown(element: IResource): void {
|
||||
if (element.type === "folder" && element.id !== this.initialResource.id) {
|
||||
this.resource = element;
|
||||
}
|
||||
}
|
||||
|
||||
updateResource() {
|
||||
updateResource(): void {
|
||||
this.$emit(
|
||||
"updateResource",
|
||||
"update-resource",
|
||||
{
|
||||
id: this.initialResource.id,
|
||||
title: this.initialResource.title,
|
||||
parent: this.resource && this.resource.path === "/" ? null : this.resource,
|
||||
parent:
|
||||
this.resource && this.resource.path === "/" ? null : this.resource,
|
||||
path: this.initialResource.path,
|
||||
},
|
||||
this.initialResource.parent
|
||||
);
|
||||
}
|
||||
|
||||
get moveDisabled() {
|
||||
get moveDisabled(): boolean | undefined {
|
||||
return (
|
||||
(this.initialResource.parent &&
|
||||
this.resource &&
|
||||
this.initialResource.parent.path === this.resource.path) ||
|
||||
(this.initialResource.parent == undefined && this.resource && this.resource.path === "/")
|
||||
(this.initialResource.parent === undefined &&
|
||||
this.resource &&
|
||||
this.resource.path === "/")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,9 @@ export default class SearchField extends Vue {
|
||||
|
||||
search = "";
|
||||
|
||||
enter() {
|
||||
async enter(): Promise<void> {
|
||||
this.$emit("navbar-search");
|
||||
this.$router.push({
|
||||
await this.$router.push({
|
||||
name: RouteName.SEARCH,
|
||||
query: { term: this.search },
|
||||
});
|
||||
|
@ -15,16 +15,27 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<b-checkbox v-model="notificationOnDay" @input="updateSetting({ notificationOnDay })">
|
||||
<b-checkbox
|
||||
v-model="notificationOnDay"
|
||||
@input="updateSetting({ notificationOnDay })"
|
||||
>
|
||||
<strong>{{ $t("Notification on the day of the event") }}</strong>
|
||||
<p>
|
||||
{{
|
||||
$t("We'll use your timezone settings to send a recap of the morning of the event.")
|
||||
$t(
|
||||
"We'll use your timezone settings to send a recap of the morning of the event."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</b-checkbox>
|
||||
</div>
|
||||
<p>{{ $t("To activate more notifications, head over to the notification settings.") }}</p>
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
"To activate more notifications, head over to the notification settings."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@ -50,7 +61,11 @@ export default class NotificationsOnboarding extends mixins(Onboarding) {
|
||||
try {
|
||||
this.doUpdateSetting(variables);
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ export default class SettingMenuItem extends Vue {
|
||||
|
||||
@Prop({ required: true, type: Object }) to!: Route;
|
||||
|
||||
get isActive() {
|
||||
get isActive(): boolean {
|
||||
if (!this.to) return false;
|
||||
if (this.to.name === this.$route.name) {
|
||||
if (this.to.params) {
|
||||
|
@ -20,11 +20,12 @@ export default class SettingMenuSection extends Vue {
|
||||
|
||||
@Prop({ required: true, type: Object }) to!: Route;
|
||||
|
||||
get sectionActive() {
|
||||
get sectionActive(): boolean {
|
||||
if (this.$slots.default) {
|
||||
return this.$slots.default.some(
|
||||
({
|
||||
componentOptions: {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
propsData: { to },
|
||||
},
|
||||
|
@ -1,18 +1,27 @@
|
||||
<template>
|
||||
<aside>
|
||||
<ul>
|
||||
<SettingMenuSection :title="$t('Account')" :to="{ name: RouteName.ACCOUNT_SETTINGS }">
|
||||
<SettingMenuSection
|
||||
:title="$t('Account')"
|
||||
:to="{ name: RouteName.ACCOUNT_SETTINGS }"
|
||||
>
|
||||
<SettingMenuItem
|
||||
:title="this.$t('General')"
|
||||
:to="{ name: RouteName.ACCOUNT_SETTINGS_GENERAL }"
|
||||
/>
|
||||
<SettingMenuItem :title="$t('Preferences')" :to="{ name: RouteName.PREFERENCES }" />
|
||||
<SettingMenuItem
|
||||
:title="$t('Preferences')"
|
||||
:to="{ name: RouteName.PREFERENCES }"
|
||||
/>
|
||||
<SettingMenuItem
|
||||
:title="this.$t('Email notifications')"
|
||||
:to="{ name: RouteName.NOTIFICATIONS }"
|
||||
/>
|
||||
</SettingMenuSection>
|
||||
<SettingMenuSection :title="$t('Profiles')" :to="{ name: RouteName.IDENTITIES }">
|
||||
<SettingMenuSection
|
||||
:title="$t('Profiles')"
|
||||
:to="{ name: RouteName.IDENTITIES }"
|
||||
>
|
||||
<SettingMenuItem
|
||||
v-for="profile in identities"
|
||||
:key="profile.preferredUsername"
|
||||
@ -22,7 +31,10 @@
|
||||
params: { identityName: profile.preferredUsername },
|
||||
}"
|
||||
/>
|
||||
<SettingMenuItem :title="$t('New profile')" :to="{ name: RouteName.CREATE_IDENTITY }" />
|
||||
<SettingMenuItem
|
||||
:title="$t('New profile')"
|
||||
:to="{ name: RouteName.CREATE_IDENTITY }"
|
||||
/>
|
||||
</SettingMenuSection>
|
||||
<SettingMenuSection
|
||||
v-if="
|
||||
@ -33,35 +45,54 @@
|
||||
:title="$t('Moderation')"
|
||||
:to="{ name: RouteName.MODERATION }"
|
||||
>
|
||||
<SettingMenuItem :title="$t('Reports')" :to="{ name: RouteName.REPORTS }" />
|
||||
<SettingMenuItem :title="$t('Moderation log')" :to="{ name: RouteName.REPORT_LOGS }" />
|
||||
<SettingMenuItem
|
||||
:title="$t('Reports')"
|
||||
:to="{ name: RouteName.REPORTS }"
|
||||
/>
|
||||
<SettingMenuItem
|
||||
:title="$t('Moderation log')"
|
||||
:to="{ name: RouteName.REPORT_LOGS }"
|
||||
/>
|
||||
<SettingMenuItem :title="$t('Users')" :to="{ name: RouteName.USERS }" />
|
||||
<SettingMenuItem :title="$t('Profiles')" :to="{ name: RouteName.PROFILES }" />
|
||||
<SettingMenuItem :title="$t('Groups')" :to="{ name: RouteName.ADMIN_GROUPS }" />
|
||||
<SettingMenuItem
|
||||
:title="$t('Profiles')"
|
||||
:to="{ name: RouteName.PROFILES }"
|
||||
/>
|
||||
<SettingMenuItem
|
||||
:title="$t('Groups')"
|
||||
:to="{ name: RouteName.ADMIN_GROUPS }"
|
||||
/>
|
||||
</SettingMenuSection>
|
||||
<SettingMenuSection
|
||||
v-if="this.currentUser.role == ICurrentUserRole.ADMINISTRATOR"
|
||||
:title="$t('Admin')"
|
||||
:to="{ name: RouteName.ADMIN }"
|
||||
>
|
||||
<SettingMenuItem :title="$t('Dashboard')" :to="{ name: RouteName.ADMIN_DASHBOARD }" />
|
||||
<SettingMenuItem
|
||||
:title="$t('Dashboard')"
|
||||
:to="{ name: RouteName.ADMIN_DASHBOARD }"
|
||||
/>
|
||||
<SettingMenuItem
|
||||
:title="$t('Instance settings')"
|
||||
:to="{ name: RouteName.ADMIN_SETTINGS }"
|
||||
/>
|
||||
<SettingMenuItem :title="$t('Federation')" :to="{ name: RouteName.RELAYS }" />
|
||||
<SettingMenuItem
|
||||
:title="$t('Federation')"
|
||||
:to="{ name: RouteName.RELAYS }"
|
||||
/>
|
||||
</SettingMenuSection>
|
||||
</ul>
|
||||
</aside>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { ICurrentUserRole } from "@/types/enums";
|
||||
import SettingMenuSection from "./SettingMenuSection.vue";
|
||||
import SettingMenuItem from "./SettingMenuItem.vue";
|
||||
import { IDENTITIES } from "../../graphql/actor";
|
||||
import { IPerson, Person } from "../../types/actor";
|
||||
import { CURRENT_USER_CLIENT } from "../../graphql/user";
|
||||
import { ICurrentUser, ICurrentUserRole } from "../../types/current-user.model";
|
||||
import { ICurrentUser } from "../../types/current-user.model";
|
||||
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@ -70,7 +101,8 @@ import RouteName from "../../router/name";
|
||||
apollo: {
|
||||
identities: {
|
||||
query: IDENTITIES,
|
||||
update: (data) => data.identities.map((identity: IPerson) => new Person(identity)),
|
||||
update: (data) =>
|
||||
data.identities.map((identity: IPerson) => new Person(identity)),
|
||||
},
|
||||
currentUser: CURRENT_USER_CLIENT,
|
||||
},
|
||||
|
@ -39,7 +39,10 @@
|
||||
timezone,
|
||||
})
|
||||
}}
|
||||
<b-message type="is-danger" v-if="!$apollo.loading && !supportedTimezone">
|
||||
<b-message
|
||||
type="is-danger"
|
||||
v-if="!$apollo.loading && !supportedTimezone"
|
||||
>
|
||||
{{ $t("Your timezone {timezone} isn't supported.", { timezone }) }}
|
||||
</b-message>
|
||||
</p>
|
||||
|
@ -2,9 +2,10 @@
|
||||
<div class="card" v-if="todo">
|
||||
<div class="card-content">
|
||||
<b-checkbox v-model="status" />
|
||||
<router-link :to="{ name: RouteName.TODO, params: { todoId: todo.id } }">{{
|
||||
todo.title
|
||||
}}</router-link>
|
||||
<router-link
|
||||
:to="{ name: RouteName.TODO, params: { todoId: todo.id } }"
|
||||
>{{ todo.title }}</router-link
|
||||
>
|
||||
<span class="details has-text-grey">
|
||||
<span v-if="todo.dueDate" class="due_date">
|
||||
<b-icon icon="calendar" />
|
||||
@ -13,7 +14,9 @@
|
||||
<span v-if="todo.assignedTo" class="assigned_to">
|
||||
<b-icon icon="account" />
|
||||
{{ `@${todo.assignedTo.preferredUsername}` }}
|
||||
<span v-if="todo.assignedTo.domain">{{ `@${todo.assignedTo.domain}` }}</span>
|
||||
<span v-if="todo.assignedTo.domain">{{
|
||||
`@${todo.assignedTo.domain}`
|
||||
}}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@ -53,7 +56,11 @@ export default class Todo extends Vue {
|
||||
});
|
||||
this.editMode = false;
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { debounce } from "lodash";
|
||||
import { debounce, DebouncedFunc } from "lodash";
|
||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { ITodo } from "../../types/todos";
|
||||
import RouteName from "../../router/name";
|
||||
@ -36,7 +36,9 @@ export default class Todo extends Vue {
|
||||
|
||||
editMode = false;
|
||||
|
||||
debounceUpdateTodo!: Function;
|
||||
debounceUpdateTodo!: DebouncedFunc<
|
||||
(obj: Record<string, unknown>) => Promise<void>
|
||||
>;
|
||||
|
||||
// We put this in data because of issues like
|
||||
// https://github.com/vuejs/vue-class-component/issues/263
|
||||
@ -89,7 +91,11 @@ export default class Todo extends Vue {
|
||||
});
|
||||
this.editMode = false;
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,11 @@
|
||||
<b-icon :icon="oauthProvider.id" />
|
||||
<span>{{ SELECTED_PROVIDERS[oauthProvider.id] }}</span></a
|
||||
>
|
||||
<a class="button is-light" :href="`/auth/${oauthProvider.id}`" v-else-if="isProviderSelected">
|
||||
<a
|
||||
class="button is-light"
|
||||
:href="`/auth/${oauthProvider.id}`"
|
||||
v-else-if="isProviderSelected"
|
||||
>
|
||||
<b-icon icon="lock" />
|
||||
<span>{{ oauthProvider.label }}</span>
|
||||
</a>
|
||||
|
@ -20,7 +20,9 @@
|
||||
</div>
|
||||
<vertical-divider :content="$t('Or')" />
|
||||
<div class="column">
|
||||
<subtitle>{{ $t("I have an account on another Mobilizon instance.") }}</subtitle>
|
||||
<subtitle>{{
|
||||
$t("I have an account on another Mobilizon instance.")
|
||||
}}</subtitle>
|
||||
<p>{{ $t("Other software may also support this.") }}</p>
|
||||
<p>{{ sentence }}</p>
|
||||
<form @submit.prevent="redirectToInstance">
|
||||
@ -34,7 +36,9 @@
|
||||
:placeholder="$t('profile@instance')"
|
||||
></b-input>
|
||||
<p class="control">
|
||||
<button class="button is-primary" type="submit">{{ $t("Go") }}</button>
|
||||
<button class="button is-primary" type="submit">
|
||||
{{ $t("Go") }}
|
||||
</button>
|
||||
</p>
|
||||
</b-field>
|
||||
</b-field>
|
||||
@ -54,7 +58,7 @@
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import VerticalDivider from "@/components/Utils/VerticalDivider.vue";
|
||||
import Subtitle from "@/components/Utils/Subtitle.vue";
|
||||
import { LoginErrorCode } from "@/types/login-error-code.model";
|
||||
import { LoginErrorCode } from "@/types/enums";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
@ -80,14 +84,22 @@ export default class RedirectWithAccount extends Vue {
|
||||
|
||||
async redirectToInstance(): Promise<void> {
|
||||
const [, host] = this.remoteActorAddress.split("@", 2);
|
||||
const remoteInteractionURI = await this.webFingerFetch(host, this.remoteActorAddress);
|
||||
const remoteInteractionURI = await this.webFingerFetch(
|
||||
host,
|
||||
this.remoteActorAddress
|
||||
);
|
||||
window.open(remoteInteractionURI);
|
||||
}
|
||||
|
||||
private async webFingerFetch(hostname: string, identity: string): Promise<string> {
|
||||
private async webFingerFetch(
|
||||
hostname: string,
|
||||
identity: string
|
||||
): Promise<string> {
|
||||
const scheme = process.env.NODE_ENV === "production" ? "https" : "http";
|
||||
const data = await (
|
||||
await fetch(`${scheme}://${hostname}/.well-known/webfinger?resource=acct:${identity}`)
|
||||
await fetch(
|
||||
`${scheme}://${hostname}/.well-known/webfinger?resource=acct:${identity}`
|
||||
)
|
||||
).json();
|
||||
if (data && Array.isArray(data.links)) {
|
||||
const link: { template: string } = data.links.find(
|
||||
|
@ -21,7 +21,8 @@ h2 {
|
||||
display: inline;
|
||||
padding: 3px 8px;
|
||||
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-size: 32px;
|
||||
}
|
||||
|
@ -14,7 +14,10 @@ function formatDateString(value: string): string {
|
||||
}
|
||||
|
||||
function formatTimeString(value: string): string {
|
||||
return parseDateTime(value).toLocaleTimeString(undefined, { hour: "numeric", minute: "numeric" });
|
||||
return parseDateTime(value).toLocaleTimeString(undefined, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function formatDateTimeString(value: string, showTime = true): string {
|
||||
|
@ -1,7 +1,12 @@
|
||||
import nl2br from "@/filters/utils";
|
||||
import { formatDateString, formatTimeString, formatDateTimeString } from "./datetime";
|
||||
import {
|
||||
formatDateString,
|
||||
formatTimeString,
|
||||
formatDateTimeString,
|
||||
} from "./datetime";
|
||||
|
||||
export default {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
install(vue: any): void {
|
||||
vue.filter("formatDateString", formatDateString);
|
||||
vue.filter("formatTimeString", formatTimeString);
|
||||
|
@ -10,6 +10,7 @@ export const FETCH_PERSON = gql`
|
||||
summary
|
||||
preferredUsername
|
||||
suspended
|
||||
mediaSize
|
||||
avatar {
|
||||
id
|
||||
name
|
||||
@ -51,6 +52,7 @@ export const GET_PERSON = gql`
|
||||
summary
|
||||
preferredUsername
|
||||
suspended
|
||||
mediaSize
|
||||
avatar {
|
||||
id
|
||||
name
|
||||
@ -63,7 +65,10 @@ export const GET_PERSON = gql`
|
||||
feedTokens {
|
||||
token
|
||||
}
|
||||
organizedEvents(page: $organizedEventsPage, limit: $organizedEventsLimit) {
|
||||
organizedEvents(
|
||||
page: $organizedEventsPage
|
||||
limit: $organizedEventsLimit
|
||||
) {
|
||||
total
|
||||
elements {
|
||||
id
|
||||
@ -206,6 +211,18 @@ export const LOGGED_USER_PARTICIPATIONS = gql`
|
||||
url
|
||||
}
|
||||
}
|
||||
attributedTo {
|
||||
avatar {
|
||||
id
|
||||
url
|
||||
}
|
||||
preferredUsername
|
||||
name
|
||||
summary
|
||||
domain
|
||||
url
|
||||
id
|
||||
}
|
||||
participantStats {
|
||||
going
|
||||
notApproved
|
||||
@ -215,6 +232,11 @@ export const LOGGED_USER_PARTICIPATIONS = gql`
|
||||
maximumAttendeeCapacity
|
||||
remainingAttendeeCapacity
|
||||
}
|
||||
tags {
|
||||
id
|
||||
slug
|
||||
title
|
||||
}
|
||||
}
|
||||
id
|
||||
role
|
||||
@ -282,6 +304,16 @@ export const LOGGED_USER_MEMBERSHIPS = gql`
|
||||
elements {
|
||||
id
|
||||
role
|
||||
actor {
|
||||
id
|
||||
avatar {
|
||||
id
|
||||
url
|
||||
}
|
||||
preferredUsername
|
||||
name
|
||||
domain
|
||||
}
|
||||
parent {
|
||||
id
|
||||
preferredUsername
|
||||
@ -419,7 +451,7 @@ export const CREATE_PERSON = gql`
|
||||
$preferredUsername: String!
|
||||
$name: String!
|
||||
$summary: String
|
||||
$avatar: PictureInput
|
||||
$avatar: MediaInput
|
||||
) {
|
||||
createPerson(
|
||||
preferredUsername: $preferredUsername
|
||||
@ -440,7 +472,12 @@ export const CREATE_PERSON = gql`
|
||||
`;
|
||||
|
||||
export const UPDATE_PERSON = gql`
|
||||
mutation UpdatePerson($id: ID!, $name: String, $summary: String, $avatar: PictureInput) {
|
||||
mutation UpdatePerson(
|
||||
$id: ID!
|
||||
$name: String
|
||||
$summary: String
|
||||
$avatar: MediaInput
|
||||
) {
|
||||
updatePerson(id: $id, name: $name, summary: $summary, avatar: $avatar) {
|
||||
id
|
||||
preferredUsername
|
||||
@ -467,7 +504,12 @@ export const DELETE_PERSON = gql`
|
||||
* Prefer CREATE_PERSON when creating another identity
|
||||
*/
|
||||
export const REGISTER_PERSON = gql`
|
||||
mutation($preferredUsername: String!, $name: String!, $summary: String!, $email: String!) {
|
||||
mutation(
|
||||
$preferredUsername: String!
|
||||
$name: String!
|
||||
$summary: String!
|
||||
$email: String!
|
||||
) {
|
||||
registerPerson(
|
||||
preferredUsername: $preferredUsername
|
||||
name: $name
|
||||
|