- Added an event category field. Administrators can extend the pre-configured list of categories through configuration.

- Added possibility for administrators to have analytics (Matomo, Plausible supported) and error handling (Sentry supported) on front-end.
 - Redesigned federation admin section with dedicated instance pages
 - Allow to filter moderation reports by domain
 - Added a button to go to past events of a group if it has no upcoming events
 - Add Überauth CAS Strategy
 - Add a CLI command to delete actors
 
 - Changed mailer library from Bamboo to Swoosh, should fix emails being considered spam. **Some configuration changes are required, see below.**
 - Expose some fields to ActivityStreams event representation: `isOnline`, `remainingAttendeeCapacity` and `participantCount`
 - Expose a new field to ActivityStreams group representation: `memberCount`
 - Improve group creation errors feedback
 - Only display locality in event card
 - Stale groups are now excluded from group search
 - Event default visibility is now set according to group privacy setting
 - Remove Koena Connect button
 - Hide the whole metadata block if group has no description
 - Increase task timeout in Refresher to 60 seconds
 - Allow webfinger to be fetched over http (not https) in dev mode
 - Improve reactions when approving/rejecting an instance follow
 - Improve instance admin view for mobile
 - Allow to reject instance following
 - Allow instance to have non-standard ports
 - Add pagination to the instances list
 - Eventually fetch actors in mentions
 - Improve IdentityPicker, JoinGroupWithAccount and ActorInline components
 - Various group and posts improvements
 - Update schema.graphql file
 - Add "Accept-Language" header to sentry request metadata
 - Hide address blocks when address has no real data
 - Remove obsolete attribute type="text/css" from <style> tags
 - Improve actor cards integration
 - Use upstream dependencies for Ueberauth providers
 - Include ongoing events in search
 - Send push notification into own task
 - Add appropriate timeouts for Repo.transactions
 - Add a proper error message when adding an instance follow that doesn't respond
 - Allow the instance to be followed from Mastodon (through relays)
 - Remove unused fragment from FETCH_PERSON GraphQL query
 
 - Fixed actor refreshment being impossible
 - Fixed ical export for undefined datetimes
 - Fixed parsing links with hashtag characters
 - Fixed fetching link details from Twitter
 - Fixed Thunderbird accessing ICS feed endpoint with special `Accept` HTTP header
 - Make sure every ICS/Feed caches are emptied when modifying entities
 - Fixed time issues with DST changes
 - Fixed group preview card not truncating description
 - Fixed redirection after login
 - Fixed user admin section showing button to confirm user when the user is already confirmed
 - Fixed creating event from group view not always setting the group as organizer
 - Fixed invalid addresses blocking event metadata preview rendering
 - Fixed group deletion with comments that caused foreign key issues
 - Fixed incoming Accept activities from participations we don't already have
 - Fixed resources that didn't have metadata size limits
 - Properly fallback to UTC when sending notifications and the user doesn't have a timezone setting set
 - Fix posts creation
 - Fix rejecting instance follow
 - Fix pagination of group events
 - Add proper fallback for when a TZ isn't registered
 - Hide side of report modal on low width screens
 - Fix Telegram Logo being replaced with Mastodon logo in ShareGroupModal
 - Change URL for Mastodon Share Manager
 - Fix receiving Flag activities on federated events
 - Fix activity notifications by preloading user.activity_settings
 - Fix text overflow on group card description
 - Exclude tags with more than 40 characters from being extracted
 - Avoid duplicate tags with different casing
 - Fix invalid HTML (<div> inside <label>)
 - Fix latest group not refreshing in admin section
 - Add missing "relay@" part of federated address to follow
 - Fix Ueberauth use of CSRF with session
 - Fix being an administrator when using 3rd-party auth provider
 - Make sure activity recipient can't be nil
 - Make sure users can't create profiles or groups with non-valid patterns
 - Add description field to address representation
 - Make sure prompt show the correct message and not just "Continue?" in mix mode
 - Make sure activity notification recaps can't be sent multiple times
 - Fix group notification of new event being sent multiple times
 - Fix links to group page in group membership emails and participation
 - Fix clicking on map crashing the app
 
 - Arabic
 - Basque
 - Belarusian
 - Bengali
 - Catalan
 - Chinese (Traditional)
 - Croatian
 - Czech
 - Danish
 - Dutch
 - Esperanto
 - Finnish
 - French
 - Gaelic
 - Galician
 - German
 - Hebrew
 - Hungarian
 - Indonesian
 - Italian
 - Japanese
 - Kabyle
 - Kannada
 - Norwegian Nynorsk
 - Occitan
 - Persian
 - Polish
 - Portuguese
 - Portuguese (Brazil)
 - Russian
 - Slovenian
 - Spanish
 - Swedish
 - Welsh
 -----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCAAdFiEExMITpfxOHHCvHn8FoGG53eDKB3MFAmKCI7kACgkQoGG53eDK
 B3M0cg//U3F1oCzg8piPaIAUGgXTx7yOZ+rRO8tHX63+whg6Yu7vu09G8Hm7iyVD
 t2CuzEwKNKdgwUV4XG6CJfban20p/lBhGupQvCpIUOBZ6gKn6m2j7KAHj2/GbEot
 um+Dqu/ktIZN+2vc48nH7HGgcO1c/oCzUE3nH0HYWA9GCVGIAIz+alHszOl9CtL/
 TFpgrF5ygRtk4Z4W/hg6j94PP4IpdCybob8/mgN8eZeF4VbaiCGBdsxl5jai+nqE
 wgebLqyyDAmnE/aW1E64TTLlRP8eZwmNkfxuyI5bjMX8sgvML/ZEcKmcIHPh7tby
 5+pEDnRPg8vnqI5SnkaNH2/77WWY0zPQRFsIV4NFnbMuqZ8fumIC9ke/RXEDGMfS
 ZQi0mWpTT6xZC/XyrjfcaGrV3JY/mrkEXUxFlEwMNm3dGnHKSGCgHIz4gPoQw5U+
 wxPyi+wcnmeHTbic7A89aGH3a4H2BF+f2iZeTrA+XyyopU/7Pr9xgzEHL/3Y/9wA
 +d/hJffLjOM5stg1Zqcndv9LxEFcTtjYBfIeV5kPLVc35s7IbmvoJizFBDrAWisj
 qLsmk9Tg2ODXCN9jIGto1YqQwBWUSYAfYgMzLyvLG1yb8mSBlhWUp/a3uZcekCUL
 qDOLNiiFKdD4TIjV/8Nst0YIHR3g4K2NtftjO92NWx2XeRBcS+w=
 =aZJM
 -----END PGP SIGNATURE-----

Merge tag '2.1.0' into chapril

- Added an event category field. Administrators can extend the pre-configured list of categories through configuration.
- Added possibility for administrators to have analytics (Matomo, Plausible supported) and error handling (Sentry supported) on front-end.
- Redesigned federation admin section with dedicated instance pages
- Allow to filter moderation reports by domain
- Added a button to go to past events of a group if it has no upcoming events
- Add Überauth CAS Strategy
- Add a CLI command to delete actors

- Changed mailer library from Bamboo to Swoosh, should fix emails being considered spam. **Some configuration changes are required, see below.**
- Expose some fields to ActivityStreams event representation: `isOnline`, `remainingAttendeeCapacity` and `participantCount`
- Expose a new field to ActivityStreams group representation: `memberCount`
- Improve group creation errors feedback
- Only display locality in event card
- Stale groups are now excluded from group search
- Event default visibility is now set according to group privacy setting
- Remove Koena Connect button
- Hide the whole metadata block if group has no description
- Increase task timeout in Refresher to 60 seconds
- Allow webfinger to be fetched over http (not https) in dev mode
- Improve reactions when approving/rejecting an instance follow
- Improve instance admin view for mobile
- Allow to reject instance following
- Allow instance to have non-standard ports
- Add pagination to the instances list
- Eventually fetch actors in mentions
- Improve IdentityPicker, JoinGroupWithAccount and ActorInline components
- Various group and posts improvements
- Update schema.graphql file
- Add "Accept-Language" header to sentry request metadata
- Hide address blocks when address has no real data
- Remove obsolete attribute type="text/css" from <style> tags
- Improve actor cards integration
- Use upstream dependencies for Ueberauth providers
- Include ongoing events in search
- Send push notification into own task
- Add appropriate timeouts for Repo.transactions
- Add a proper error message when adding an instance follow that doesn't respond
- Allow the instance to be followed from Mastodon (through relays)
- Remove unused fragment from FETCH_PERSON GraphQL query

- Fixed actor refreshment being impossible
- Fixed ical export for undefined datetimes
- Fixed parsing links with hashtag characters
- Fixed fetching link details from Twitter
- Fixed Thunderbird accessing ICS feed endpoint with special `Accept` HTTP header
- Make sure every ICS/Feed caches are emptied when modifying entities
- Fixed time issues with DST changes
- Fixed group preview card not truncating description
- Fixed redirection after login
- Fixed user admin section showing button to confirm user when the user is already confirmed
- Fixed creating event from group view not always setting the group as organizer
- Fixed invalid addresses blocking event metadata preview rendering
- Fixed group deletion with comments that caused foreign key issues
- Fixed incoming Accept activities from participations we don't already have
- Fixed resources that didn't have metadata size limits
- Properly fallback to UTC when sending notifications and the user doesn't have a timezone setting set
- Fix posts creation
- Fix rejecting instance follow
- Fix pagination of group events
- Add proper fallback for when a TZ isn't registered
- Hide side of report modal on low width screens
- Fix Telegram Logo being replaced with Mastodon logo in ShareGroupModal
- Change URL for Mastodon Share Manager
- Fix receiving Flag activities on federated events
- Fix activity notifications by preloading user.activity_settings
- Fix text overflow on group card description
- Exclude tags with more than 40 characters from being extracted
- Avoid duplicate tags with different casing
- Fix invalid HTML (<div> inside <label>)
- Fix latest group not refreshing in admin section
- Add missing "relay@" part of federated address to follow
- Fix Ueberauth use of CSRF with session
- Fix being an administrator when using 3rd-party auth provider
- Make sure activity recipient can't be nil
- Make sure users can't create profiles or groups with non-valid patterns
- Add description field to address representation
- Make sure prompt show the correct message and not just "Continue?" in mix mode
- Make sure activity notification recaps can't be sent multiple times
- Fix group notification of new event being sent multiple times
- Fix links to group page in group membership emails and participation
- Fix clicking on map crashing the app

- Arabic
- Basque
- Belarusian
- Bengali
- Catalan
- Chinese (Traditional)
- Croatian
- Czech
- Danish
- Dutch
- Esperanto
- Finnish
- French
- Gaelic
- Galician
- German
- Hebrew
- Hungarian
- Indonesian
- Italian
- Japanese
- Kabyle
- Kannada
- Norwegian Nynorsk
- Occitan
- Persian
- Polish
- Portuguese
- Portuguese (Brazil)
- Russian
- Slovenian
- Spanish
- Swedish
- Welsh
This commit is contained in:
Tykayn 2022-05-24 15:48:39 +02:00 committed by tykayn
commit b0b5251211
378 changed files with 23307 additions and 19016 deletions

View File

@ -19,7 +19,6 @@ MOBILIZON_REPLY_EMAIL=contact@mobilizon.lan
# Email settings # Email settings
MOBILIZON_SMTP_SERVER=localhost MOBILIZON_SMTP_SERVER=localhost
MOBILIZON_SMTP_PORT=25 MOBILIZON_SMTP_PORT=25
MOBILIZON_SMTP_HOSTNAME=localhost
MOBILIZON_SMTP_USERNAME=noreply@mobilizon.lan MOBILIZON_SMTP_USERNAME=noreply@mobilizon.lan
MOBILIZON_SMTP_PASSWORD=password MOBILIZON_SMTP_PASSWORD=password
MOBILIZON_SMTP_SSL=false MOBILIZON_SMTP_SSL=false

View File

@ -1,3 +1,4 @@
[ [
inputs: ["{mix,.formatter}.exs", "{config,lib,test,priv}/**/*.{ex,exs}"] plugins: [Phoenix.LiveView.HTMLFormatter],
inputs: ["{mix,.formatter}.exs", "{config,lib,test,priv}/**/*.{ex,exs,heex}"]
] ]

View File

@ -110,7 +110,7 @@ deps:
exunit: exunit:
stage: test stage: test
services: services:
- name: postgis/postgis:13-3.1 - name: postgis/postgis:14-3.2
alias: postgres alias: postgres
variables: variables:
MIX_ENV: test MIX_ENV: test
@ -238,9 +238,13 @@ build-docker-tag:
# Packaging app for amd64 # Packaging app for amd64
package-app: package-app:
image: mobilizon/buildpack:1.13.4-erlang-24.3.3-debian-buster
stage: package stage: package
variables: &release-variables variables: &release-variables
MIX_ENV: "prod" MIX_ENV: "prod"
DEBIAN_FRONTEND: noninteractive
TZ: Etc/UTC
APP_ASSET: "${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME}_${ARCH}.tar.gz"
script: &release-script script: &release-script
- mix local.hex --force - mix local.hex --force
- mix local.rebar --force - mix local.rebar --force
@ -274,7 +278,7 @@ package-app-dev:
# Packaging app for multi-arch # Packaging app for multi-arch
multi-arch-release: multi-arch-release:
stage: package stage: package
image: docker:stable image: docker:20.10.12
variables: variables:
DOCKER_TLS_CERTDIR: "/certs" DOCKER_TLS_CERTDIR: "/certs"
DOCKER_HOST: tcp://docker:2376 DOCKER_HOST: tcp://docker:2376
@ -282,8 +286,9 @@ multi-arch-release:
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client" DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
DOCKER_DRIVER: overlay2 DOCKER_DRIVER: overlay2
APP_ASSET: "${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME}_${ARCH}.tar.gz" APP_ASSET: "${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME}_${ARCH}.tar.gz"
OS: debian-buster
services: services:
- docker:stable-dind - docker:20.10.12-dind
cache: {} cache: {}
before_script: before_script:
# Install buildx # Install buildx

View File

@ -8,5 +8,5 @@
out: "", out: "",
threshold: "medium", threshold: "medium",
ignore: ["Config.HTTPS", "Config.CSP"], ignore: ["Config.HTTPS", "Config.CSP"],
ignore_files: ["config/dev.1.secret.exs", "config/dev.2.secret.exs", "config/dev.3.secret.exs", "config/dev.secret.exs", "config/e2e.secret.exs", "config/prod.secret.exs", "config/test.secret.exs", "config/runtime.1.secret.exs", "config/runtime.2.secret.exs", "config/runtime.3.secret.exs", "config/runtime.exs"] ignore_files: ["config/runtime.exs"]
] ]

View File

@ -1,12 +1,16 @@
5048AE33D6269B15E21CF28C6F545AB6 02CE4963DFD1B0D6D5C567357CAFFE97
752C0E897CA81ACD81F4BB215FA5F8E4
23412CF16549E4E88366DC9DECF39071
81C1F600C5809C7029EE32DE4818CD7D
155A1FB53DE39EC8EFCFD7FB94EA823D 155A1FB53DE39EC8EFCFD7FB94EA823D
73B351E4CB3AF715AD450A085F5E6304 2262742E5C8944D5BF6698EC61F5DE50
BBACD7F0BACD4A6D3010C26604671692 25BEE162A99754480967216281E9EF33
6D4D4A4821B93BCFAC9CDBB367B34C4B 2A6F71CD6F1246F0B152C2376E2E398A
5674F0D127852889ED0132DC2F442AAB 30552A09D485A6AA73401C1D54F63C21
1600B7206E47F630D94AB54C360906F0 52900CE4EE3598F6F178A651FB256770
6151F44368FC19F2394274F513C29151
765526195D4C6D770EAF4DC944A8CBF4
B2FF1A12F13B873507C85091688C1D6D
B9AF8A342CD7FF39E10CC10A408C28E1
C042E87389F7BDCFF4E076E95731AE69
C42BFAEF7100F57BED75998B217C857A
D11958E86F1B6D37EF656B63405CA8A4
F16F054F2628609A726B9FF2F089D484

View File

@ -1,2 +1,2 @@
elixir 1.13.3-otp-24 elixir 1.13.4-otp-24
erlang 24.3.2 erlang 24.3.3

View File

@ -1,21 +1,55 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.1.0 - 2022-03-28 ## 2.1.0 - 2022-05-16
### Added ### Added
- Event category field - Added an event category field. Administrators can extend the pre-configured list of categories through configuration.
- Added possibility for administrators to have analytics (Matomo, Plausible supported) and error handling (Sentry supported) on front-end.
- Redesigned federation admin section with dedicated instance pages - Redesigned federation admin section with dedicated instance pages
- Allow to filter moderation reports by domain - Allow to filter moderation reports by domain
- Added a button to go to past events of a group if it has no upcoming events
- Add Überauth CAS Strategy
- Add a CLI command to delete actors
### Changed ### Changed
- Changed mailer library from Bamboo to Swoosh, should fix emails being considered spam. **Some configuration changes are required, see below.**
- Expose some fields to ActivityStreams event representation: `isOnline`, `remainingAttendeeCapacity` and `participantCount` - Expose some fields to ActivityStreams event representation: `isOnline`, `remainingAttendeeCapacity` and `participantCount`
- Expose a new field to ActivityStreams group representation: `memberCount` - Expose a new field to ActivityStreams group representation: `memberCount`
- Improve group creation errors feedback - Improve group creation errors feedback
- Only display locality in event card
- Stale groups are now excluded from group search
- Event default visibility is now set according to group privacy setting
- Remove Koena Connect button
- Hide the whole metadata block if group has no description
- Increase task timeout in Refresher to 60 seconds
- Allow webfinger to be fetched over http (not https) in dev mode
- Improve reactions when approving/rejecting an instance follow
- Improve instance admin view for mobile
- Allow to reject instance following
- Allow instance to have non-standard ports
- Add pagination to the instances list
- Eventually fetch actors in mentions
- Improve IdentityPicker, JoinGroupWithAccount and ActorInline components
- Various group and posts improvements
- Update schema.graphql file
- Add "Accept-Language" header to sentry request metadata
- Hide address blocks when address has no real data
- Remove obsolete attribute type="text/css" from <style> tags
- Improve actor cards integration
- Use upstream dependencies for Ueberauth providers
- Include ongoing events in search
- Send push notification into own task
- Add appropriate timeouts for Repo.transactions
- Add a proper error message when adding an instance follow that doesn't respond
- Allow the instance to be followed from Mastodon (through relays)
- Remove unused fragment from FETCH_PERSON GraphQL query
### Fixed ### Fixed
@ -27,6 +61,216 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Make sure every ICS/Feed caches are emptied when modifying entities - Make sure every ICS/Feed caches are emptied when modifying entities
- Fixed time issues with DST changes - Fixed time issues with DST changes
- Fixed group preview card not truncating description - Fixed group preview card not truncating description
- Fixed redirection after login
- Fixed user admin section showing button to confirm user when the user is already confirmed
- Fixed creating event from group view not always setting the group as organizer
- Fixed invalid addresses blocking event metadata preview rendering
- Fixed group deletion with comments that caused foreign key issues
- Fixed incoming Accept activities from participations we don't already have
- Fixed resources that didn't have metadata size limits
- Properly fallback to UTC when sending notifications and the user doesn't have a timezone setting set
- Fix posts creation
- Fix rejecting instance follow
- Fix pagination of group events
- Add proper fallback for when a TZ isn't registered
- Hide side of report modal on low width screens
- Fix Telegram Logo being replaced with Mastodon logo in ShareGroupModal
- Change URL for Mastodon Share Manager
- Fix receiving Flag activities on federated events
- Fix activity notifications by preloading user.activity_settings
- Fix text overflow on group card description
- Exclude tags with more than 40 characters from being extracted
- Avoid duplicate tags with different casing
- Fix invalid HTML (<div> inside <label>)
- Fix latest group not refreshing in admin section
- Add missing "relay@" part of federated address to follow
- Fix Ueberauth use of CSRF with session
- Fix being an administrator when using 3rd-party auth provider
- Make sure activity recipient can't be nil
- Make sure users can't create profiles or groups with non-valid patterns
- Add description field to address representation
- Make sure prompt show the correct message and not just "Continue?" in mix mode
- Make sure activity notification recaps can't be sent multiple times
- Fix group notification of new event being sent multiple times
- Fix links to group page in group membership emails and participation
- Fix clicking on map crashing the app
### Translations
- Arabic
- Basque
- Belarusian
- Bengali
- Catalan
- Chinese (Traditional)
- Croatian
- Czech
- Danish
- Dutch
- Esperanto
- Finnish
- French
- Gaelic
- Galician
- German
- Hebrew
- Hungarian
- Indonesian
- Italian
- Japanese
- Kabyle
- Kannada
- Norwegian Nynorsk
- Occitan
- Persian
- Polish
- Portuguese
- Portuguese (Brazil)
- Russian
- Slovenian
- Spanish
- Swedish
- Welsh
## 2.1.0-rc.6 - 2022-05-11
Changes since rc.5:
- Allow the instance to be followed from Mastodon (through relays)
- Make sure activity recipient can't be nil
- Make sure users can't create profiles or groups with non-valid patterns
- Add description field to address representation
- Make sure prompt show the correct message and not just "Continue?" in mix mode
- Add a CLI command to delete actors
- Make sure activity notification recaps can't be sent multiple times
- Fix group notification of new event being sent multiple times
- Fix links to group page in group membership emails and participation
- Fix clicking on map crashing the app
- Remove unused fragment from FETCH_PERSON GraphQL query
## 2.1.0-rc.5 - 2022-05-06
Changes since rc.4:
- Add appropriate timeouts for Repo.transactions
- Remove OS-specific packages
- Remove refresh instance triggers
- Add a proper error message when adding an instance follow that doesn't respond
## 2.1.0-rc.4 - 2022-05-03
Changes since rc.3:
- Use upstream dependencies for Ueberauth providers
- Fix Ueberauth use of CSRF with session
- Fix being an administrator when using 3rd-party auth provider
- Include ongoing events in search
- Send push notification into own task
- Add Überauth CAS Strategy
## 2.1.0-rc.3 - 2022-04-24
Changes since rc.2:
- Fix activity notifications by preloading user.activity_settings
- Add "Accept-Language" header to sentry request metadata
- Hide address blocks when address has no real data
- Fix text overflow on group card description
- Exclude tags with more than 40 characters from being extracted
- Avoid duplicate tags with different casing
- Fix invalid HTML (<div> inside <label>)
- Remove attribute type="text/css" from <style> tags
- Improve actor cards integration
- Fix latest group not refreshing in admin section
- Add missing "relay@" part of federated address to follow
## 2.1.0-rc.2 - 2022-04-20
Changes since rc.1:
- Hide the whole metadata block if group has no description
- Increase task timeout in Refresher to 60 seconds
- Allow webfinger to be fetched over http (not https) in dev mode
- Fix rejecting instance follow
- Allow instance to have non-standard ports
- Improve reactions when approving/rejecting an instance follow
- Improve instance admin view for mobile
- Allow to reject instance following
- Fix pagination of group events
- Add pagination to the instances list
- Upgrade deps
- Eventually fetch actors in mentions
- Add proper fallback for when a TZ isn't registered
- Improve IdentityPicker
- Hide side of report modal on low width screens
- Improve JoinGroupWithAccount component
- Various group and posts improvements
- Fix Telegram Logo being replaced with Mastodon logo in ShareGroupModal
- Change URL to Mastodon Share Manager
- Improve ActorInline component
- Avoid assuming we're on Debian-based in release build
- Fix receiving Flag activities on federated events
- Update schema.graphql file
## 2.1.0-rc.1 - 2022-04-18
Changes since beta.3:
- Fix posts creation
- Fix some typespecs
- Remove Koena Connect button
- Update dependencies
## 2.1.0-beta.3 - 2022-04-09
Changes since beta.2:
- Add Fedora and Alpine builds
## 2.1.0-beta.2 - 2022-04-08
Changes since beta.1 :
- Build release packages for several distributions (Debian Bullseye, Debian Buster, Ubuntu Focal, Ubuntu Bionic) because of libc version changes
## 2.1.0-beta.1 - 2022-04-07
### Added
- Added an event category field. Administrators can extend the pre-configured list of categories through configuration.
- Added possibility for administrators to have analytics (Matomo, Plausible supported) and error handling (Sentry supported) on front-end.
- Redesigned federation admin section with dedicated instance pages
- Allow to filter moderation reports by domain
- Added a button to go to past events of a group if it has no upcoming events
### Changed
- Changed mailer library from Bamboo to Swoosh, should fix emails being considered spam. **Some configuration changes are required, see below.**
- Expose some fields to ActivityStreams event representation: `isOnline`, `remainingAttendeeCapacity` and `participantCount`
- Expose a new field to ActivityStreams group representation: `memberCount`
- Improve group creation errors feedback
- Only display locality in event card
- Stale groups are now excluded from group search
- Event default visibility is now set according to group privacy setting
### Fixed
- Fixed actor refreshment being impossible
- Fixed ical export for undefined datetimes
- Fixed parsing links with hashtag characters
- Fixed fetching link details from Twitter
- Fixed Thunderbird accessing ICS feed endpoint with special `Accept` HTTP header
- Make sure every ICS/Feed caches are emptied when modifying entities
- Fixed time issues with DST changes
- Fixed group preview card not truncating description
- Fixed redirection after login
- Fixed user admin section showing button to confirm user when the user is already confirmed
- Fixed creating event from group view not always setting the group as organizer
- Fixed invalid addresses blocking event metadata preview rendering
- Fixed group deletion with comments that caused foreign key issues
- Fixed incoming Accept activities from participations we don't already have
- Fixed resources that didn't have metadata size limits
- Properly fallback to UTC when sending notifications and the user doesn't have a timezone setting set
### Translations ### Translations
@ -81,6 +325,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed the admin page when a group/profile/user was not found - Fixed the admin page when a group/profile/user was not found
- Fixed group members pagination on admin group profile view - Fixed group members pagination on admin group profile view
- Fixed admin edition of the instance's language - Fixed admin edition of the instance's language
### Translations ### Translations
- Croatian - Croatian
@ -253,7 +498,6 @@ Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/mai
- Slovenian - Slovenian
- Spanish - Spanish
## 2.0.0-rc.3 - 2021-11-22 ## 2.0.0-rc.3 - 2021-11-22
This lists changes since 2.0.0-rc.3. Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/main/UPGRADE.md#upgrading-from-13-to-20) file as well. This lists changes since 2.0.0-rc.3. Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/main/UPGRADE.md#upgrading-from-13-to-20) file as well.
@ -274,11 +518,13 @@ This lists changes since 2.0.0-rc.1. Please read the [UPGRADE.md](https://framag
- Improve MyEvents page description text - Improve MyEvents page description text
### Fixed ### Fixed
- Fix spacing in organizer picker - Fix spacing in organizer picker
- Increase number of close events and follow group events - Increase number of close events and follow group events
- Fix accessing user profile in admin section - Fix accessing user profile in admin section
- Set initial values for some EventMetadata elements, fixing submitting them right away with no value - Set initial values for some EventMetadata elements, fixing submitting them right away with no value
- Avoid giving an error page if the apollo futureParticipations query is undefined - Avoid giving an error page if the apollo futureParticipations query is undefined
### Translations ### Translations
- German - German
@ -299,6 +545,7 @@ This lists changes since 2.0.0-beta.2. Please read the [UPGRADE.md](https://fram
- Add "formerType" and "delete" attributes on Tombstones ActivityPub objects representation - Add "formerType" and "delete" attributes on Tombstones ActivityPub objects representation
### Fixed ### Fixed
- Fixed creating group activities when creating events with some fields - Fixed creating group activities when creating events with some fields
- Move release package at correct path for CI upload - Move release package at correct path for CI upload
- Fixed event contacts that were not exposed and fetched over federation - Fixed event contacts that were not exposed and fetched over federation
@ -317,6 +564,7 @@ This lists changes since 2.0.0-beta.2. Please read the [UPGRADE.md](https://fram
## 2.0.0-beta.2 - 2021-11-15 ## 2.0.0-beta.2 - 2021-11-15
This lists changes since 2.0.0-beta.1. Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/main/UPGRADE.md#upgrading-from-13-to-20) file as well. This lists changes since 2.0.0-beta.1. Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/main/UPGRADE.md#upgrading-from-13-to-20) file as well.
### Added ### Added
- Group followers and members get an notification email by default when a group publishes a new event (subject to activity notification settings) - Group followers and members get an notification email by default when a group publishes a new event (subject to activity notification settings)
@ -353,6 +601,7 @@ This lists changes since 2.0.0-beta.1. Please read the [UPGRADE.md](https://fram
## 2.0.0-beta.1 - 2021-11-09 ## 2.0.0-beta.1 - 2021-11-09
Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/main/UPGRADE.md#upgrading-from-13-to-20) file as well. Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/main/UPGRADE.md#upgrading-from-13-to-20) file as well.
### Added ### Added
- Added possibility to follow groups and be notified from new upcoming events - Added possibility to follow groups and be notified from new upcoming events
@ -417,6 +666,7 @@ Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/mai
### Security ### Security
- Fixed private messages sent as event replies from Mastodon that were shown publically as public comments. They are now discarded. - Fixed private messages sent as event replies from Mastodon that were shown publically as public comments. They are now discarded.
### Translations ### Translations
- Czech - Czech
@ -506,7 +756,6 @@ Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/mai
- Fixed token refreshment issues - Fixed token refreshment issues
- Fixed search from 404 page - Fixed search from 404 page
### Translations ### Translations
- Catalan - Catalan
@ -532,6 +781,7 @@ Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/mai
- Fixed group discussions with deleted comments - Fixed group discussions with deleted comments
## 1.2.2 - 2021-07-01 ## 1.2.2 - 2021-07-01
### Changed ### Changed
- Improved UI for participations when message is too long - Improved UI for participations when message is too long
@ -558,6 +808,7 @@ Please read the [UPGRADE.md](https://framagit.org/framasoft/mobilizon/-/blob/mai
- Fixed compatibility check in Notification section for service workers - Fixed compatibility check in Notification section for service workers
## 1.2.0 - 2021-06-29 ## 1.2.0 - 2021-06-29
### Added ### Added
- **Notifications for various group and event activity, both by email and browser push notifications. Daily and weekly digests are also available.** - **Notifications for various group and event activity, both by email and browser push notifications. Daily and weekly digests are also available.**
@ -925,6 +1176,7 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
- Hungarian - Hungarian
- Russian - Russian
- Spanish - Spanish
## 1.1.0-rc.1 - 2021-03-29 ## 1.1.0-rc.1 - 2021-03-29
### Added ### Added
@ -972,11 +1224,13 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
## 1.1.0-beta.6 - 2021-03-17 ## 1.1.0-beta.6 - 2021-03-17
### Fixed ### Fixed
- Fixed a typo in range/radius showing the wrong radius for close events on homepage - Fixed a typo in range/radius showing the wrong radius for close events on homepage
## 1.1.0-beta.5 - 2021-03-17 ## 1.1.0-beta.5 - 2021-03-17
### Fixed ### Fixed
- Fixed a typo in range/radius preventing close events from showing up - Fixed a typo in range/radius preventing close events from showing up
## 1.1.0-beta.4 - 2021-03-17 ## 1.1.0-beta.4 - 2021-03-17
@ -990,15 +1244,18 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
## 1.1.0-beta.3 - 2021-03-16 ## 1.1.0-beta.3 - 2021-03-16
### Fixed ### Fixed
- Handle ActivityPub Fetcher returning text that's not JSON - Handle ActivityPub Fetcher returning text that's not JSON
- Fix accessing a group profile when not a member - Fix accessing a group profile when not a member
## 1.1.0-beta.2 - 2021-03-16 ## 1.1.0-beta.2 - 2021-03-16
### Fixed ### Fixed
- Fixed geospatial configuration only being evaluated at compile-time, not at runtime - Fixed geospatial configuration only being evaluated at compile-time, not at runtime
### Translations ### Translations
- Slovenian - Slovenian
## 1.1.0-beta.1 - 2021-03-10 ## 1.1.0-beta.1 - 2021-03-10
@ -1140,23 +1397,23 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
### Special operations ### Special operations
* **Reattach media files to their entity.** - **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. 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 - Source install
`MIX_ENV=prod mix mobilizon.maintenance.fix_unattached_media_in_body` `MIX_ENV=prod mix mobilizon.maintenance.fix_unattached_media_in_body`
* Docker - Docker
`docker-compose exec mobilizon mobilizon_ctl maintenance.fix_unattached_media_in_body` `docker-compose exec mobilizon mobilizon_ctl maintenance.fix_unattached_media_in_body`
* **Refresh remote profiles to save avatars locally** - **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. 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 - Source install
`MIX_ENV=prod mix mobilizon.actors.refresh --all` `MIX_ENV=prod mix mobilizon.actors.refresh --all`
* Docker - Docker
`docker-compose exec mobilizon mobilizon_ctl actors.refresh --all` `docker-compose exec mobilizon mobilizon_ctl actors.refresh --all`
* **imagemagick and webp are now a required dependency** to build Mobilizon. - **imagemagick and webp are now a required dependency** to build Mobilizon.
Optimized versions of Mobilizon's pictures are now produced during front-end build. 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. See [the documentation](https://docs.joinmobilizon.org/administration/dependencies/#misc) to make sure these dependencies are installed.
@ -1208,6 +1465,7 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
### Translations ### Translations
Updated translations: Updated translations:
- Catalan - Catalan
- Dutch - Dutch
- English - English
@ -1380,14 +1638,15 @@ Updated translations:
### Special operations ### Special operations
* We added `application/ld+json` as acceptable MIME type for ActivityPub requests, so you'll need to recompile the `mime` library we use before recompiling Mobilizon: - We added `application/ld+json` as acceptable MIME type for ActivityPub requests, so you'll need to recompile the `mime` library we use before recompiling Mobilizon:
``` ```
MIX_ENV=prod mix deps.clean mime --build MIX_ENV=prod mix deps.clean mime --build
``` ```
* The [nginx configuration](https://framagit.org/framasoft/mobilizon/-/blob/main/support/nginx/mobilizon.conf) has been changed with improvements and support for custom error pages. - The [nginx configuration](https://framagit.org/framasoft/mobilizon/-/blob/main/support/nginx/mobilizon.conf) has been changed with improvements and support for custom error pages.
* The cmake dependency has been added (see [our documentation](https://docs.joinmobilizon.org/administration/dependencies/#basic-tools)) - The cmake dependency has been added (see [our documentation](https://docs.joinmobilizon.org/administration/dependencies/#basic-tools))
### Added ### Added
@ -1422,6 +1681,7 @@ Updated translations:
## [1.0.0-beta.3] - 2020-06-24 ## [1.0.0-beta.3] - 2020-06-24
### Special operations ### Special operations
Config has moved from `.env` files to a more traditional way to handle things in the Elixir world, with `.exs` files. Config has moved from `.env` files to a more traditional way to handle things in the Elixir world, with `.exs` files.
To migrate existing configuration, you can simply run `mix mobilizon.instance gen` and fill in the adequate values previously in `.env` files (you don't need to perform the operations to create the database). To migrate existing configuration, you can simply run `mix mobilizon.instance gen` and fill in the adequate values previously in `.env` files (you don't need to perform the operations to create the database).
@ -1431,6 +1691,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/main/support/systemd/mobilizon.service). Also make sure to remove the `EnvironmentFile=` line from the systemd service and set `Environment=MIX_ENV=prod` instead. See [the updated file](https://framagit.org/framasoft/mobilizon/blob/main/support/systemd/mobilizon.service).
### Added ### Added
- Possibility to participate to an event without an account (confirmation through email required) - Possibility to participate to an event without an account (confirmation through email required)
- Possibility to participate to a remote event (being redirected by providing federated identity) - Possibility to participate to a remote event (being redirected by providing federated identity)
- Possibility to add a note as a participant when event participation is manually validated (required when participating without an account) - Possibility to add a note as a participant when event participation is manually validated (required when participating without an account)
@ -1447,6 +1708,7 @@ Also make sure to remove the `EnvironmentFile=` line from the systemd service an
- Allow user to change language - Allow user to change language
### Changed ### Changed
- Configuration handling (see above) - Configuration handling (see above)
- Improved a bit color theme - Improved a bit color theme
- Signature validation also now checks if `Date` header has acceptable values - Signature validation also now checks if `Date` header has acceptable values
@ -1457,6 +1719,7 @@ Also make sure to remove the `EnvironmentFile=` line from the systemd service an
- Improved public event page - Improved public event page
### Fixed ### Fixed
- Fixed URL search - Fixed URL search
- Fixed content accessed through URL search being public - Fixed content accessed through URL search being public
- Fix event links in some emails - Fix event links in some emails
@ -1464,17 +1727,21 @@ Also make sure to remove the `EnvironmentFile=` line from the systemd service an
## [1.0.0-beta.2] - 2019-12-18 ## [1.0.0-beta.2] - 2019-12-18
### Special operations ### Special operations
These two operations couldn't be handled during migrations. These two operations couldn't be handled during migrations.
They are optional, but you won't be able to search or get participant stats on existing events if they are not executed. They are optional, but you won't be able to search or get participant stats on existing events if they are not executed.
These commands will be removed in Mobilizon 1.0.0-beta.3. These commands will be removed in Mobilizon 1.0.0-beta.3.
In order to populate search index for existing events, you need to run the following command (with prod environment): In order to populate search index for existing events, you need to run the following command (with prod environment):
* `mix mobilizon.setup_search`
- `mix mobilizon.setup_search`
In order to move participant stats to the event table for existing events, you need to run the following command (with prod environment): In order to move participant stats to the event table for existing events, you need to run the following command (with prod environment):
* `mix mobilizon.move_participant_stats`
- `mix mobilizon.move_participant_stats`
### Added ### Added
- Federation is active - Federation is active
- Added an interface for admins to view and manage instance followers and followings - Added an interface for admins to view and manage instance followers and followings
- Ability to comment below events - Ability to comment below events
@ -1499,6 +1766,7 @@ In order to move participant stats to the event table for existing events, you n
- Upgraded frontend and backend dependencies - Upgraded frontend and backend dependencies
### Changed ### Changed
- Move participant stats to event table **(read special instructions above)** - Move participant stats to event table **(read special instructions above)**
- Limit length (20 characters) and number (10) of tags allowed - Limit length (20 characters) and number (10) of tags allowed
- Added some backend changes and validation for field length - Added some backend changes and validation for field length
@ -1512,6 +1780,7 @@ In order to move participant stats to the event table for existing events, you n
- Also consider the PeerTube `CommentsEnabled` property to know if you can reply to an event - Also consider the PeerTube `CommentsEnabled` property to know if you can reply to an event
### Fixed ### Fixed
- Fix event URL validation and check if hostname is correct before showing it - Fix event URL validation and check if hostname is correct before showing it
- Fix participations stats on the MyEvents page - Fix participations stats on the MyEvents page
- Fix event description lists margin - Fix event description lists margin
@ -1541,8 +1810,11 @@ In order to move participant stats to the event table for existing events, you n
- Fixed event HTML representation when `GET` request has no `Accept` header - Fixed event HTML representation when `GET` request has no `Accept` header
### Security ### Security
- Sanitize event title to avoid XSS - Sanitize event title to avoid XSS
## [1.0.0-beta.1] - 2019-10-15 ## [1.0.0-beta.1] - 2019-10-15
### Added ### Added
- Initial release - Initial release

View File

@ -1,7 +1,49 @@
# Upgrading from 2.0 to 2.1
## Mailer library change
### Docker
The change is already applied. You may remove the `MOBILIZON_SMTP_HOSTNAME` environment key which is not used anymore.
### Release and source mode
In your configuration file under `config :mobilizon, Mobilizon.Web.Email.Mailer`,
- Change `Bamboo.SMTPAdapter` to `Swoosh.Adapters.SMTP`,
- rename the `server` key to `relay`
- remove the `hostname` key,
- the default value of the username and password fields is an empty string and no longer `nil`.
```diff
config :mobilizon, Mobilizon.Web.Email.Mailer,
- adapter: Bamboo.SMTPAdapter,
+ adapter: Swoosh.Adapters.SMTP,
- server: "localhost",
+ relay: "localhost",
- hostname: "localhost",
# usually 25, 465 or 587
port: 25,
- username: nil,
+ username: "",
- password: nil,
+ password: "",
# can be `:always` or `:never`
tls: :if_available,
allowed_tls_versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
retries: 1,
# can be `true`
no_mx_lookups: false,
# can be `:always`. If your smtp relay requires authentication set it to `:always`.
auth: :if_available
```
# Upgrading from 1.3 to 2.0 # Upgrading from 1.3 to 2.0
Requirements dependencies depend on the way Mobilizon is installed. Requirements dependencies depend on the way Mobilizon is installed.
## New Elixir version requirement ## New Elixir version requirement
### Docker and Release install ### Docker and Release install
You are already using latest Elixir version in the release tarball and Docker images. You are already using latest Elixir version in the release tarball and Docker images.
@ -17,17 +59,19 @@ Mobilizon 2.0 uses data based on [timezone-boundary-builder](https://github.com/
### Docker install ### Docker install
The geographic timezone data is already bundled into the image, you have nothing to do. The geographic timezone data is already bundled into the image, you have nothing to do.
### Release install ### Release install
In order to keep the release tarballs light, the geographic timezone data is not bundled directly. You need to download the data : In order to keep the release tarballs light, the geographic timezone data is not bundled directly. You need to download the data :
* either raw from Github, but **requires an extra ~1Gio of memory** to process the data
- either raw from Github, but **requires an extra ~1Gio of memory** to process the data
```sh ```sh
sudo -u mobilizon mkdir /var/lib/mobilizon/timezones sudo -u mobilizon mkdir /var/lib/mobilizon/timezones
sudo -u mobilizon ./bin/mobilizon_ctl tz_world.update sudo -u mobilizon ./bin/mobilizon_ctl tz_world.update
``` ```
* either already processed from our own distribution server - either already processed from our own distribution server
```sh ```sh
sudo -u mobilizon mkdir /var/lib/mobilizon/timezones sudo -u mobilizon mkdir /var/lib/mobilizon/timezones
@ -35,6 +79,7 @@ In order to keep the release tarballs light, the geographic timezone data is not
``` ```
In both cases, ~700Mio of disk will be used. You may use the following configuration to specify where the data is expected if you decide to change it from the default location (`/var/lib/mobilizon/timezones`) : In both cases, ~700Mio of disk will be used. You may use the following configuration to specify where the data is expected if you decide to change it from the default location (`/var/lib/mobilizon/timezones`) :
```elixir ```elixir
config :tz_world, data_dir: "/some/place" config :tz_world, data_dir: "/some/place"
``` ```
@ -42,14 +87,15 @@ config :tz_world, data_dir: "/some/place"
### Source install ### Source install
You need to download the data : You need to download the data :
* either raw from Github, but **requires an extra ~1Gio of memory** to process the data
- either raw from Github, but **requires an extra ~1Gio of memory** to process the data
```sh ```sh
sudo -u mobilizon mkdir /var/lib/mobilizon/timezones sudo -u mobilizon mkdir /var/lib/mobilizon/timezones
sudo -u mobilizon mix mobilizon.tz_world.update sudo -u mobilizon mix mobilizon.tz_world.update
``` ```
* either already processed from our own distribution server - either already processed from our own distribution server
```sh ```sh
sudo -u mobilizon mkdir /var/lib/mobilizon/timezones sudo -u mobilizon mkdir /var/lib/mobilizon/timezones
@ -57,6 +103,7 @@ You need to download the data :
``` ```
In both cases, ~700Mio of disk will be used. You may use the following configuration to specify where the data is expected: In both cases, ~700Mio of disk will be used. You may use the following configuration to specify where the data is expected:
```elixir ```elixir
config :tz_world, data_dir: "/some/place" config :tz_world, data_dir: "/some/place"
``` ```
@ -75,14 +122,18 @@ Files in this folder are temporary and are cleaned once an hour.
## New optional dependencies ## New optional dependencies
These are optional, installing them will allow Mobilizon to export to PDF and ODS as well. Mobilizon 2.0 allows to export the participant list, but more is planned. These are optional, installing them will allow Mobilizon to export to PDF and ODS as well. Mobilizon 2.0 allows to export the participant list, but more is planned.
### Docker ### Docker
Everything is included in our Docker image. Everything is included in our Docker image.
### Release and source install ### Release and source install
New optional Python dependencies: New optional Python dependencies:
* `Python` >= 3.6
* `weasyprint` for PDF export (with [a few extra dependencies](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html)) - `Python` >= 3.6
* `pyexcel-ods3` for ODS export (no extra dependencies) - `weasyprint` for PDF export (with [a few extra dependencies](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html))
- `pyexcel-ods3` for ODS export (no extra dependencies)
Both can be installed through pip. You need to enable and configure exports for PDF and ODS in the configuration afterwards. Read [the dedicated docs page about this](https://docs.joinmobilizon.org/administration/configure/exports/). Both can be installed through pip. You need to enable and configure exports for PDF and ODS in the configuration afterwards. Read [the dedicated docs page about this](https://docs.joinmobilizon.org/administration/configure/exports/).
@ -91,35 +142,41 @@ Both can be installed through pip. You need to enable and configure exports for
The 1.1 version of Mobilizon brings Elixir releases support. An Elixir release is a self-contained directory that contains all of Mobilizon's code (front-end and backend), it's dependencies, as well as the Erlang Virtual Machine and runtime (only the parts you need). As long as the release has been assembled on the same OS and architecture, it can be deploy and run straight away. [Read more about releases](https://elixir-lang.org/getting-started/mix-otp/config-and-releases.html#releases). The 1.1 version of Mobilizon brings Elixir releases support. An Elixir release is a self-contained directory that contains all of Mobilizon's code (front-end and backend), it's dependencies, as well as the Erlang Virtual Machine and runtime (only the parts you need). As long as the release has been assembled on the same OS and architecture, it can be deploy and run straight away. [Read more about releases](https://elixir-lang.org/getting-started/mix-otp/config-and-releases.html#releases).
## Comparison ## Comparison
Migrating to releases means: Migrating to releases means:
* You only get a precompiled binary, so you avoid compilation times when updating
* No need to have Elixir/NodeJS installed on the system - You only get a precompiled binary, so you avoid compilation times when updating
* Code/data/config location is more common (/opt, /var/lib, /etc) - No need to have Elixir/NodeJS installed on the system
* More efficient, as only what you need from the Elixir/Erlang standard libraries is included and all of the code is directly preloaded - Code/data/config location is more common (/opt, /var/lib, /etc)
* You can't hardcode modifications in Mobilizon's code - More efficient, as only what you need from the Elixir/Erlang standard libraries is included and all of the code is directly preloaded
- You can't hardcode modifications in Mobilizon's code
Staying on source releases means: Staying on source releases means:
* You need to recompile everything with each update
* Compiling frontend and backend has higher system requirements than just running Mobilizon - You need to recompile everything with each update
* You can change things in Mobilizon's code and recompile right away to test changes - Compiling frontend and backend has higher system requirements than just running Mobilizon
- You can change things in Mobilizon's code and recompile right away to test changes
## Releases ## Releases
If you want to migrate to releases, [we provide a full guide](https://docs.joinmobilizon.org/administration/upgrading/source_to_release/). You may do this at any time. If you want to migrate to releases, [we provide a full guide](https://docs.joinmobilizon.org/administration/upgrading/source_to_release/). You may do this at any time.
## Source install ## Source install
To stay on a source release, you just need to check the following things: To stay on a source release, you just need to check the following things:
* Rename your configuration file `config/prod.secret.exs` to `config/runtime.exs`.
* If your config file includes `server: true` under `Mobilizon.Web.Endpoint`, remove it. - Rename your configuration file `config/prod.secret.exs` to `config/runtime.exs`.
- If your config file includes `server: true` under `Mobilizon.Web.Endpoint`, remove it.
```diff ```diff
config :mobilizon, Mobilizon.Web.Endpoint, config :mobilizon, Mobilizon.Web.Endpoint,
- server: true, - server: true,
``` ```
* The uploads default directory is now `/var/lib/mobilizon/uploads`. To keep it in the previous `uploads/` directory, just add the following line to `config/runtime.exs`: - The uploads default directory is now `/var/lib/mobilizon/uploads`. To keep it in the previous `uploads/` directory, just add the following line to `config/runtime.exs`:
```elixir ```elixir
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, uploads: "uploads" config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, uploads: "uploads"
``` ```
Or you may use any other directory where the `mobilizon` user has write permissions. Or you may use any other directory where the `mobilizon` user has write permissions.
* The GeoIP database default directory is now `/var/lib/mobilizon/geo/GeoLite2-City.mmdb`. To keep it in the previous `priv/data/GeoLite2-City.mmdb` directory, just add the following line to `config/runtime.exs`: - The GeoIP database default directory is now `/var/lib/mobilizon/geo/GeoLite2-City.mmdb`. To keep it in the previous `priv/data/GeoLite2-City.mmdb` directory, just add the following line to `config/runtime.exs`:
```elixir ```elixir
config :geolix, databases: [ config :geolix, databases: [
%{ %{

View File

@ -106,13 +106,16 @@ config :mobilizon, :media_proxy,
] ]
config :mobilizon, Mobilizon.Web.Email.Mailer, config :mobilizon, Mobilizon.Web.Email.Mailer,
adapter: Bamboo.SMTPAdapter, adapter: Swoosh.Adapters.SMTP,
server: "localhost", relay: "localhost",
hostname: "localhost",
# usually 25, 465 or 587 # usually 25, 465 or 587
port: 25, port: 25,
username: nil, username: "",
password: nil, password: "",
# can be `:always` or `:never`
auth: :if_available,
# can be `true`
ssl: false,
# can be `:always` or `:never` # can be `:always` or `:never`
tls: :if_available, tls: :if_available,
allowed_tls_versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"], allowed_tls_versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
@ -212,7 +215,8 @@ config :mobilizon, :activitypub,
# One day # One day
actor_stale_period: 3_600 * 48, actor_stale_period: 3_600 * 48,
actor_key_rotation_delay: 3_600 * 48, actor_key_rotation_delay: 3_600 * 48,
sign_object_fetches: true sign_object_fetches: true,
stale_actor_search_exclusion_after: 3_600 * 24 * 7
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim
@ -343,6 +347,8 @@ config :mobilizon, :exports,
Mobilizon.Service.Export.Participants.ODS Mobilizon.Service.Export.Participants.ODS
] ]
config :mobilizon, :analytics, providers: []
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs" import_config "#{config_env()}.exs"

View File

@ -67,7 +67,7 @@ config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation # Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime config :phoenix, :plug_init_mode, :runtime
config :mobilizon, Mobilizon.Web.Email.Mailer, adapter: Bamboo.LocalAdapter config :mobilizon, Mobilizon.Web.Email.Mailer, adapter: Swoosh.Adapters.Local
# Configure your database # Configure your database
config :mobilizon, Mobilizon.Storage.Repo, config :mobilizon, Mobilizon.Storage.Repo,

View File

@ -43,9 +43,8 @@ config :mobilizon, Mobilizon.Storage.Repo,
pool_size: 10 pool_size: 10
config :mobilizon, Mobilizon.Web.Email.Mailer, config :mobilizon, Mobilizon.Web.Email.Mailer,
adapter: Bamboo.SMTPAdapter, adapter: Swoosh.Adapters.SMTP,
server: System.get_env("MOBILIZON_SMTP_SERVER", "localhost"), relay: System.get_env("MOBILIZON_SMTP_SERVER", "localhost"),
hostname: System.get_env("MOBILIZON_SMTP_HOSTNAME", "localhost"),
port: System.get_env("MOBILIZON_SMTP_PORT", "25"), port: System.get_env("MOBILIZON_SMTP_PORT", "25"),
username: System.get_env("MOBILIZON_SMTP_USERNAME", nil), username: System.get_env("MOBILIZON_SMTP_USERNAME", nil),
password: System.get_env("MOBILIZON_SMTP_PASSWORD", nil), password: System.get_env("MOBILIZON_SMTP_PASSWORD", nil),

View File

@ -54,7 +54,7 @@ config :mobilizon, :ldap,
bind_uid: System.get_env("LDAP_BIND_UID"), bind_uid: System.get_env("LDAP_BIND_UID"),
bind_password: System.get_env("LDAP_BIND_PASSWORD") bind_password: System.get_env("LDAP_BIND_PASSWORD")
config :mobilizon, Mobilizon.Web.Email.Mailer, adapter: Bamboo.TestAdapter config :mobilizon, Mobilizon.Web.Email.Mailer, adapter: Swoosh.Adapters.Test
config :mobilizon, Mobilizon.Web.Upload, filters: [], link_name: false config :mobilizon, Mobilizon.Web.Upload, filters: [], link_name: false

View File

@ -49,7 +49,7 @@ LABEL org.opencontainers.image.title="mobilizon" \
org.opencontainers.image.revision=$VCS_REF \ org.opencontainers.image.revision=$VCS_REF \
org.opencontainers.image.created=$BUILD_DATE org.opencontainers.image.created=$BUILD_DATE
RUN apk add --no-cache curl openssl ca-certificates ncurses-libs file postgresql-client libgcc libstdc++ imagemagick python3 py3-pip py3-pillow py3-cffi py3-brotli gcc musl-dev python3-dev pango libxslt-dev ttf-cantarell RUN apk add --no-cache curl openssl ca-certificates ncurses-libs file postgresql-client libgcc libstdc++ imagemagick python3 py3-pip py3-pillow py3-cffi py3-brotli gcc g++ musl-dev python3-dev pango libxslt-dev ttf-cantarell
RUN pip install weasyprint pyexcel-ods3 RUN pip install weasyprint pyexcel-ods3
RUN mkdir -p /var/lib/mobilizon/uploads && chown nobody:nobody /var/lib/mobilizon/uploads RUN mkdir -p /var/lib/mobilizon/uploads && chown nobody:nobody /var/lib/mobilizon/uploads

View File

@ -1,15 +1,11 @@
FROM elixir:latest FROM elixir:latest
LABEL maintainer="Thomas Citharel <tcit@tcit.fr>" LABEL maintainer="Thomas Citharel <tcit@tcit.fr>"
ENV REFRESHED_AT=2021-12-15 ENV REFRESHED_AT=2022-04-06
RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 cmake exiftool python3-pip python3-setuptools RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 cmake exiftool python3-pip python3-setuptools
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install nodejs -yq RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install nodejs -yq
RUN npm install -g yarn wait-on RUN npm install -g yarn wait-on
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN mix local.hex --force && mix local.rebar --force RUN mix local.hex --force && mix local.rebar --force
# Weasyprint 53 requires pango >= 1.44.0, which is not available in Stretch. RUN pip3 install -Iv weasyprint pyexcel_ods3
# TODO: Remove the version requirement when elixir:latest is based on Bullseye
# https://github.com/erlang/docker-erlang-otp/issues/362
# https://github.com/Kozea/WeasyPrint/issues/1384
RUN pip3 install -Iv weasyprint==52 pyexcel_ods3
RUN curl https://dbip.mirror.framasoft.org/files/dbip-city-lite-latest.mmdb --output GeoLite2-City.mmdb -s && mkdir -p /usr/share/GeoIP && mv GeoLite2-City.mmdb /usr/share/GeoIP/ RUN curl https://dbip.mirror.framasoft.org/files/dbip-city-lite-latest.mmdb --output GeoLite2-City.mmdb -s && mkdir -p /usr/share/GeoIP && mv GeoLite2-City.mmdb /usr/share/GeoIP/

View File

@ -1,6 +1,6 @@
{ {
"name": "mobilizon", "name": "mobilizon",
"version": "2.0.2", "version": "2.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@ -8,7 +8,7 @@
"test:unit": "LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 TZ=UTC vue-cli-service test:unit", "test:unit": "LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 TZ=UTC vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e", "test:e2e": "vue-cli-service test:e2e",
"lint": "vue-cli-service lint", "lint": "vue-cli-service lint",
"build:assets": "vue-cli-service build", "build:assets": "vue-cli-service build --report",
"build:pictures": "bash ./scripts/build/pictures.sh" "build:pictures": "bash ./scripts/build/pictures.sh"
}, },
"dependencies": { "dependencies": {
@ -16,7 +16,9 @@
"@absinthe/socket-apollo-link": "^0.2.1", "@absinthe/socket-apollo-link": "^0.2.1",
"@apollo/client": "^3.3.16", "@apollo/client": "^3.3.16",
"@mdi/font": "^6.1.95", "@mdi/font": "^6.1.95",
"@tailwindcss/line-clamp": "^0.3.0", "@sentry/tracing": "^6.16.1",
"@sentry/vue": "^6.16.1",
"@tailwindcss/line-clamp": "^0.4.0",
"@tiptap/core": "^2.0.0-beta.41", "@tiptap/core": "^2.0.0-beta.41",
"@tiptap/extension-blockquote": "^2.0.0-beta.25", "@tiptap/extension-blockquote": "^2.0.0-beta.25",
"@tiptap/extension-bold": "^2.0.0-beta.24", "@tiptap/extension-bold": "^2.0.0-beta.24",
@ -69,7 +71,9 @@
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-class-component": "^7.2.3", "vue-class-component": "^7.2.3",
"vue-i18n": "^8.14.0", "vue-i18n": "^8.14.0",
"vue-matomo": "^4.1.0",
"vue-meta": "^2.3.1", "vue-meta": "^2.3.1",
"vue-plausible": "^1.3.1",
"vue-property-decorator": "^9.0.0", "vue-property-decorator": "^9.0.0",
"vue-router": "^3.1.6", "vue-router": "^3.1.6",
"vue-scrollto": "^2.17.1", "vue-scrollto": "^2.17.1",
@ -112,7 +116,7 @@
"jest-junit": "^13.0.0", "jest-junit": "^13.0.0",
"mock-apollo-client": "^1.1.0", "mock-apollo-client": "^1.1.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"prettier-eslint": "^13.0.0", "prettier-eslint": "^14.0.0",
"sass": "^1.34.1", "sass": "^1.34.1",
"sass-loader": "^12.0.0", "sass-loader": "^12.0.0",
"ts-jest": "27", "ts-jest": "27",

View File

@ -203,6 +203,16 @@ export default class App extends Vue {
this.interval = undefined; this.interval = undefined;
} }
@Watch("config")
async initializeStatistics(config: IConfig) {
if (config) {
const { statistics } = (await import("./services/statistics")) as {
statistics: (config: IConfig, environment: Record<string, any>) => void;
};
statistics(config, { router: this.$router, version: config.version });
}
}
@Watch("$route", { immediate: true }) @Watch("$route", { immediate: true })
updateAnnouncement(route: Route): void { updateAnnouncement(route: Route): void {
const pageTitle = this.extractPageTitleFromRoute(route); const pageTitle = this.extractPageTitleFromRoute(route);
@ -221,7 +231,7 @@ export default class App extends Vue {
? this.routerView?.$refs?.componentFocusTarget ? this.routerView?.$refs?.componentFocusTarget
: this.routerView?.$el : this.routerView?.$el
) as HTMLElement; ) as HTMLElement;
if (focusTarget) { if (focusTarget && focusTarget instanceof Element) {
// Make focustarget programmatically focussable // Make focustarget programmatically focussable
focusTarget.setAttribute("tabindex", "-1"); focusTarget.setAttribute("tabindex", "-1");

View File

@ -7,61 +7,18 @@
@import "styles/vue-announcer.scss"; @import "styles/vue-announcer.scss";
@import "styles/vue-skip-to.scss"; @import "styles/vue-skip-to.scss";
// a {
// color: $violet-2;
// }
a.out, a.out,
.content a, .content a,
.ProseMirror a { .ProseMirror a {
text-decoration: underline; text-decoration: underline;
text-decoration-color: $secondary; text-decoration-color: #ed8d07;
text-decoration-thickness: 2px; text-decoration-thickness: 2px;
} }
.step-content {
height: auto;
}
main {
> section > .columns {
min-height: 50vh;
}
> section {
&.container {
min-height: 80vh;
}
}
> .container {
background: $whitest;
min-height: 70vh;
}
> #homepage {
background: $whitest;
#featured_events {
background: $whitest;
}
#picture {
.container,
.section {
background: $whitest;
}
}
> .container {
min-height: 25vh;
}
}
}
.section { .section {
padding: 1rem 1% 4rem; padding: 1rem 1% 4rem;
} }
figure img.is-rounded {
border: 1px solid $chapril_blue;
}
$color-black: #000; $color-black: #000;
.mention { .mention {
@ -104,16 +61,11 @@ body {
overflow-x: hidden; overflow-x: hidden;
} }
#mobilizon { #mobilizon > .container > .message {
> main {
background: $body-background-color;
}
> .container > .message {
margin: 1rem auto auto; margin: 1rem auto auto;
.message-header { .message-header {
button.delete { button.delete {
background: $chapril_grey; background: #4a4a4a;
}
} }
} }
} }
@ -130,7 +82,7 @@ $list-radius: $radius !default;
$list-item-border: 1px solid $border !default; $list-item-border: 1px solid $border !default;
$list-item-color: $text !default; $list-item-color: $text !default;
$list-item-active-background-color: $purple-1 !default; $list-item-active-background-color: $link !default;
$list-item-active-color: $link-invert !default; $list-item-active-color: $link-invert !default;
$list-item-hover-background-color: $background !default; $list-item-hover-background-color: $background !default;
@ -165,74 +117,16 @@ a.list-item {
.setting-title { .setting-title {
margin-top: 2rem; margin-top: 2rem;
margin-bottom: 1rem; margin-bottom: 1rem;
h1,
h2,
h3,
h4,
h5,
h6 {
background: $secondary;
color: $white;
span {
background: $secondary !important;
color: $white !important;
}
}
h2 { h2 {
display: inline; display: inline;
background: $secondary;
padding: 2px 7.5px; padding: 2px 7.5px;
text-transform: uppercase; text-transform: uppercase;
font-size: 1.25rem; font-size: 1.25rem;
} }
} }
.hero-body {
background-color: $chapril_blue_light;
}
.columns {
background: $whitest;
}
.setting-menu-item {
background-color: $yellow-4;
.router-link-active,
.router-link-active {
background-color: $info;
color: $white;
a {
color: $white;
font-weight: 600 !important;
text-decoration: none;
}
}
}
.date-component-container {
.datetime-container {
margin-right: 1em;
}
}
.time.datetime-container {
color: $white;
background: $chapril_blue_light;
span.month {
color: $whitest;
}
}
/**
footer
*/
footer.footer[data-v-40ab164b] span.select select {
background: $chapril_blue_light;
color: $footer-text-color;
}
@mixin focus() { @mixin focus() {
&:focus { &:focus {
border: 2px solid black; border: 2px solid black;

View File

@ -1,7 +1,7 @@
<template> <template>
<div <div
class="w-80 bg-white rounded-lg shadow-md flex space-x-4 items-center" class="bg-white rounded-lg flex space-x-4 items-center"
:class="{ 'flex-col p-4 sm:p-8 pb-10': !inline }" :class="{ 'flex-col p-4 shadow-md sm:p-8 pb-10 w-80': !inline }"
> >
<div> <div>
<figure class="w-12 h-12" v-if="actor.avatar"> <figure class="w-12 h-12" v-if="actor.avatar">
@ -20,8 +20,10 @@
class="ltr:-mr-0.5 rtl:-ml-0.5" class="ltr:-mr-0.5 rtl:-ml-0.5"
/> />
</div> </div>
<div :class="{ 'text-center': !inline }"> <div :class="{ 'text-center': !inline }" class="overflow-hidden w-full">
<h5 class="text-xl font-medium violet-title tracking-tight text-gray-900"> <h5
class="text-xl font-medium violet-title tracking-tight text-gray-900 whitespace-pre-line line-clamp-2"
>
{{ displayName(actor) }} {{ displayName(actor) }}
</h5> </h5>
<p class="text-gray-500 truncate" v-if="actor.name"> <p class="text-gray-500 truncate" v-if="actor.name">
@ -29,7 +31,11 @@
</p> </p>
<div <div
v-if="full" v-if="full"
:class="{ 'line-clamp-3': limit }" class="only-first-child"
:class="{
'line-clamp-3': limit,
'line-clamp-10': !limit,
}"
v-html="actor.summary" v-html="actor.summary"
/> />
</div> </div>
@ -93,3 +99,8 @@ export default class ActorCard extends Vue {
displayName = displayName; displayName = displayName;
} }
</script> </script>
<style scoped>
.only-first-child ::v-deep :not(:first-child) {
display: none;
}
</style>

View File

@ -1,16 +1,19 @@
<template> <template>
<div class="actor-inline"> <div class="inline-flex items-start">
<div class="actor-avatar"> <div class="flex-none mr-2">
<figure class="image is-24x24" v-if="actor.avatar"> <figure class="image is-48x48" v-if="actor.avatar">
<img class="is-rounded" :src="actor.avatar.url" alt="" /> <img class="is-rounded" :src="actor.avatar.url" alt="" />
</figure> </figure>
<b-icon v-else size="is-medium" icon="account-circle" /> <b-icon v-else size="is-large" icon="account-circle" />
</div> </div>
<div class="actor-name"> <div class="flex-auto">
<p> <p class="text-base line-clamp-3 md:line-clamp-2 max-w-xl">
{{ displayName(actor) }} {{ displayName(actor) }}
</p> </p>
<p class="text-sm text-gray-500 truncate">
@{{ usernameWithDomain(actor) }}
</p>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="ellipsis" class="truncate"
:title=" :title="
isDescriptionDifferentFromLocality isDescriptionDifferentFromLocality
? `${physicalAddress.description}, ${physicalAddress.locality}` ? `${physicalAddress.description}, ${physicalAddress.locality}`
@ -8,8 +8,7 @@
" "
> >
<b-icon icon="map-marker" /> <b-icon icon="map-marker" />
<span v-if="isDescriptionDifferentFromLocality"> <span v-if="physicalAddress.locality">
{{ physicalAddress.description }},
{{ physicalAddress.locality }} {{ physicalAddress.locality }}
</span> </span>
<span v-else> <span v-else>
@ -35,11 +34,3 @@ export default class InlineAddress extends Vue {
} }
} }
</script> </script>
<style lang="scss" scoped>
.ellipsis {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@ -48,13 +48,75 @@
$t("Mobilizon") $t("Mobilizon")
}}</a> }}</a>
</i18n> </i18n>
<span v-if="sentryEnabled && sentryReady">
{{
$t(
"We collect your feedback and the error information in order to improve this service."
)
}}</span
>
<span v-else>
{{ {{
$t( $t(
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):" "We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):"
) )
}} }}
</span>
</p> </p>
<div class="content"> <form
v-if="sentryEnabled && sentryReady && !submittedFeedback"
@submit.prevent="sendErrorToSentry"
>
<b-field :label="$t('What happened?')" label-for="what-happened">
<b-input
v-model="feedback"
type="textarea"
id="what-happened"
:placeholder="$t(`I've clicked on X, then on Y`)"
/>
</b-field>
<b-button icon-left="send" native-type="submit" type="is-primary">{{
$t("Send feedback")
}}</b-button>
<p class="content">
{{
$t(
"Please add as many details as possible to help identify the problem."
)
}}
</p>
</form>
<b-message type="is-danger" v-else-if="feedbackError">
<p>
{{
$t(
"Sorry, we wen't able to save your feedback. Don't worry, we'll try to fix this issue anyway."
)
}}
</p>
<i18n path="You may now close this page or {return_to_the_homepage}.">
<template #return_to_the_homepage>
<router-link :to="{ name: RouteName.HOME }">{{
$t("return to the homepage")
}}</router-link>
</template>
</i18n>
</b-message>
<b-message type="is-success" v-else-if="submittedFeedback">
<p>{{ $t("Thanks a lot, your feedback was submitted!") }}</p>
<i18n path="You may now close this page or {return_to_the_homepage}.">
<template #return_to_the_homepage>
<router-link :to="{ name: RouteName.HOME }">{{
$t("return to the homepage")
}}</router-link>
</template>
</i18n>
</b-message>
<div
class="content"
v-if="!(sentryEnabled && sentryReady) || submittedFeedback"
>
<p v-if="submittedFeedback">{{ $t("You may also:") }}</p>
<ul> <ul>
<li> <li>
<a <a
@ -65,7 +127,7 @@
</li> </li>
<li> <li>
<a <a
href="https://framagit.org/framasoft/mobilizon/-/issues/new?issuable_template=Bug" href="https://framagit.org/framasoft/mobilizon/-/issues/"
target="_blank" target="_blank"
>{{ >{{
$t("Open an issue on our bug tracker (advanced users)") $t("Open an issue on our bug tracker (advanced users)")
@ -74,7 +136,7 @@
</li> </li>
</ul> </ul>
</div> </div>
<p class="content"> <p class="content" v-if="!sentryEnabled">
{{ {{
$t( $t(
"Please add as many details as possible to help identify the problem." "Please add as many details as possible to help identify the problem."
@ -89,14 +151,14 @@
<p>{{ $t("Error stacktrace") }}</p> <p>{{ $t("Error stacktrace") }}</p>
<pre>{{ error.stack }}</pre> <pre>{{ error.stack }}</pre>
</details> </details>
<p> <p v-if="!sentryEnabled">
{{ {{
$t( $t(
"The technical details of the error can help developers solve the problem more easily. Please add them to your feedback." "The technical details of the error can help developers solve the problem more easily. Please add them to your feedback."
) )
}} }}
</p> </p>
<div class="buttons"> <div class="buttons" v-if="!sentryEnabled">
<b-tooltip <b-tooltip
:label="tooltipConfig.label" :label="tooltipConfig.label"
:type="tooltipConfig.type" :type="tooltipConfig.type"
@ -115,14 +177,20 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { CONTACT } from "@/graphql/config"; import { CONFIG } from "@/graphql/config";
import { checkProviderConfig, convertConfig } from "@/services/statistics";
import { IAnalyticsConfig, IConfig } from "@/types/config.model";
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { LOGGED_USER } from "@/graphql/user";
import { IUser } from "@/types/current-user.model";
import { ISentryConfiguration } from "@/types/analytics/sentry.model";
import { submitFeedback } from "@/services/statistics/sentry";
import RouteName from "@/router/name";
@Component({ @Component({
apollo: { apollo: {
config: { config: CONFIG,
query: CONTACT, loggedUser: LOGGED_USER,
},
}, },
metaInfo() { metaInfo() {
return { return {
@ -138,7 +206,17 @@ export default class ErrorComponent extends Vue {
copied: "success" | "error" | false = false; copied: "success" | "error" | false = false;
config!: { contact: string | null; name: string }; config!: IConfig;
feedback = "";
submittedFeedback = false;
feedbackError = false;
loggedUser!: IUser;
RouteName = RouteName;
async copyErrorToClipboard(): Promise<void> { async copyErrorToClipboard(): Promise<void> {
try { try {
@ -193,6 +271,56 @@ export default class ErrorComponent extends Vue {
document.body.removeChild(textArea); document.body.removeChild(textArea);
} }
get sentryEnabled(): boolean {
return this.sentryProvider?.enabled === true;
}
get sentryProvider(): IAnalyticsConfig | undefined {
return this.config && checkProviderConfig(this.config, "sentry");
}
get sentryConfig(): ISentryConfiguration | undefined {
if (this.sentryProvider?.configuration) {
return convertConfig(
this.sentryProvider?.configuration
) as ISentryConfiguration;
}
return undefined;
}
get sentryReady() {
const eventId = window.sessionStorage.getItem("lastEventId");
const dsn = this.sentryConfig?.dsn;
const organization = this.sentryConfig?.organization;
const project = this.sentryConfig?.project;
const host = this.sentryConfig?.host;
return eventId && dsn && organization && project && host;
}
async sendErrorToSentry() {
try {
const eventId = window.sessionStorage.getItem("lastEventId");
const dsn = this.sentryConfig?.dsn;
const organization = this.sentryConfig?.organization;
const project = this.sentryConfig?.project;
const host = this.sentryConfig?.host;
const endpoint = `https://${host}/api/0/projects/${organization}/${project}/user-feedback/`;
if (eventId && dsn && this.sentryReady) {
await submitFeedback(endpoint, dsn, {
event_id: eventId,
name:
this.loggedUser?.defaultActor?.preferredUsername || "Unknown user",
email: this.loggedUser?.email || "unknown@email.org",
comments: this.feedback,
});
this.submittedFeedback = true;
}
} catch (error) {
console.error(error);
this.feedbackError = true;
}
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -67,7 +67,6 @@
<inline-address <inline-address
dir="auto" dir="auto"
v-if="event.physicalAddress" v-if="event.physicalAddress"
class="event-subtitle"
:physical-address="event.physicalAddress" :physical-address="event.physicalAddress"
/> />
<div <div

View File

@ -36,6 +36,7 @@
> >
<router-link <router-link
v-if="event.attributedTo" v-if="event.attributedTo"
class="hover:underline"
:to="{ :to="{
name: RouteName.GROUP, name: RouteName.GROUP,
params: { params: {
@ -53,6 +54,7 @@
</router-link> </router-link>
<actor-card v-else :actor="event.organizerActor" :inline="true" /> <actor-card v-else :actor="event.organizerActor" :inline="true" />
<actor-card <actor-card
:inline="true"
:actor="contact" :actor="contact"
v-for="contact in event.contacts" v-for="contact in event.contacts"
:key="contact.id" :key="contact.id"
@ -65,6 +67,7 @@
> >
<a <a
target="_blank" target="_blank"
class="hover:underline"
rel="noopener noreferrer ugc" rel="noopener noreferrer ugc"
:href="event.onlineAddress" :href="event.onlineAddress"
:title=" :title="

View File

@ -1,6 +1,6 @@
<template> <template>
<router-link <router-link
class="event-minimalist-card-wrapper" class="event-minimalist-card-wrapper bg-white rounded-lg shadow-md"
dir="auto" dir="auto"
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }" :to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
> >

View File

@ -5,14 +5,33 @@
:placeholder="$t('Filter by profile or group name')" :placeholder="$t('Filter by profile or group name')"
v-model="actorFilter" v-model="actorFilter"
/> />
<b-radio-button <transition-group
v-model="selectedActor" tag="ul"
:native-value="availableActor" class="grid grid-cols-1 gap-y-3 m-5 max-w-md mx-auto"
class="list-item" enter-active-class="duration-300 ease-out"
enter-from-class="transform opacity-0"
enter-to-class="opacity-100"
leave-active-class="duration-200 ease-in"
leave-from-class="opacity-100"
leave-to-class="transform opacity-0"
>
<li
class="relative focus-within:shadow-lg"
v-for="availableActor in actualFilteredAvailableActors" v-for="availableActor in actualFilteredAvailableActors"
:key="availableActor.id" :key="availableActor.id"
> >
<div class="media" dir="auto"> <input
class="sr-only peer"
type="radio"
:value="availableActor"
name="availableActors"
v-model="selectedActor"
:id="`availableActor-${availableActor.id}`"
/>
<label
class="flex flex-wrap p-3 bg-white border border-gray-300 rounded-lg cursor-pointer hover:bg-gray-50 peer-checked:ring-primary peer-checked:ring-2 peer-checked:border-transparent"
:for="`availableActor-${availableActor.id}`"
>
<figure class="image is-48x48" v-if="availableActor.avatar"> <figure class="image is-48x48" v-if="availableActor.avatar">
<img <img
class="image is-rounded" class="image is-rounded"
@ -20,18 +39,14 @@
alt="" alt=""
/> />
</figure> </figure>
<b-icon <b-icon v-else size="is-large" icon="account-circle" />
class="media-left" <div>
v-else
size="is-large"
icon="account-circle"
/>
<div class="media-content">
<h3>{{ availableActor.name }}</h3> <h3>{{ availableActor.name }}</h3>
<small>{{ `@${availableActor.preferredUsername}` }}</small> <small>{{ `@${availableActor.preferredUsername}` }}</small>
</div> </div>
</div> </label>
</b-radio-button> </li>
</transition-group>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">

View File

@ -1,5 +1,8 @@
<template> <template>
<div class="organizer-picker" v-if="selectedActor"> <div
class="bg-white border border-gray-300 rounded-lg cursor-pointer"
v-if="selectedActor"
>
<!-- If we have a current actor (inline) --> <!-- If we have a current actor (inline) -->
<div <div
v-if="inline && selectedActor.id" v-if="inline && selectedActor.id"
@ -69,7 +72,8 @@
<p>{{ $t("Add a contact") }}</p> <p>{{ $t("Add a contact") }}</p>
<b-input <b-input
:placeholder="$t('Filter by name')" :placeholder="$t('Filter by name')"
v-model="contactFilter" :value="contactFilter"
@input="debounceSetFilterByName"
dir="auto" dir="auto"
/> />
<div v-if="actorMembers.length > 0"> <div v-if="actorMembers.length > 0">
@ -144,11 +148,12 @@ import EmptyContent from "../Utils/EmptyContent.vue";
import { import {
CURRENT_ACTOR_CLIENT, CURRENT_ACTOR_CLIENT,
IDENTITIES, IDENTITIES,
LOGGED_USER_MEMBERSHIPS, PERSON_GROUP_MEMBERSHIPS,
} from "../../graphql/actor"; } from "../../graphql/actor";
import { Paginate } from "../../types/paginate"; import { Paginate } from "../../types/paginate";
import { GROUP_MEMBERS } from "@/graphql/member"; import { GROUP_MEMBERS } from "@/graphql/member";
import { ActorType, MemberRole } from "@/types/enums"; import { ActorType, MemberRole } from "@/types/enums";
import debounce from "lodash/debounce";
const MEMBER_ROLES = [ const MEMBER_ROLES = [
MemberRole.CREATOR, MemberRole.CREATOR,
@ -179,15 +184,17 @@ const MEMBER_ROLES = [
}, },
}, },
currentActor: CURRENT_ACTOR_CLIENT, currentActor: CURRENT_ACTOR_CLIENT,
userMemberships: { personMemberships: {
query: LOGGED_USER_MEMBERSHIPS, query: PERSON_GROUP_MEMBERSHIPS,
variables() { variables() {
return { return {
id: this.currentActor?.id,
page: 1, page: 1,
limit: 10, limit: 10,
groupId: this.$route.query?.actorId,
}; };
}, },
update: (data) => data.loggedUser.memberships, update: (data) => data.person.memberships,
}, },
identities: IDENTITIES, identities: IDENTITIES,
}, },
@ -197,6 +204,9 @@ export default class OrganizerPickerWrapper extends Vue {
@Prop({ default: true, type: Boolean }) inline!: boolean; @Prop({ default: true, type: Boolean }) inline!: boolean;
@Prop({ type: Array, required: false, default: () => [] })
contacts!: IActor[];
currentActor!: IPerson; currentActor!: IPerson;
identities!: IPerson[]; identities!: IPerson[];
@ -207,13 +217,17 @@ export default class OrganizerPickerWrapper extends Vue {
usernameWithDomain = usernameWithDomain; usernameWithDomain = usernameWithDomain;
@Prop({ type: Array, required: false, default: () => [] })
contacts!: IActor[];
members: Paginate<IMember> = { elements: [], total: 0 }; members: Paginate<IMember> = { elements: [], total: 0 };
membersPage = 1; membersPage = 1;
userMemberships: Paginate<IMember> = { elements: [], total: 0 }; personMemberships: Paginate<IMember> = { elements: [], total: 0 };
data(): Record<string, unknown> {
return {
debounceSetFilterByName: debounce(this.setContactFilter, 1000),
};
}
get actualContacts(): (string | undefined)[] { get actualContacts(): (string | undefined)[] {
return this.contacts.map(({ id }) => id); return this.contacts.map(({ id }) => id);
@ -226,15 +240,17 @@ export default class OrganizerPickerWrapper extends Vue {
); );
} }
@Watch("userMemberships") setContactFilter(contactFilter: string) {
this.contactFilter = contactFilter;
}
@Watch("personMemberships")
setInitialActor(): void { setInitialActor(): void {
if (this.$route.query?.actorId) { if (
const actorId = this.$route.query?.actorId as string; this.personMemberships?.elements[0]?.parent?.id ===
const actor = this.userMemberships.elements.find( this.$route.query?.actorId
({ parent: { id }, role }) => ) {
actorId === id && MEMBER_ROLES.includes(role) this.selectedActor = this.personMemberships?.elements[0]?.parent;
)?.parent as IActor;
this.selectedActor = actor;
} }
} }
@ -276,7 +292,7 @@ export default class OrganizerPickerWrapper extends Vue {
actor.preferredUsername.toLowerCase(), actor.preferredUsername.toLowerCase(),
actor.name?.toLowerCase(), actor.name?.toLowerCase(),
actor.domain?.toLowerCase(), actor.domain?.toLowerCase(),
].some((match) => match?.includes(this.contactFilter.toLowerCase())); ];
}); });
} }

View File

@ -190,7 +190,7 @@ export default class ShareEventModal extends Vue {
} }
get mastodonShareUrl(): string { get mastodonShareUrl(): string {
return `https://toot.karamoff.dev/?text=${encodeURIComponent( return `https://toot.kytta.dev/?text=${encodeURIComponent(
this.basicTextToEncode this.basicTextToEncode
)}`; )}`;
} }

View File

@ -31,15 +31,11 @@
</span> </span>
</div> </div>
</div> </div>
<div <div class="mb-2 line-clamp-3" dir="auto" v-html="group.summary" />
class="content mb-2 line-clamp-3"
dir="auto"
v-html="group.summary"
/>
<div> <div>
<inline-address <inline-address
class="has-text-grey-dark" class="has-text-grey-dark"
v-if="group.physicalAddress" v-if="group.physicalAddress && addressFullName(group.physicalAddress)"
:physicalAddress="group.physicalAddress" :physicalAddress="group.physicalAddress"
/> />
<p class="has-text-grey-dark"> <p class="has-text-grey-dark">
@ -65,6 +61,7 @@ import { displayName, IGroup, usernameWithDomain } from "@/types/actor";
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue"; import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import InlineAddress from "@/components/Address/InlineAddress.vue"; import InlineAddress from "@/components/Address/InlineAddress.vue";
import { addressFullName } from "@/types/address.model";
@Component({ @Component({
components: { components: {
@ -80,6 +77,8 @@ export default class GroupCard extends Vue {
usernameWithDomain = usernameWithDomain; usernameWithDomain = usernameWithDomain;
displayName = displayName; displayName = displayName;
addressFullName = addressFullName;
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -10,7 +10,7 @@
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import RedirectWithAccount from "@/components/Utils/RedirectWithAccount.vue"; import RedirectWithAccount from "@/components/Utils/RedirectWithAccount.vue";
import { FETCH_GROUP } from "@/graphql/group"; import { FETCH_GROUP } from "@/graphql/group";
import { IGroup } from "@/types/actor"; import { displayName, IGroup } from "@/types/actor";
@Component({ @Component({
components: { RedirectWithAccount }, components: { RedirectWithAccount },
@ -52,7 +52,7 @@ export default class JoinGroupWithAccount extends Vue {
} }
get groupTitle(): undefined | string { get groupTitle(): undefined | string {
return this.group?.name || this.group?.preferredUsername; return this.group && displayName(this.group);
} }
sentence = this.$t( sentence = this.$t(

View File

@ -117,7 +117,7 @@ import { Component, Prop, Vue, Ref } from "vue-property-decorator";
import { GroupVisibility } from "@/types/enums"; import { GroupVisibility } from "@/types/enums";
import DiasporaLogo from "../Share/DiasporaLogo.vue"; import DiasporaLogo from "../Share/DiasporaLogo.vue";
import MastodonLogo from "../Share/MastodonLogo.vue"; import MastodonLogo from "../Share/MastodonLogo.vue";
import TelegramLogo from "../Share/MastodonLogo.vue"; import TelegramLogo from "../Share/TelegramLogo.vue";
import { displayName, IGroup } from "@/types/actor"; import { displayName, IGroup } from "@/types/actor";
@Component({ @Component({
@ -177,7 +177,7 @@ export default class ShareGroupModal extends Vue {
} }
get mastodonShareUrl(): string { get mastodonShareUrl(): string {
return `https://toot.karamoff.dev/?text=${encodeURIComponent( return `https://toot.kytta.dev/?text=${encodeURIComponent(
this.basicTextToEncode this.basicTextToEncode
)}`; )}`;
} }

View File

@ -142,8 +142,10 @@ export default class Map extends Vue {
} }
updateDraggableMarkerPosition(e: LatLng): void { updateDraggableMarkerPosition(e: LatLng): void {
if (this.updateDraggableMarkerCallback) {
this.updateDraggableMarkerCallback(e, this.zoom); this.updateDraggableMarkerCallback(e, this.zoom);
} }
}
updateZoom(zoom: number): void { updateZoom(zoom: number): void {
this.zoom = zoom; this.zoom = zoom;

View File

@ -56,21 +56,6 @@
>{{ $t("Create") }}</b-button >{{ $t("Create") }}</b-button
> >
</b-navbar-item> </b-navbar-item>
<b-navbar-item
v-if="config && config.features.koenaConnect"
class="koena"
tag="a"
href="https://mediation.koena.net/framasoft/mobilizon/"
target="_blank"
rel="noopener external"
hreflang="fr"
>
<img
src="/img/koena-a11y.svg"
width="150"
alt="Contact accessibilité"
/>
</b-navbar-item>
</template> </template>
<template slot="end"> <template slot="end">
<b-navbar-item <b-navbar-item
@ -394,15 +379,6 @@ nav {
background-color: inherit; background-color: inherit;
} }
.koena {
padding-top: 0;
padding-bottom: 0;
& > img {
max-height: 4rem;
padding-top: 0.2rem;
}
}
.identity-wrapper { .identity-wrapper {
display: flex; display: flex;

View File

@ -9,7 +9,7 @@
:rounded="true" :rounded="true"
style="height: 120px" style="height: 120px"
/> />
<div class="title-info-wrapper has-text-grey-dark"> <div class="title-info-wrapper has-text-grey-dark px-1">
<h3 class="post-minimalist-title" :lang="post.language"> <h3 class="post-minimalist-title" :lang="post.language">
{{ post.title }} {{ post.title }}
</h3> </h3>

View File

@ -179,7 +179,7 @@ export default class SharePostModal extends Vue {
} }
get mastodonShareUrl(): string { get mastodonShareUrl(): string {
return `https://toot.karamoff.dev/?text=${encodeURIComponent( return `https://toot.kytta.dev/?text=${encodeURIComponent(
this.basicTextToEncode this.basicTextToEncode
)}`; )}`;
} }

View File

@ -9,7 +9,7 @@
:class="{ 'is-titleless': !title }" :class="{ 'is-titleless': !title }"
> >
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left hidden md:block">
<b-icon icon="alert" type="is-warning" size="is-large" /> <b-icon icon="alert" type="is-warning" size="is-large" />
</div> </div>
<div class="media-content"> <div class="media-content">

View File

@ -45,7 +45,7 @@
</a> </a>
<resource-dropdown <resource-dropdown
class="actions" class="actions"
v-if="!inline || !preview" v-if="!inline && !preview"
@delete="$emit('delete', resource.id)" @delete="$emit('delete', resource.id)"
@move="$emit('move', resource)" @move="$emit('move', resource)"
@rename="$emit('rename', resource)" @rename="$emit('rename', resource)"

View File

@ -1,7 +1,7 @@
<template> <template>
<div v-if="resource"> <div v-if="resource">
<article class="panel is-primary"> <article class="panel is-primary">
<p class="panel-heading"> <p class="panel-heading truncate">
{{ {{
$t('Move "{resourceName}"', { resourceName: initialResource.title }) $t('Move "{resourceName}"', { resourceName: initialResource.title })
}} }}
@ -28,7 +28,7 @@
</a> </a>
<template v-if="resource.children"> <template v-if="resource.children">
<a <a
class="panel-block" class="panel-block flex-wrap"
v-for="element in resource.children.elements" v-for="element in resource.children.elements"
:class="{ :class="{
clickable: clickable:
@ -37,6 +37,7 @@
:key="element.id" :key="element.id"
@click="goDown(element)" @click="goDown(element)"
> >
<p class="truncate">
<span class="panel-icon"> <span class="panel-icon">
<b-icon <b-icon
icon="folder" icon="folder"
@ -45,7 +46,8 @@
/> />
<b-icon icon="link" size="is-small" v-else /> <b-icon icon="link" size="is-small" v-else />
</span> </span>
{{ element.title }} <span>{{ element.title }}</span>
</p>
<span v-if="element.id === initialResource.id"> <span v-if="element.id === initialResource.id">
<em v-if="element.type === 'folder'"> {{ $t("(this folder)") }}</em> <em v-if="element.type === 'folder'"> {{ $t("(this folder)") }}</em>
<em v-else> {{ $t("(this link)") }}</em> <em v-else> {{ $t("(this link)") }}</em>
@ -89,7 +91,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator"; import { Component, Vue, Prop, Watch } from "vue-property-decorator";
import { GET_RESOURCE } from "../../graphql/resources"; import { GET_RESOURCE } from "../../graphql/resources";
import { IResource } from "../../types/resource"; import { IResource } from "../../types/resource";
@ -119,7 +121,7 @@ export default class ResourceSelector extends Vue {
@Prop({ required: true }) username!: string; @Prop({ required: true }) username!: string;
resource: IResource | undefined = this.initialResource.parent; resource: IResource | undefined = undefined;
RESOURCES_PER_PAGE = 10; RESOURCES_PER_PAGE = 10;
@ -131,6 +133,20 @@ export default class ResourceSelector extends Vue {
} }
} }
data() {
return {
resource: this.initialResource?.parent,
};
}
@Watch("initialResource")
updateResourceFromProp() {
if (this.initialResource) {
this.resource = this.initialResource?.parent;
this.$apollo.queries.resource.refetch();
}
}
updateResource(): void { updateResource(): void {
this.$emit( this.$emit(
"update-resource", "update-resource",

View File

@ -1,18 +1,22 @@
<template> <template>
<label for="navSearchField"> <b-field label-for="navSearchField" class="-mt-2">
<span class="visually-hidden">{{ defaultPlaceHolder }}</span>
<b-input <b-input
custom-class="searchField" :placeholder="defaultPlaceHolder"
type="search"
id="navSearchField" id="navSearchField"
icon="magnify" icon="magnify"
type="search" icon-clickable
rounded rounded
custom-class="searchField"
dir="auto" dir="auto"
:placeholder="defaultPlaceHolder"
v-model="search" v-model="search"
@keyup.native.enter="enter" @keyup.native.enter="enter"
/> >
</label> </b-input>
<template #label>
<span class="sr-only">{{ defaultPlaceHolder }}</span>
</template>
</b-field>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
@ -47,6 +51,7 @@ label span.visually-hidden {
input.searchField { input.searchField {
box-shadow: none; box-shadow: none;
border-color: #b5b5b5; border-color: #b5b5b5;
border-radius: 9999px !important;
&::placeholder { &::placeholder {
color: gray; color: gray;

View File

@ -1,11 +1,15 @@
<template> <template>
<div class="empty-content" :class="{ inline }" role="note"> <div
class="empty-content"
:class="{ inline, 'text-center': center }"
role="note"
>
<b-icon :icon="icon" size="is-large" /> <b-icon :icon="icon" size="is-large" />
<h2 class="empty-content__title"> <h2 class="empty-content__title">
<!-- @slot Mandatory title --> <!-- @slot Mandatory title -->
<slot /> <slot />
</h2> </h2>
<p v-show="$slots.desc"> <p v-show="$slots.desc" :class="descriptionClasses">
<!-- @slot Optional description --> <!-- @slot Optional description -->
<slot name="desc" /> <slot name="desc" />
</p> </p>
@ -17,7 +21,10 @@ import { Component, Prop, Vue } from "vue-property-decorator";
@Component @Component
export default class EmptyContent extends Vue { export default class EmptyContent extends Vue {
@Prop({ type: String, required: true }) icon!: string; @Prop({ type: String, required: true }) icon!: string;
@Prop({ type: String, required: false, default: "" })
descriptionClasses!: string;
@Prop({ type: Boolean, required: false, default: false }) inline!: boolean; @Prop({ type: Boolean, required: false, default: false }) inline!: boolean;
@Prop({ type: Boolean, required: false, default: false }) center!: boolean;
} }
</script> </script>

View File

@ -34,16 +34,6 @@ export const FETCH_PERSON = gql`
feedTokens { feedTokens {
token token
} }
organizedEvents {
total
elements {
id
uuid
title
beginsOn
status
}
}
} }
} }
${ACTOR_FRAGMENT} ${ACTOR_FRAGMENT}
@ -351,6 +341,30 @@ export const PERSON_STATUS_GROUP = gql`
${ACTOR_FRAGMENT} ${ACTOR_FRAGMENT}
`; `;
export const PERSON_GROUP_MEMBERSHIPS = gql`
query PersonGroupMemberships($id: ID!, $groupId: ID!) {
person(id: $id) {
id
memberships(groupId: $groupId) {
total
elements {
id
role
parent {
...ActorFragment
}
invitedBy {
...ActorFragment
}
insertedAt
updatedAt
}
}
}
}
${ACTOR_FRAGMENT}
`;
export const GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED = gql` export const GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED = gql`
subscription GroupMembershipSubscriptionChanged( subscription GroupMembershipSubscriptionChanged(
$actorId: ID! $actorId: ID!

View File

@ -74,6 +74,7 @@ export const INSTANCE_FRAGMENT = gql`
fragment InstanceFragment on Instance { fragment InstanceFragment on Instance {
domain domain
hasRelay hasRelay
relayAddress
followerStatus followerStatus
followedStatus followedStatus
eventCount eventCount
@ -131,15 +132,6 @@ export const ADD_INSTANCE = gql`
${INSTANCE_FRAGMENT} ${INSTANCE_FRAGMENT}
`; `;
export const ADD_RELAY = gql`
mutation addRelay($address: String!) {
addRelay(address: $address) {
...relayFragment
}
}
${RELAY_FRAGMENT}
`;
export const REMOVE_RELAY = gql` export const REMOVE_RELAY = gql`
mutation removeRelay($address: String!) { mutation removeRelay($address: String!) {
removeRelay(address: $address) { removeRelay(address: $address) {

View File

@ -71,7 +71,6 @@ export const CONFIG = gql`
features { features {
groups groups
eventCreation eventCreation
koenaConnect
} }
restrictions { restrictions {
onlyAdminCanCreateGroups onlyAdminCanCreateGroups
@ -96,6 +95,15 @@ export const CONFIG = gql`
enabled enabled
publicKey publicKey
} }
analytics {
id
enabled
configuration {
key
value
type
}
}
} }
} }
`; `;

View File

@ -226,6 +226,15 @@ export const FETCH_GROUP = gql`
${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT} ${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT}
`; `;
export const FETCH_GROUP_BY_ID = gql`
query FetchGroupById($id: ID!) {
groupById(id: $name) {
...GroupFullFields
}
}
${GROUP_FIELDS_FRAGMENTS}
`;
export const GET_GROUP = gql` export const GET_GROUP = gql`
query GetGroup( query GetGroup(
$id: ID! $id: ID!

View File

@ -98,7 +98,7 @@ export const FETCH_POST = gql`
export const CREATE_POST = gql` export const CREATE_POST = gql`
mutation CreatePost( mutation CreatePost(
$title: String! $title: String!
$body: String $body: String!
$attributedToId: ID! $attributedToId: ID!
$visibility: PostVisibility $visibility: PostVisibility
$draft: Boolean $draft: Boolean

View File

@ -103,6 +103,7 @@
"Anonymous participant": "Anonymní účastník", "Anonymous participant": "Anonymní účastník",
"Anonymous participants will be asked to confirm their participation through e-mail.": "Anonymní účastníci budou požádáni o potvrzení své účasti e-mailem.", "Anonymous participants will be asked to confirm their participation through e-mail.": "Anonymní účastníci budou požádáni o potvrzení své účasti e-mailem.",
"Anonymous participations": "Anonymní účasti", "Anonymous participations": "Anonymní účasti",
"Any category": "Jakákoliv kategorie",
"Any day": "Jakýkoliv den", "Any day": "Jakýkoliv den",
"Any type": "Jakýkoli typ", "Any type": "Jakýkoli typ",
"Anyone can join freely": "Každý se může volně připojit", "Anyone can join freely": "Každý se může volně připojit",
@ -158,6 +159,7 @@
"Cancel my participation…": "Zrušit moji účast…", "Cancel my participation…": "Zrušit moji účast…",
"Cancelled": "Zrušeno", "Cancelled": "Zrušeno",
"Cancelled: Won't happen": "Zrušeno: Neuskuteční se", "Cancelled: Won't happen": "Zrušeno: Neuskuteční se",
"Category": "Kategorie",
"Change": "Změnit", "Change": "Změnit",
"Change email": "Změnit e-mail", "Change email": "Změnit e-mail",
"Change my email": "Změnit můj email", "Change my email": "Změnit můj email",
@ -818,6 +820,7 @@
"Search": "Hledat", "Search": "Hledat",
"Search events, groups, etc.": "Hledání událostí, skupin atd.", "Search events, groups, etc.": "Hledání událostí, skupin atd.",
"Searching…": "Hledání…", "Searching…": "Hledání…",
"Select a category": "Vybrat kategorii",
"Select a language": "Výběr jazyka", "Select a language": "Výběr jazyka",
"Select a radius": "Vybrat poloměr", "Select a radius": "Vybrat poloměr",
"Select a timezone": "Výběr časového pásma", "Select a timezone": "Výběr časového pásma",

View File

@ -158,6 +158,7 @@
"Cancel my participation…": "Meine Teilnahme stornieren…", "Cancel my participation…": "Meine Teilnahme stornieren…",
"Cancelled": "Abgesagt", "Cancelled": "Abgesagt",
"Cancelled: Won't happen": "Abgesagt: Wird nicht stattfinden", "Cancelled: Won't happen": "Abgesagt: Wird nicht stattfinden",
"Category": "Kategorie",
"Change": "Ändern", "Change": "Ändern",
"Change email": "E-Mail ändern", "Change email": "E-Mail ändern",
"Change my email": "Meine E-Mail ändern", "Change my email": "Meine E-Mail ändern",
@ -284,7 +285,7 @@
"Draft": "Entwurf", "Draft": "Entwurf",
"Drafts": "Entwürfe", "Drafts": "Entwürfe",
"Due on": "Fällig am", "Due on": "Fällig am",
"Duplicate": "Duplikat", "Duplicate": "Duplizieren",
"Edit": "Bearbeiten", "Edit": "Bearbeiten",
"Edit post": "Beitrag bearbeiten", "Edit post": "Beitrag bearbeiten",
"Edit profile {profile}": "Profil {profile} bearbeiten", "Edit profile {profile}": "Profil {profile} bearbeiten",
@ -390,7 +391,7 @@
"Gather ⋅ Organize ⋅ Mobilize": "Treffen ⋅ Organisieren ⋅ Mobilisieren", "Gather ⋅ Organize ⋅ Mobilize": "Treffen ⋅ Organisieren ⋅ Mobilisieren",
"General": "Allgemein", "General": "Allgemein",
"General information": "Allgemeine Informationen", "General information": "Allgemeine Informationen",
"General settings": "Allgemeine EInstellungen", "General settings": "Allgemeine Einstellungen",
"Geolocation was not determined in time.": "Geolokalisierung konnte nicht rechtzeitig ermittelt werden.", "Geolocation was not determined in time.": "Geolokalisierung konnte nicht rechtzeitig ermittelt werden.",
"Getting location": "Standort ermitteln", "Getting location": "Standort ermitteln",
"Getting there": "Hin kommen", "Getting there": "Hin kommen",
@ -427,7 +428,7 @@
"Homepage": "Website", "Homepage": "Website",
"Hourly email summary": "Stündliche E-Mail-Zusammenfassungen", "Hourly email summary": "Stündliche E-Mail-Zusammenfassungen",
"I agree to the {instanceRules} and {termsOfService}": "Ich stimme den {instanceRules} und den {termsOfService} zu", "I agree to the {instanceRules} and {termsOfService}": "Ich stimme den {instanceRules} und den {termsOfService} zu",
"I create an identity": "Ich erstelle eine Identität", "I create an identity": "Neue Identität erstellen",
"I don't have a Mobilizon account": "Ich habe kein Mobilizon Konto", "I don't have a Mobilizon account": "Ich habe kein Mobilizon Konto",
"I have a Mobilizon account": "Ich habe ein Mobilizon Konto", "I have a Mobilizon account": "Ich habe ein Mobilizon Konto",
"I have an account on another Mobilizon instance.": "Ich habe ein Konto auf einer anderen Mobilizon-Instanz.", "I have an account on another Mobilizon instance.": "Ich habe ein Konto auf einer anderen Mobilizon-Instanz.",

View File

@ -1309,5 +1309,26 @@
"Reset filters": "Reset filters", "Reset filters": "Reset filters",
"Category": "Category", "Category": "Category",
"Select a category": "Select a category", "Select a category": "Select a category",
"Any category": "Any category" "Any category": "Any category",
"We collect your feedback and the error information in order to improve this service.": "We collect your feedback and the error information in order to improve this service.",
"What happened?": "What happened?",
"I've clicked on X, then on Y": "I've clicked on X, then on Y",
"Send feedback": "Send feedback",
"Sorry, we wen't able to save your feedback. Don't worry, we'll try to fix this issue anyway.": "Sorry, we wen't able to save your feedback. Don't worry, we'll try to fix this issue anyway.",
"return to the homepage": "return to the homepage",
"Thanks a lot, your feedback was submitted!": "Thanks a lot, your feedback was submitted!",
"You may also:": "You may also:",
"You may now close this page or {return_to_the_homepage}.": "You may now close this page or {return_to_the_homepage}.",
"This group is a remote group, it's possible the original instance has more informations.": "This group is a remote group, it's possible the original instance has more informations.",
"View the group profile on the original instance": "View the group profile on the original instance",
"View past events": "View past events",
"Get informed of the upcoming public events": "Get informed of the upcoming public events",
"Join": "Join",
"Become part of the community and start organizing events": "Become part of the community and start organizing events",
"Follow requests will be approved by a group moderator": "Follow requests will be approved by a group moderator",
"Follow request pending approval": "Follow request pending approval",
"Your membership is pending approval": "Your membership is pending approval",
"Activate notifications": "Activate notifications",
"Deactivate notifications": "Deactivate notifications",
"Membership requests will be approved by a group moderator": "Membership requests will be approved by a group moderator"
} }

View File

@ -55,6 +55,7 @@
"Account settings": "Configuración de la cuenta", "Account settings": "Configuración de la cuenta",
"Actions": "Acciones", "Actions": "Acciones",
"Activate browser push notifications": "Activar notificaciones push del navegador", "Activate browser push notifications": "Activar notificaciones push del navegador",
"Activate notifications": "Activar notificaciones",
"Activated": "Activado", "Activated": "Activado",
"Active": "Activo", "Active": "Activo",
"Activity": "Actividad", "Activity": "Actividad",
@ -103,6 +104,7 @@
"Anonymous participant": "Participante anónimo", "Anonymous participant": "Participante anónimo",
"Anonymous participants will be asked to confirm their participation through e-mail.": "Los participantes anónimos deberán confirmar su participación por correo electrónico.", "Anonymous participants will be asked to confirm their participation through e-mail.": "Los participantes anónimos deberán confirmar su participación por correo electrónico.",
"Anonymous participations": "Participaciones anónimas", "Anonymous participations": "Participaciones anónimas",
"Any category": "Cualquier categoría",
"Any day": "Cualquier día", "Any day": "Cualquier día",
"Any type": "Cualquier tipo", "Any type": "Cualquier tipo",
"Anyone can join freely": "Cualquiera puede unirse libremente", "Anyone can join freely": "Cualquiera puede unirse libremente",
@ -135,6 +137,7 @@
"Back to top": "Volver arriba", "Back to top": "Volver arriba",
"Back to user list": "Volver a la lista de usuarios", "Back to user list": "Volver a la lista de usuarios",
"Banner": "Pancarta", "Banner": "Pancarta",
"Become part of the community and start organizing events": "Forma parte de la comunidad y empieza a organizar eventos",
"Before you can login, you need to click on the link inside it to validate your account.": "Antes de iniciar sesión, debe hacer clic en el enlace que se encuentra dentro para validar su cuenta.", "Before you can login, you need to click on the link inside it to validate your account.": "Antes de iniciar sesión, debe hacer clic en el enlace que se encuentra dentro para validar su cuenta.",
"Begins on": "Comienza en", "Begins on": "Comienza en",
"Big Blue Button": "Big Blue Button", "Big Blue Button": "Big Blue Button",
@ -158,6 +161,7 @@
"Cancel my participation…": "Cancelar mi participación …", "Cancel my participation…": "Cancelar mi participación …",
"Cancelled": "Cancelado", "Cancelled": "Cancelado",
"Cancelled: Won't happen": "Cancelado: no sucederá", "Cancelled: Won't happen": "Cancelado: no sucederá",
"Category": "Categoría",
"Change": "Cambiar", "Change": "Cambiar",
"Change email": "Cambiar e-mail", "Change email": "Cambiar e-mail",
"Change my email": "Cambiar mi correo electrónico", "Change my email": "Cambiar mi correo electrónico",
@ -239,6 +243,7 @@
"Date and time": "Fecha y hora", "Date and time": "Fecha y hora",
"Date and time settings": "Configuración de fecha y hora", "Date and time settings": "Configuración de fecha y hora",
"Date parameters": "Parámetros de fecha", "Date parameters": "Parámetros de fecha",
"Deactivate notifications": "Activar notificaciones",
"Decline": "Rechazar", "Decline": "Rechazar",
"Decrease": "Disminución", "Decrease": "Disminución",
"Default": "Valor predeterminados", "Default": "Valor predeterminados",
@ -365,6 +370,8 @@
"Follow": "Seguir", "Follow": "Seguir",
"Follow a new instance": "Seguir una nueva instancia", "Follow a new instance": "Seguir una nueva instancia",
"Follow instance": "Seguir instancia", "Follow instance": "Seguir instancia",
"Follow request pending approval": "Seguir solicitud pendiente de aprobación",
"Follow requests will be approved by a group moderator": "Las solicitudes de seguimiento serán aprobadas por un moderador del grupo",
"Follow status": "Seguir estado", "Follow status": "Seguir estado",
"Followed": "Seguidos", "Followed": "Seguidos",
"Followed, pending response": "Seguido, pendiente de respuesta", "Followed, pending response": "Seguido, pendiente de respuesta",
@ -392,6 +399,7 @@
"General information": "Información general", "General information": "Información general",
"General settings": "Configuración general", "General settings": "Configuración general",
"Geolocation was not determined in time.": "La geolocalización no se determinó a tiempo.", "Geolocation was not determined in time.": "La geolocalización no se determinó a tiempo.",
"Get informed of the upcoming public events": "Infórmese de los próximos eventos públicos",
"Getting location": "Obtener ubicación", "Getting location": "Obtener ubicación",
"Getting there": "Llegar allí", "Getting there": "Llegar allí",
"Glossary": "Glosario", "Glossary": "Glosario",
@ -436,6 +444,7 @@
"I want to approve every participation request": "Quiero aprobar cada solicitud de participación", "I want to approve every participation request": "Quiero aprobar cada solicitud de participación",
"I've been mentionned in a comment under an event": "Me han mencionado en un comentario en un evento", "I've been mentionned in a comment under an event": "Me han mencionado en un comentario en un evento",
"I've been mentionned in a group discussion": "Me han mencionado en una discusión grupal", "I've been mentionned in a group discussion": "Me han mencionado en una discusión grupal",
"I've clicked on X, then on Y": "He hecho clic en X, luego en Y",
"ICS feed for events": "Flujo ICS para eventos", "ICS feed for events": "Flujo ICS para eventos",
"ICS/WebCal Feed": "Flujo ICS/WebCal", "ICS/WebCal Feed": "Flujo ICS/WebCal",
"IP Address": "Dirección IP", "IP Address": "Dirección IP",
@ -484,6 +493,7 @@
"It is possible that the content is not accessible on this instance, because this instance has blocked the profiles or groups behind this content.": "Es posible que el contenido no sea accesible en esta instancia, porque esta instancia ha bloqueado los perfiles o grupos detrás de este contenido.", "It is possible that the content is not accessible on this instance, because this instance has blocked the profiles or groups behind this content.": "Es posible que el contenido no sea accesible en esta instancia, porque esta instancia ha bloqueado los perfiles o grupos detrás de este contenido.",
"Italic": "Cursiva", "Italic": "Cursiva",
"Jitsi Meet": "Jitsi Meet", "Jitsi Meet": "Jitsi Meet",
"Join": "Unirse",
"Join <b>{instance}</b>, a Mobilizon instance": "Únase a <b> {instance} </b>, una instancia de Mobilizon", "Join <b>{instance}</b>, a Mobilizon instance": "Únase a <b> {instance} </b>, una instancia de Mobilizon",
"Join group": "Unirse al grupo", "Join group": "Unirse al grupo",
"Join group {group}": "Unirse al grupo {group}", "Join group {group}": "Unirse al grupo {group}",
@ -532,6 +542,7 @@
"Member": "Miembro", "Member": "Miembro",
"Members": "Miembros", "Members": "Miembros",
"Members-only post": "Publicación solo para miembros", "Members-only post": "Publicación solo para miembros",
"Membership requests will be approved by a group moderator": "Las solicitudes de membresía serán aprobadas por un moderador del grupo",
"Memberships": "Miembros", "Memberships": "Miembros",
"Mentions": "Menciones", "Mentions": "Menciones",
"Message": "Mensaje", "Message": "Mensaje",
@ -819,6 +830,7 @@
"Search": "Buscar", "Search": "Buscar",
"Search events, groups, etc.": "Buscar eventos, grupos, etc.", "Search events, groups, etc.": "Buscar eventos, grupos, etc.",
"Searching…": "Buscando…", "Searching…": "Buscando…",
"Select a category": "Seleccione una categoría",
"Select a language": "Selecciona un idioma", "Select a language": "Selecciona un idioma",
"Select a radius": "Seleccione un radio", "Select a radius": "Seleccione un radio",
"Select a timezone": "Selecciona una zona horaria", "Select a timezone": "Selecciona una zona horaria",
@ -826,6 +838,7 @@
"Select the activities for which you wish to receive an email or a push notification.": "Seleccione las actividades para las que desea recibir un correo electrónico o una notificación automática.", "Select the activities for which you wish to receive an email or a push notification.": "Seleccione las actividades para las que desea recibir un correo electrónico o una notificación automática.",
"Send": "Enviar", "Send": "Enviar",
"Send email": "Enviar correo electrónico", "Send email": "Enviar correo electrónico",
"Send feedback": "Enviar comentarios",
"Send notification e-mails": "Enviar correos electrónicos de notificación", "Send notification e-mails": "Enviar correos electrónicos de notificación",
"Send password reset": "Enviar restablecimiento de contraseña", "Send password reset": "Enviar restablecimiento de contraseña",
"Send the confirmation email again": "Enviar el correo electrónico de confirmación nuevamente", "Send the confirmation email again": "Enviar el correo electrónico de confirmación nuevamente",
@ -852,6 +865,7 @@
"Skip to main content": "Saltar al contenido principal", "Skip to main content": "Saltar al contenido principal",
"Social": "Social", "Social": "Social",
"Some terms, technical or otherwise, used in the text below may cover concepts that are difficult to grasp. We have provided a glossary here to help you understand them better:": "Algunos términos, técnicos o de otro tipo, utilizados en el texto a continuación pueden abarcar conceptos que son difíciles de comprender. Hemos proporcionado un glosario aquí para ayudarlo a comprenderlos mejor:", "Some terms, technical or otherwise, used in the text below may cover concepts that are difficult to grasp. We have provided a glossary here to help you understand them better:": "Algunos términos, técnicos o de otro tipo, utilizados en el texto a continuación pueden abarcar conceptos que son difíciles de comprender. Hemos proporcionado un glosario aquí para ayudarlo a comprenderlos mejor:",
"Sorry, we wen't able to save your feedback. Don't worry, we'll try to fix this issue anyway.": "Lo sentimos, no pudimos guardar sus comentarios. No se preocupe, intentaremos solucionar este problema de todos modos.",
"Starts on…": "Comienza en …", "Starts on…": "Comienza en …",
"Status": "Estado", "Status": "Estado",
"Stop following instance": "Dejar de seguir la instancia", "Stop following instance": "Dejar de seguir la instancia",
@ -871,6 +885,7 @@
"Terms": "Condiciones", "Terms": "Condiciones",
"Terms of service": "Términos de servicio", "Terms of service": "Términos de servicio",
"Text": "Texto", "Text": "Texto",
"Thanks a lot, your feedback was submitted!": "¡Muchas gracias, sus comentarios fueron enviados!",
"That you follow or of which you are a member": "Que sigues o del que eres miembro", "That you follow or of which you are a member": "Que sigues o del que eres miembro",
"The Big Blue Button video teleconference URL": "La URL de la videoconferencia de Big Blue Button", "The Big Blue Button video teleconference URL": "La URL de la videoconferencia de Big Blue Button",
"The Google Meet video teleconference URL": "La URL de la videoconferencia de Google Meet", "The Google Meet video teleconference URL": "La URL de la videoconferencia de Google Meet",
@ -944,6 +959,7 @@
"This event has been cancelled.": "Este evento ha sido cancelado.", "This event has been cancelled.": "Este evento ha sido cancelado.",
"This event is accessible only through it's link. Be careful where you post this link.": "Este evento es accesible solo a través de su enlace. Tenga cuidado donde publica este enlace.", "This event is accessible only through it's link. Be careful where you post this link.": "Este evento es accesible solo a través de su enlace. Tenga cuidado donde publica este enlace.",
"This group doesn't have a description yet.": "Este grupo aún no tiene una descripción.", "This group doesn't have a description yet.": "Este grupo aún no tiene una descripción.",
"This group is a remote group, it's possible the original instance has more informations.": "Este grupo es un grupo remoto, es posible que la instancia original tenga más información.",
"This group is accessible only through it's link. Be careful where you post this link.": "Este grupo es accesible solo a través de su enlace. Tenga cuidado donde publica este enlace.", "This group is accessible only through it's link. Be careful where you post this link.": "Este grupo es accesible solo a través de su enlace. Tenga cuidado donde publica este enlace.",
"This group is invite-only": "Este grupo es solo por invitación", "This group is invite-only": "Este grupo es solo por invitación",
"This group was not found": "No se encontró este grupo", "This group was not found": "No se encontró este grupo",
@ -1045,6 +1061,8 @@
"View less": "Ver menos", "View less": "Ver menos",
"View more": "Ver más", "View more": "Ver más",
"View page on {hostname} (in a new window)": "Ver página en {hostname} (en una nueva ventana)", "View page on {hostname} (in a new window)": "Ver página en {hostname} (en una nueva ventana)",
"View past events": "Ver eventos pasados",
"View the group profile on the original instance": "Ver el perfil del grupo en la instancia original",
"Visibility was set to an unknown value.": "La visibilidad se estableció en un valor desconocido.", "Visibility was set to an unknown value.": "La visibilidad se estableció en un valor desconocido.",
"Visibility was set to private.": "La visibilidad se estableció en privada.", "Visibility was set to private.": "La visibilidad se estableció en privada.",
"Visibility was set to public.": "La visibilidad se estableció en público.", "Visibility was set to public.": "La visibilidad se estableció en público.",
@ -1052,6 +1070,7 @@
"Visible everywhere on the web (public)": "Visible en todas partes de la web (público)", "Visible everywhere on the web (public)": "Visible en todas partes de la web (público)",
"Waiting for organization team approval.": "Esperando la aprobación del equipo de la organización.", "Waiting for organization team approval.": "Esperando la aprobación del equipo de la organización.",
"Warning": "Advertencia", "Warning": "Advertencia",
"We collect your feedback and the error information in order to improve this service.": "Recopilamos sus comentarios y la información de error para mejorar este servicio.",
"We couldn't save your participation inside this browser. Not to worry, you have successfully confirmed your participation, we just couldn't save it's status in this browser because of a technical issue.": "No pudimos guardar su participación dentro de este navegador. No se preocupe, ha confirmado con éxito su participación, simplemente no pudimos guardar su estado en este navegador debido a un problema técnico.", "We couldn't save your participation inside this browser. Not to worry, you have successfully confirmed your participation, we just couldn't save it's status in this browser because of a technical issue.": "No pudimos guardar su participación dentro de este navegador. No se preocupe, ha confirmado con éxito su participación, simplemente no pudimos guardar su estado en este navegador debido a un problema técnico.",
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):": "Mejoramos este software gracias a sus comentarios. Para informarnos sobre este problema, hay dos posibilidades (ambas lamentablemente requieren la creación de una cuenta de usuario):", "We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):": "Mejoramos este software gracias a sus comentarios. Para informarnos sobre este problema, hay dos posibilidades (ambas lamentablemente requieren la creación de una cuenta de usuario):",
"We just sent an email to {email}": "Acabamos de enviar un correo electrónico a {email}", "We just sent an email to {email}": "Acabamos de enviar un correo electrónico a {email}",
@ -1067,6 +1086,7 @@
"Welcome back!": "¡Bienvenido de nuevo!", "Welcome back!": "¡Bienvenido de nuevo!",
"Welcome to Mobilizon, {username}!": "¡Bienvenido a Mobilizon, {username}!", "Welcome to Mobilizon, {username}!": "¡Bienvenido a Mobilizon, {username}!",
"What can I do to help?": "¿Que puedo hacer para ayudar?", "What can I do to help?": "¿Que puedo hacer para ayudar?",
"What happened?": "¿Qué pasó?",
"Wheelchair accessibility": "Accesibilidad para sillas de ruedas", "Wheelchair accessibility": "Accesibilidad para sillas de ruedas",
"When a moderator from the group creates an event and attributes it to the group, it will show up here.": "Cuando un moderador del grupo crea un evento y lo atribuye al grupo, se mostrará aquí.", "When a moderator from the group creates an event and attributes it to the group, it will show up here.": "Cuando un moderador del grupo crea un evento y lo atribuye al grupo, se mostrará aquí.",
"When the event is private, you'll need to share the link around.": "Cuando el evento sea privado, deberá compartir el enlace.", "When the event is private, you'll need to share the link around.": "Cuando el evento sea privado, deberá compartir el enlace.",
@ -1125,7 +1145,9 @@
"You have one event tomorrow.": "No tienes eventos mañana|Tienes un evento mañana.|Tienes {count} eventos mañana", "You have one event tomorrow.": "No tienes eventos mañana|Tienes un evento mañana.|Tienes {count} eventos mañana",
"You haven't interacted with other instances yet.": "Aún no has interactuado con otras instancias.", "You haven't interacted with other instances yet.": "Aún no has interactuado con otras instancias.",
"You invited {member}.": "as invitado a {member}.", "You invited {member}.": "as invitado a {member}.",
"You may also:": "También puedes:",
"You may clear all participation information for this device with the buttons below.": "Puede borrar toda la información de participación de este dispositivo con los botones a continuación.", "You may clear all participation information for this device with the buttons below.": "Puede borrar toda la información de participación de este dispositivo con los botones a continuación.",
"You may now close this page or {return_to_the_homepage}.": "Ahora puede cerrar esta página o {return_to_the_homepage}.",
"You may now close this window, or {return_to_event}.": "Ahora puede cerrar esta ventana o {return_to_event}.", "You may now close this window, or {return_to_event}.": "Ahora puede cerrar esta ventana o {return_to_event}.",
"You may show some members as contacts.": "Puede mostrar algunos miembros como contactos.", "You may show some members as contacts.": "Puede mostrar algunos miembros como contactos.",
"You moved the folder {resource} into {new_path}.": "Moviste la carpeta {resource} a {new_path}.", "You moved the folder {resource} into {new_path}.": "Moviste la carpeta {resource} a {new_path}.",
@ -1177,6 +1199,7 @@
"Your email is being changed": "Su correo electrónico está siendo cambiado", "Your email is being changed": "Su correo electrónico está siendo cambiado",
"Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.": "Su correo electrónico solo se utilizará para confirmar que es una persona real y enviarle actualizaciones eventuales para este evento. NO se transmitirá a otras instancias ni al organizador del evento.", "Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.": "Su correo electrónico solo se utilizará para confirmar que es una persona real y enviarle actualizaciones eventuales para este evento. NO se transmitirá a otras instancias ni al organizador del evento.",
"Your federated identity": "Su identidad federada", "Your federated identity": "Su identidad federada",
"Your membership is pending approval": "Su membresía está pendiente de aprobación",
"Your membership was approved by {profile}.": "Su adesión fue aprobada por {profile}.", "Your membership was approved by {profile}.": "Su adesión fue aprobada por {profile}.",
"Your participation has been confirmed": "Su participación ha sido confirmada", "Your participation has been confirmed": "Su participación ha sido confirmada",
"Your participation has been rejected": "Su participación ha sido rechazada", "Your participation has been rejected": "Su participación ha sido rechazada",
@ -1227,6 +1250,7 @@
"profile@instance": "perfil@instancia", "profile@instance": "perfil@instancia",
"report #{report_number}": "informe #{report_number}", "report #{report_number}": "informe #{report_number}",
"return to the event's page": "volver a la página del evento", "return to the event's page": "volver a la página del evento",
"return to the homepage": "Vuelve a la página inicial",
"terms of service": "términos de servicio", "terms of service": "términos de servicio",
"with another identity…": "con otra identidad …", "with another identity…": "con otra identidad …",
"your notification settings": "tu configuración de notificaciones", "your notification settings": "tu configuración de notificaciones",

View File

@ -1308,5 +1308,18 @@
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap", "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
"Category": "Catégorie", "Category": "Catégorie",
"Select a category": "Choisissez une categorie", "Select a category": "Choisissez une categorie",
"Any category": "N'importe quelle catégorie" "Any category": "N'importe quelle catégorie",
"No instance found.": "Aucune instance trouvée.",
"This group is a remote group, it's possible the original instance has more informations.": "Ce groupe est un groupe distant, il est possible que l'instance d'origine ait plus d'informations.",
"View the group profile on the original instance": "Afficher le profil du groupe sur l'instance d'origine",
"View past events": "Voir les événements passés",
"Get informed of the upcoming public events": "Soyez informé⋅e des événements publics à venir",
"Join": "Rejoindre",
"Become part of the community and start organizing events": "Faites partie de la communauté et commencez à organiser des événements",
"Follow requests will be approved by a group moderator": "Les demandes de suivi seront approuvées par un⋅e modérateur⋅ice du groupe",
"Follow request pending approval": "Demande de suivi en attente d'approbation",
"Your membership is pending approval": "Votre adhésion est en attente d'approbation",
"Activate notifications": "Activer les notifications",
"Deactivate notifications": "Désactiver les notifications",
"Membership requests will be approved by a group moderator": "Les demandes d'adhésion seront approuvées par un⋅e modérateur⋅ice du groupe"
} }

View File

@ -55,6 +55,7 @@
"Account settings": "Roghainnean a chunntais", "Account settings": "Roghainnean a chunntais",
"Actions": "Gnìomhan", "Actions": "Gnìomhan",
"Activate browser push notifications": "Gnìomhaich brathan putaidh a bhrabhsair", "Activate browser push notifications": "Gnìomhaich brathan putaidh a bhrabhsair",
"Activate notifications": "Gnìomhaich na brathan",
"Activated": "An gnìomh", "Activated": "An gnìomh",
"Active": "Gnìomhach", "Active": "Gnìomhach",
"Activity": "Gnìomhachd", "Activity": "Gnìomhachd",
@ -103,6 +104,7 @@
"Anonymous participant": "Freastalaiche gun ainm", "Anonymous participant": "Freastalaiche gun ainm",
"Anonymous participants will be asked to confirm their participation through e-mail.": "Thèid iarraidh air freastalaichean gun ainm gun dearbh iad an com-pàirteachadh air a phost-d.", "Anonymous participants will be asked to confirm their participation through e-mail.": "Thèid iarraidh air freastalaichean gun ainm gun dearbh iad an com-pàirteachadh air a phost-d.",
"Anonymous participations": "Com-pàirteachaidhean gun ainm", "Anonymous participations": "Com-pàirteachaidhean gun ainm",
"Any category": "Roinn-seòrsa sam bith",
"Any day": "Latha sam bith", "Any day": "Latha sam bith",
"Any type": "Seòrsa sam bith", "Any type": "Seòrsa sam bith",
"Anyone can join freely": "Faodaidh neach sam bith a dhol an sàs ann gu saor", "Anyone can join freely": "Faodaidh neach sam bith a dhol an sàs ann gu saor",
@ -135,6 +137,7 @@
"Back to top": "Air ais gun bhàrr", "Back to top": "Air ais gun bhàrr",
"Back to user list": "Air ais gu liosta nan cleachdaichean", "Back to user list": "Air ais gu liosta nan cleachdaichean",
"Banner": "Bratach", "Banner": "Bratach",
"Become part of the community and start organizing events": "Faigh ballrachd sa choimhearsnachd agus tòisich air tachartasan a chur air dòigh",
"Before you can login, you need to click on the link inside it to validate your account.": "Mus urrainn dhut clàradh a-steach, feumaidh tu briogadh air a cheangal na broinn gus an cunntas agad a dhearbhadh.", "Before you can login, you need to click on the link inside it to validate your account.": "Mus urrainn dhut clàradh a-steach, feumaidh tu briogadh air a cheangal na broinn gus an cunntas agad a dhearbhadh.",
"Begins on": "Tòisichidh e aig", "Begins on": "Tòisichidh e aig",
"Big Blue Button": "Big Blue Button", "Big Blue Button": "Big Blue Button",
@ -158,6 +161,7 @@
"Cancel my participation…": "Sguir dhen chom-pàirteachadh agam…", "Cancel my participation…": "Sguir dhen chom-pàirteachadh agam…",
"Cancelled": "Chaidh a chur gu neoini", "Cancelled": "Chaidh a chur gu neoini",
"Cancelled: Won't happen": "Sguireadh dheth: Cha tachair seo", "Cancelled: Won't happen": "Sguireadh dheth: Cha tachair seo",
"Category": "Roinn-seòrsa",
"Change": "Atharraich", "Change": "Atharraich",
"Change email": "Atharraich am post-d", "Change email": "Atharraich am post-d",
"Change my email": "Atharraich am post-d agam", "Change my email": "Atharraich am post-d agam",
@ -192,7 +196,7 @@
"Confirm my particpation": "Dearbh an com-pàirteachadh agam", "Confirm my particpation": "Dearbh an com-pàirteachadh agam",
"Confirm participation": "Dearbh an com-pàirteachadh", "Confirm participation": "Dearbh an com-pàirteachadh",
"Confirm user": "Dearbh an cleachdaiche", "Confirm user": "Dearbh an cleachdaiche",
"Confirmed": "Air a dhearbhachadh", "Confirmed": "Air a dhearbhadh",
"Confirmed at": "Chaidh a dhearbhachadh", "Confirmed at": "Chaidh a dhearbhachadh",
"Confirmed: Will happen": "Air dearbhadh: Tachraidh seo", "Confirmed: Will happen": "Air dearbhadh: Tachraidh seo",
"Congratulations, your account is now created!": "Meal do naidheachd, chaidh an cunntas agad a chruthachadh!", "Congratulations, your account is now created!": "Meal do naidheachd, chaidh an cunntas agad a chruthachadh!",
@ -239,6 +243,7 @@
"Date and time": "Ceann-là s àm", "Date and time": "Ceann-là s àm",
"Date and time settings": "Roghainnean a chinn-là s an ama", "Date and time settings": "Roghainnean a chinn-là s an ama",
"Date parameters": "Paramadairean a chinn-là", "Date parameters": "Paramadairean a chinn-là",
"Deactivate notifications": "Cuir na brathan à gnìomh",
"Decline": "Diùlt", "Decline": "Diùlt",
"Decrease": "Lùghdaich", "Decrease": "Lùghdaich",
"Default": "Bun-roghainn", "Default": "Bun-roghainn",
@ -365,6 +370,8 @@
"Follow": "Lean air", "Follow": "Lean air",
"Follow a new instance": "Lean air ionstans ùr", "Follow a new instance": "Lean air ionstans ùr",
"Follow instance": "Lean air an ionstans", "Follow instance": "Lean air an ionstans",
"Follow request pending approval": "Iarrtas leantainn ri aontachadh",
"Follow requests will be approved by a group moderator": "Thèid aontachadh ri iarrtasan leantainn le maor a bhuidhinn",
"Follow status": "Staid na leantainn", "Follow status": "Staid na leantainn",
"Followed": "Ga leantainn leinne", "Followed": "Ga leantainn leinne",
"Followed, pending response": "Ga leantainn leinne, a feitheamh air freagairt", "Followed, pending response": "Ga leantainn leinne, a feitheamh air freagairt",
@ -392,6 +399,7 @@
"General information": "Fiosrachadh coitcheann", "General information": "Fiosrachadh coitcheann",
"General settings": "Roghainnean coitcheann", "General settings": "Roghainnean coitcheann",
"Geolocation was not determined in time.": "Cha deach leis a gheò-lorgadh ri àm.", "Geolocation was not determined in time.": "Cha deach leis a gheò-lorgadh ri àm.",
"Get informed of the upcoming public events": "Faigh naidheachdan mu thachartasan poblach ri thighinn",
"Getting location": "A faighinn an ionaid", "Getting location": "A faighinn an ionaid",
"Getting there": "Mar a gheibh thu ann", "Getting there": "Mar a gheibh thu ann",
"Glossary": "Briathrachan", "Glossary": "Briathrachan",
@ -436,6 +444,7 @@
"I want to approve every participation request": "Tha mi airson aontachadh ris a h-uile iarrtas air com-pàirteachadh", "I want to approve every participation request": "Tha mi airson aontachadh ris a h-uile iarrtas air com-pàirteachadh",
"I've been mentionned in a comment under an event": "Chaidh iomradh a thoirt orm ann am beachd fo thachartas", "I've been mentionned in a comment under an event": "Chaidh iomradh a thoirt orm ann am beachd fo thachartas",
"I've been mentionned in a group discussion": "Chaidh iomradh a thoirt orm ann an deasbad buidhinn", "I've been mentionned in a group discussion": "Chaidh iomradh a thoirt orm ann an deasbad buidhinn",
"I've clicked on X, then on Y": "Briog mi air X s an uairsin air Y",
"ICS feed for events": "Inbhir ICS dha na tachartasan", "ICS feed for events": "Inbhir ICS dha na tachartasan",
"ICS/WebCal Feed": "Inbhir ICS/WebCal", "ICS/WebCal Feed": "Inbhir ICS/WebCal",
"IP Address": "Seòladh IP", "IP Address": "Seòladh IP",
@ -484,6 +493,7 @@
"It is possible that the content is not accessible on this instance, because this instance has blocked the profiles or groups behind this content.": "Dhfhaoidte nach gabh an t-susbaint inntrigeadh air an ionstans seo on a bhac an t-ionstans seo na pròifilean no buidhnean a tha air cùlaibh na susbainte seo.", "It is possible that the content is not accessible on this instance, because this instance has blocked the profiles or groups behind this content.": "Dhfhaoidte nach gabh an t-susbaint inntrigeadh air an ionstans seo on a bhac an t-ionstans seo na pròifilean no buidhnean a tha air cùlaibh na susbainte seo.",
"Italic": "Eadailteach", "Italic": "Eadailteach",
"Jitsi Meet": "Jitsi Meet", "Jitsi Meet": "Jitsi Meet",
"Join": "Faigh ballrachd",
"Join <b>{instance}</b>, a Mobilizon instance": "Faigh ballrachd air <b>{instance}</b>, seo ionstans Mobilizon", "Join <b>{instance}</b>, a Mobilizon instance": "Faigh ballrachd air <b>{instance}</b>, seo ionstans Mobilizon",
"Join group": "Faigh ballrachd sa bhuidheann", "Join group": "Faigh ballrachd sa bhuidheann",
"Join group {group}": "Faigh ballrachd sa bhuidheann {group}", "Join group {group}": "Faigh ballrachd sa bhuidheann {group}",
@ -532,6 +542,7 @@
"Member": "Ball", "Member": "Ball",
"Members": "Buill", "Members": "Buill",
"Members-only post": "Post do bhuill a-mhàin", "Members-only post": "Post do bhuill a-mhàin",
"Membership requests will be approved by a group moderator": "Thèid aontachadh ri iarrtasan ballrachd le maor a bhuidhinn",
"Memberships": "Ballrachdan", "Memberships": "Ballrachdan",
"Mentions": "Iomraidhean", "Mentions": "Iomraidhean",
"Message": "Teachdaireachd", "Message": "Teachdaireachd",
@ -818,6 +829,7 @@
"Search": "Lorg", "Search": "Lorg",
"Search events, groups, etc.": "Lorg tachartasan, buidhnean is msaa.", "Search events, groups, etc.": "Lorg tachartasan, buidhnean is msaa.",
"Searching…": "Ga lorg…", "Searching…": "Ga lorg…",
"Select a category": "Tagh roinn-seòrsa",
"Select a language": "Tagh cànan", "Select a language": "Tagh cànan",
"Select a radius": "Tagh astar", "Select a radius": "Tagh astar",
"Select a timezone": "Tagh roinn-tìde", "Select a timezone": "Tagh roinn-tìde",
@ -825,6 +837,7 @@
"Select the activities for which you wish to receive an email or a push notification.": "Tagh na gnìomhachdan dhan fhaigh thu post-d no brath putaidh.", "Select the activities for which you wish to receive an email or a push notification.": "Tagh na gnìomhachdan dhan fhaigh thu post-d no brath putaidh.",
"Send": "Cuir", "Send": "Cuir",
"Send email": "Cuir post-d", "Send email": "Cuir post-d",
"Send feedback": "Cuir do bheachd thugainn",
"Send notification e-mails": "Cuir puist-d bhrathan", "Send notification e-mails": "Cuir puist-d bhrathan",
"Send password reset": "Cuir ath-shuidheachadh an fhacail-fhaire", "Send password reset": "Cuir ath-shuidheachadh an fhacail-fhaire",
"Send the confirmation email again": "Cuir am post-d dearbhaidh a-rithist", "Send the confirmation email again": "Cuir am post-d dearbhaidh a-rithist",
@ -851,6 +864,7 @@
"Skip to main content": "Thoir leum gun phrìomh shusbaint", "Skip to main content": "Thoir leum gun phrìomh shusbaint",
"Social": "Sòisealta", "Social": "Sòisealta",
"Some terms, technical or otherwise, used in the text below may cover concepts that are difficult to grasp. We have provided a glossary here to help you understand them better:": "Tha cuid dhe na faclan a tha gan cleachdadh san teacsa gu h-ìosal, co-dhiù an e faclan teicnigeach a th annta gus nach e, mu bheachdan a tha caran doirbh a thuigsinn ma dhfhaoidte. Rinn sinn briathrachan ach am bhiod e na b fhasa dhut an tuigsinn:", "Some terms, technical or otherwise, used in the text below may cover concepts that are difficult to grasp. We have provided a glossary here to help you understand them better:": "Tha cuid dhe na faclan a tha gan cleachdadh san teacsa gu h-ìosal, co-dhiù an e faclan teicnigeach a th annta gus nach e, mu bheachdan a tha caran doirbh a thuigsinn ma dhfhaoidte. Rinn sinn briathrachan ach am bhiod e na b fhasa dhut an tuigsinn:",
"Sorry, we wen't able to save your feedback. Don't worry, we'll try to fix this issue anyway.": "Tha sinn duilich ach cha b urrainn dhuinn do bheachd a shàbhaladh. Na gabh dragh, feuchaidh sinn gun càraich sinn an duilgheadas co-dhiù.",
"Starts on…": "Àm-tòiseachaidh…", "Starts on…": "Àm-tòiseachaidh…",
"Status": "Staid", "Status": "Staid",
"Stop following instance": "Na lean tuilleadh air an ionstans", "Stop following instance": "Na lean tuilleadh air an ionstans",
@ -870,6 +884,7 @@
"Terms": "Teirmichean", "Terms": "Teirmichean",
"Terms of service": "Teirmichean na seirbheise", "Terms of service": "Teirmichean na seirbheise",
"Text": "Teacsa", "Text": "Teacsa",
"Thanks a lot, your feedback was submitted!": "Mòran taing, chaidh do bheachdan a chur thugainn!",
"That you follow or of which you are a member": "A tha thu a leantainn orra no nad bhall ann", "That you follow or of which you are a member": "A tha thu a leantainn orra no nad bhall ann",
"The Big Blue Button video teleconference URL": "URL co-labhairt video Big Blue Button", "The Big Blue Button video teleconference URL": "URL co-labhairt video Big Blue Button",
"The Google Meet video teleconference URL": "URL co-labhairt video Google Meet", "The Google Meet video teleconference URL": "URL co-labhairt video Google Meet",
@ -943,6 +958,7 @@
"This event has been cancelled.": "Chaidh an tachartas seo a chur gu neoini.", "This event has been cancelled.": "Chaidh an tachartas seo a chur gu neoini.",
"This event is accessible only through it's link. Be careful where you post this link.": "Cha ghabh an tachartas seo inntrigeadh ach leis a cheangal aige. Thoir an aire mus postaich thu an ceangal seo am badeigin.", "This event is accessible only through it's link. Be careful where you post this link.": "Cha ghabh an tachartas seo inntrigeadh ach leis a cheangal aige. Thoir an aire mus postaich thu an ceangal seo am badeigin.",
"This group doesn't have a description yet.": "Chan eil tuairisgeul aig a bhuidheann seo fhathast.", "This group doesn't have a description yet.": "Chan eil tuairisgeul aig a bhuidheann seo fhathast.",
"This group is a remote group, it's possible the original instance has more informations.": "S e buidheann cèin a tha seo agus dhfhaoidte gu bheil barrachd fiosrachaidh aig an ionstans tùsail.",
"This group is accessible only through it's link. Be careful where you post this link.": "Cha ghabh an tachartas seo inntrigeadh ach leis a cheangal aige. Thoir an aire mus postaich thu an ceangal seo am badeigin.", "This group is accessible only through it's link. Be careful where you post this link.": "Cha ghabh an tachartas seo inntrigeadh ach leis a cheangal aige. Thoir an aire mus postaich thu an ceangal seo am badeigin.",
"This group is invite-only": "Feumaidh tu cuireadh airson ballrachd fhaighinn sa bhuidheann seo", "This group is invite-only": "Feumaidh tu cuireadh airson ballrachd fhaighinn sa bhuidheann seo",
"This group was not found": "Cha deach am buidheann seo a lorg", "This group was not found": "Cha deach am buidheann seo a lorg",
@ -1044,6 +1060,8 @@
"View less": "Seall nas lugha", "View less": "Seall nas lugha",
"View more": "Seall barrachd", "View more": "Seall barrachd",
"View page on {hostname} (in a new window)": "Seall an duilleag air {hostname} (ann an uinneag ùr)", "View page on {hostname} (in a new window)": "Seall an duilleag air {hostname} (ann an uinneag ùr)",
"View past events": "Seall na tachartasan san àm a dhfhalbh",
"View the group profile on the original instance": "Faic pròifil a buidhinn air an ionstans tùsail",
"Visibility was set to an unknown value.": "Chaidh luach nach aithne dhuinn a shuidheachadh air an t-so-fhaicsinneachd.", "Visibility was set to an unknown value.": "Chaidh luach nach aithne dhuinn a shuidheachadh air an t-so-fhaicsinneachd.",
"Visibility was set to private.": "Chaidh so-fhaicsinneachd phrìobhaideach a shuidheachadh air.", "Visibility was set to private.": "Chaidh so-fhaicsinneachd phrìobhaideach a shuidheachadh air.",
"Visibility was set to public.": "Chaidh so-fhaicsinneachd phoblach a shuidheachadh air.", "Visibility was set to public.": "Chaidh so-fhaicsinneachd phoblach a shuidheachadh air.",
@ -1051,6 +1069,7 @@
"Visible everywhere on the web (public)": "Chithear air feadh an lìn e (poblach)", "Visible everywhere on the web (public)": "Chithear air feadh an lìn e (poblach)",
"Waiting for organization team approval.": "A feitheamh air aontachadh leis an sgioba eagrachaidh.", "Waiting for organization team approval.": "A feitheamh air aontachadh leis an sgioba eagrachaidh.",
"Warning": "Rabhadh", "Warning": "Rabhadh",
"We collect your feedback and the error information in order to improve this service.": "Cruinnichidh sinn do bheachdan agus am fiosrachadh mun mhearachd ach an doir sinn piseach air an t-seirbheis seo.",
"We couldn't save your participation inside this browser. Not to worry, you have successfully confirmed your participation, we just couldn't save it's status in this browser because of a technical issue.": "Cha b urrainn dhuinn an com-pàirteachadh agad a shàbhaladh sa bhrabhsair seo. Na gabh dragh, dhearbh thu gun gabh thu pàirt ann ach cha b urrainn dhuinn sin a shàbhaladh sa bhrabhsair seo ri linn duilgheadas teicnigeach.", "We couldn't save your participation inside this browser. Not to worry, you have successfully confirmed your participation, we just couldn't save it's status in this browser because of a technical issue.": "Cha b urrainn dhuinn an com-pàirteachadh agad a shàbhaladh sa bhrabhsair seo. Na gabh dragh, dhearbh thu gun gabh thu pàirt ann ach cha b urrainn dhuinn sin a shàbhaladh sa bhrabhsair seo ri linn duilgheadas teicnigeach.",
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):": "Bheir sinn piseach air a bhathar-bhog le taic do bheachdan. Tha dà dhòigh ann airson innse dhuinn mu dhèidhinn na trioblaide seo (gu mì-fhortanach, feumaidh tu cunntas a chruthachadh dhaibh):", "We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):": "Bheir sinn piseach air a bhathar-bhog le taic do bheachdan. Tha dà dhòigh ann airson innse dhuinn mu dhèidhinn na trioblaide seo (gu mì-fhortanach, feumaidh tu cunntas a chruthachadh dhaibh):",
"We just sent an email to {email}": "Tha sinn air post-d a chur gu {email}", "We just sent an email to {email}": "Tha sinn air post-d a chur gu {email}",
@ -1066,6 +1085,7 @@
"Welcome back!": "Fàilte air ais!", "Welcome back!": "Fàilte air ais!",
"Welcome to Mobilizon, {username}!": "Fàilte gu Mobilizon, {username}!", "Welcome to Mobilizon, {username}!": "Fàilte gu Mobilizon, {username}!",
"What can I do to help?": "Dè nì mi airson cuideachadh?", "What can I do to help?": "Dè nì mi airson cuideachadh?",
"What happened?": "Dè thachair?",
"Wheelchair accessibility": "Inntrigeadh cathrach-cuibhle", "Wheelchair accessibility": "Inntrigeadh cathrach-cuibhle",
"When a moderator from the group creates an event and attributes it to the group, it will show up here.": "Nuair a chruthaicheas maor a bhuidhinn tachartas le iomruineadh dhan bhuidheann, nochdaidh e an-seo.", "When a moderator from the group creates an event and attributes it to the group, it will show up here.": "Nuair a chruthaicheas maor a bhuidhinn tachartas le iomruineadh dhan bhuidheann, nochdaidh e an-seo.",
"When the event is private, you'll need to share the link around.": "Nuair a bhios an tachartas prìobhaideach, feumaidh tu fhèin an ceangal a cho-roinneadh.", "When the event is private, you'll need to share the link around.": "Nuair a bhios an tachartas prìobhaideach, feumaidh tu fhèin an ceangal a cho-roinneadh.",
@ -1124,7 +1144,9 @@
"You have one event tomorrow.": "Bidh {count} tachartas agad a-màireach| Bidh {count} thachartas agad a-màireach| Bidh {count} tachartasan agad a-màireach| Bidh {count} tachartas agad a-màireach", "You have one event tomorrow.": "Bidh {count} tachartas agad a-màireach| Bidh {count} thachartas agad a-màireach| Bidh {count} tachartasan agad a-màireach| Bidh {count} tachartas agad a-màireach",
"You haven't interacted with other instances yet.": "Cha do rinn thu eadar-ghnìomh le ionstans sam bith eile fhathast.", "You haven't interacted with other instances yet.": "Cha do rinn thu eadar-ghnìomh le ionstans sam bith eile fhathast.",
"You invited {member}.": "Thug thu cuireadh dha {member}.", "You invited {member}.": "Thug thu cuireadh dha {member}.",
"You may also:": "S urrainn dhut cuideachd:",
"You may clear all participation information for this device with the buttons below.": "S urrainn dhut gach fiosrachadh mun chom-pàirteachadh a shuathadh bàn on uidheam seo leis na putanan gu h-ìosal.", "You may clear all participation information for this device with the buttons below.": "S urrainn dhut gach fiosrachadh mun chom-pàirteachadh a shuathadh bàn on uidheam seo leis na putanan gu h-ìosal.",
"You may now close this page or {return_to_the_homepage}.": "S urrainn dhut an duilleag seo a dhùnadh a-nis no {return_to_the_homepage}.",
"You may now close this window, or {return_to_event}.": "S urrainn dhut an uinneag seo a dhùnadh a-nis no {return_to_event}.", "You may now close this window, or {return_to_event}.": "S urrainn dhut an uinneag seo a dhùnadh a-nis no {return_to_event}.",
"You may show some members as contacts.": "Faodaidh tu cuid a bhuill a shealltainn nan luchd-aithne.", "You may show some members as contacts.": "Faodaidh tu cuid a bhuill a shealltainn nan luchd-aithne.",
"You moved the folder {resource} into {new_path}.": "Ghluais thu am pasgan {resource} gu {new_path}.", "You moved the folder {resource} into {new_path}.": "Ghluais thu am pasgan {resource} gu {new_path}.",
@ -1176,6 +1198,7 @@
"Your email is being changed": "Tha am post-d agad ga atharrachadh", "Your email is being changed": "Tha am post-d agad ga atharrachadh",
"Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.": "Cha dèid am post-d agad a chleachdadh ach airson dearbhadh gur e neach a th annad agus airson naidheachdan a chur thugad mun tachartas seo. S ann NACH DÈID a thar-chur gu ionstansan eile no gu eagraiche an tachartais.", "Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.": "Cha dèid am post-d agad a chleachdadh ach airson dearbhadh gur e neach a th annad agus airson naidheachdan a chur thugad mun tachartas seo. S ann NACH DÈID a thar-chur gu ionstansan eile no gu eagraiche an tachartais.",
"Your federated identity": "An dearbh-aithne co-naisgte agad", "Your federated identity": "An dearbh-aithne co-naisgte agad",
"Your membership is pending approval": "Tha do bhallrachd a feitheamh ri aontachadh",
"Your membership was approved by {profile}.": "Dhaontaich {profile} gum faigh thu ballrachd.", "Your membership was approved by {profile}.": "Dhaontaich {profile} gum faigh thu ballrachd.",
"Your participation has been confirmed": "Chaidh an com-pàirteachadh agad a dhearbhadh", "Your participation has been confirmed": "Chaidh an com-pàirteachadh agad a dhearbhadh",
"Your participation has been rejected": "Chaidh an com-pàirteachadh agad a dhiùltadh", "Your participation has been rejected": "Chaidh an com-pàirteachadh agad a dhiùltadh",
@ -1226,6 +1249,7 @@
"profile@instance": "ainm@ionstans", "profile@instance": "ainm@ionstans",
"report #{report_number}": "gearan #{report_number}", "report #{report_number}": "gearan #{report_number}",
"return to the event's page": "till gu duilleag an tachartais", "return to the event's page": "till gu duilleag an tachartais",
"return to the homepage": "tilleadh dhan duilleag-dhachaigh",
"terms of service": "teirmichean na seirbheise", "terms of service": "teirmichean na seirbheise",
"with another identity…": "le dearbh-aithne eile…", "with another identity…": "le dearbh-aithne eile…",
"your notification settings": "roghainnean nam brathan", "your notification settings": "roghainnean nam brathan",

View File

@ -258,10 +258,16 @@
"Previous page": "הדף הקודם", "Previous page": "הדף הקודם",
"Privacy Policy": "מדיניות פרטיות", "Privacy Policy": "מדיניות פרטיות",
"Private event": "אירוע פרטי", "Private event": "אירוע פרטי",
"Private feeds": "היזנים פרטיים",
"Profiles": "פרופילים",
"Public RSS/Atom Feed": "ערוץ RSS/Atom פומבי", "Public RSS/Atom Feed": "ערוץ RSS/Atom פומבי",
"Public comment moderation": "בקרת תגובות פומביות",
"Public event": "אירוע פומבי", "Public event": "אירוע פומבי",
"Public feeds": "היזנים פומביים",
"Public iCal Feed": "ערוץ iCal פומבי", "Public iCal Feed": "ערוץ iCal פומבי",
"Publish": "פרסום", "Publish": "פרסום",
"Published events with <b>{comments}</b> comments and <b>{participations}</b> confirmed participations": "אירועים שפורסמו שיש להם <b>{comments}</b> תגובות ו־<b>{participations}</b> אישורי השתתפות",
"RSS/Atom Feed": "היזן רסס/אטום",
"Region": "אזור", "Region": "אזור",
"Registration is allowed, anyone can register.": "ההרשמה מאופשרת, כל אחד.ת יכול.ה להירשם.", "Registration is allowed, anyone can register.": "ההרשמה מאופשרת, כל אחד.ת יכול.ה להירשם.",
"Registration is closed.": "ההרשמה סגורה.", "Registration is closed.": "ההרשמה סגורה.",
@ -272,9 +278,11 @@
"Report": "דיווח", "Report": "דיווח",
"Report this comment": "דיווח על תגובה זו", "Report this comment": "דיווח על תגובה זו",
"Report this event": "דיווח על אירוע זה", "Report this event": "דיווח על אירוע זה",
"Reported": "מדווח",
"Reported by": "דווח על־ידי", "Reported by": "דווח על־ידי",
"Reported by someone on {domain}": "דווח על־ידי מישהו.י מהאתר {domain}", "Reported by someone on {domain}": "דווח על־ידי מישהו.י מהאתר {domain}",
"Reported by {reporter}": "דווח על־ידי {reporter}", "Reported by {reporter}": "דווח על־ידי {reporter}",
"Reported identity": "זהות מדווחת",
"Reports": "דיווחים", "Reports": "דיווחים",
"Reset my password": "איפוס הססמה שלי", "Reset my password": "איפוס הססמה שלי",
"Resolved": "נפתר", "Resolved": "נפתר",
@ -287,6 +295,7 @@
"Searching…": "מחפש…", "Searching…": "מחפש…",
"Send email": "שליחת דוא\"ל", "Send email": "שליחת דוא\"ל",
"Send the report": "שליחת הדיווח", "Send the report": "שליחת הדיווח",
"Set an URL to a page with your own terms.": "הגדרת קישור לעמוד עם תנאים משלך.",
"Settings": "הגדרות", "Settings": "הגדרות",
"Share this event": "שיתוף אירוע זה", "Share this event": "שיתוף אירוע זה",
"Show map": "הצגת מפה", "Show map": "הצגת מפה",
@ -294,6 +303,8 @@
"Show the time when the event begins": "הצגת זמן תחילת האירוע", "Show the time when the event begins": "הצגת זמן תחילת האירוע",
"Show the time when the event ends": "הצגת זמן סיום האירוע", "Show the time when the event ends": "הצגת זמן סיום האירוע",
"Sign up": "הרשמה", "Sign up": "הרשמה",
"Starts on…": "מתחיל ב…",
"Status": "מצב",
"Street": "רחוב", "Street": "רחוב",
"Tentative: Will be confirmed later": "לא סופי: יאושר בהמשך", "Tentative: Will be confirmed later": "לא סופי: יאושר בהמשך",
"Terms": "תנאים", "Terms": "תנאים",
@ -304,5 +315,27 @@
"The event has been created as a draft": "האירוע נוצר כטיוטה", "The event has been created as a draft": "האירוע נוצר כטיוטה",
"The event has been published": "האירוע פורסם", "The event has been published": "האירוע פורסם",
"The event has been updated": "האירוע עודכן", "The event has been updated": "האירוע עודכן",
"The event has been updated and published": "האירוע עודכן ופורסם" "The event has been updated and published": "האירוע עודכן ופורסם",
"The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?": "בקשות השתתפות מאושרות ידנית על־ידי מארגנ.ת האירוע. האם ברצונך להוסיף הערה עם הסבר מדוע את.ה רוצה להשתתף באירוע זה?",
"The event organizer didn't add any description.": "מארגנ.ת האירוע לא הוסיפ.ה תיאור.",
"The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event.": "בקשות השתתפות מאושרות ידנית על־ידי מארגנ.ת האירוע. היות ובחרת להשתתף ללא חשבון, אנא צרפ.י הסבר מדוע את.ה רוצה להשתתף באירוע זה.",
"The event title will be ellipsed.": "כותרת האירוע תוצג באופן מקוצר עם שלוש נקודות בסוף.",
"The page you're looking for doesn't exist.": "העמוד שחיפשת לא קיים.",
"The password was successfully changed": "הססמה שונתה בהצלחה",
"The report will be sent to the moderators of your instance. You can explain why you report this content below.": "הדיווח יישלח למנהלים.ות של השרת שלך. ניתן להסביר למטה את סיבת הדיווח של תוכן זה.",
"The {default_terms} will be used. They will be translated in the user's language.": "ייעשה שימוש ב{default_terms}. הם יתורגמו לשפה של המשתמש.ת.",
"There are {participants} participants.": "יש {participants} משתתפים.ות.",
"There will be no way to recover your data.": "לא תהיה אפשרות לשחזר את המידע שלך.",
"These events may interest you": "האירועים האלה עשויים לעניין אותך",
"This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.": "שרת מוביליזון זה ואירוע זה מאפשרים בקשת השתתפות ללא חשבון, אך נדרש אימות באמצעות דואר אלקטרוני.",
"This information is saved only on your computer. Click for details": "מידע זה נשמר רק על המחשב שלך. לפרטים לחצ.י",
"This instance isn't opened to registrations, but you can register on other instances.": "שרת זה אינו פתוח להרשמה, אך ניתן להירשם בשרתים אחרים.",
"This is a demonstration site to test Mobilizon.": "זהו אתר הדגמה לצורך בדיקה של מוביליזון.",
"This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.": "הדבר ימחק / יהפוך לאנונימי את כל התוכן (אירועים, תגובות, הודעות, אישורי השתתפות…) שנוצר מתוך זהות זו.",
"Title": "כותרת",
"To confirm, type your event title \"{eventTitle}\"": "לאישור, נא להזין את כותרת האירוע \"{eventTitle}\"",
"To confirm, type your identity username \"{preferredUsername}\"": "לאישור, נא להזין את שם המשתמש.ת שלך \"{preferredUsername}\"",
"Transfer to {outsideDomain}": "העברה לשרת {outsideDomain}",
"Type": "סוג",
"URL": "קישור"
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,50 @@
import {
IAnalyticsConfig,
IConfig,
IKeyValueConfig,
} from "@/types/config.model";
export const statistics = async (config: IConfig, environement: any) => {
console.debug("Loading statistics", config.analytics);
const matomoConfig = checkProviderConfig(config, "matomo");
if (matomoConfig?.enabled === true) {
const { matomo } = (await import("./matomo")) as any;
matomo(environement, convertConfig(matomoConfig.configuration));
}
const sentryConfig = checkProviderConfig(config, "sentry");
if (sentryConfig?.enabled === true) {
const { sentry } = (await import("./sentry")) as any;
sentry(environement, convertConfig(sentryConfig.configuration));
}
};
export const checkProviderConfig = (
config: IConfig,
providerName: string
): IAnalyticsConfig | undefined => {
return config?.analytics?.find((provider) => provider.id === providerName);
};
export const convertConfig = (
configs: IKeyValueConfig[]
): Record<string, any> => {
return configs.reduce((acc, config) => {
acc[config.key] = toType(config.value, config.type);
return acc;
}, {} as Record<string, any>);
};
const toType = (value: string, type: string): string | number | boolean => {
switch (type) {
case "boolean":
return value === "true";
case "integer":
return parseInt(value, 10);
case "float":
return parseFloat(value);
case "string":
default:
return value;
}
};

View File

@ -0,0 +1,14 @@
import Vue from "vue";
import VueMatomo from "vue-matomo";
export const matomo = (environment: any, matomoConfiguration: any) => {
console.debug("Loading Matomo statistics");
console.debug(
"Calling VueMatomo with the following configuration",
matomoConfiguration
);
Vue.use(VueMatomo, {
...matomoConfiguration,
router: environment.router,
});
};

View File

@ -0,0 +1,11 @@
import VueRouter from "vue-router";
import Vue from "vue";
import { VuePlausible } from "vue-plausible";
export default (router: VueRouter, plausibleConfiguration: any) => {
console.debug("Loading Plausible statistics");
Vue.use(VuePlausible, {
// see configuration section
...plausibleConfiguration,
});
};

View File

@ -0,0 +1,54 @@
import Vue from "vue";
import * as Sentry from "@sentry/vue";
import { Integrations } from "@sentry/tracing";
export const sentry = (environment: any, sentryConfiguration: any) => {
console.debug("Loading Sentry statistics");
console.debug(
"Calling Sentry with the following configuration",
sentryConfiguration
);
// Don't attach errors to previous events
window.sessionStorage.removeItem("lastEventId");
Sentry.init({
Vue,
dsn: sentryConfiguration.dsn,
integrations: [
new Integrations.BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(
environment.router
),
tracingOrigins: ["localhost", "mobilizon1.com", /^\//],
}),
],
beforeSend(event) {
// Check if it is an exception, and if so, save it in session storage
// so that it can be retreived from the error component
if (event.exception && event.event_id) {
window.sessionStorage.setItem("lastEventId", event.event_id);
}
return event;
},
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: sentryConfiguration.tracesSampleRate,
release: environment.version,
});
};
export const submitFeedback = async (
endpoint: string,
dsn: string,
params: Record<string, string>
): Promise<void> => {
await fetch(endpoint, {
method: "POST",
headers: {
"Content-type": "application/json",
Authorization: `DSN ${dsn}`,
},
body: JSON.stringify(params),
});
};

View File

@ -68,7 +68,7 @@ export function usernameWithDomain(actor: IActor, force = false): string {
} }
export function displayName(actor: IActor): string { export function displayName(actor: IActor): string {
return actor.name != null && actor.name !== "" return actor && actor.name != null && actor.name !== ""
? actor.name ? actor.name
: usernameWithDomain(actor); : usernameWithDomain(actor);
} }

View File

@ -65,26 +65,40 @@ export class Address implements IAddress {
} }
get poiInfos(): IPoiInfo { get poiInfos(): IPoiInfo {
return addressToPoiInfos(this);
}
get fullName(): string {
return addressFullName(this);
}
get iconForPOI(): IPOIIcon {
return iconForAddress(this);
}
}
export function addressToPoiInfos(address: IAddress): IPoiInfo {
/* generate name corresponding to poi type */ /* generate name corresponding to poi type */
let name = ""; let name = "";
let alternativeName = ""; let alternativeName = "";
let poiIcon: IPOIIcon = poiIcons.default; let poiIcon: IPOIIcon = poiIcons.default;
let addressType = address.type;
// Google Maps doesn't have a type // Google Maps doesn't have a type
if (this.type == null && this.description === this.street) { if (address.type == null && address.description === address.street) {
this.type = "house"; addressType = "house";
} }
switch (this.type) { switch (addressType) {
case "house": case "house":
name = this.description; name = address.description;
alternativeName = [this.postalCode, this.locality, this.country] alternativeName = [address.postalCode, address.locality, address.country]
.filter((zone) => zone) .filter((zone) => zone)
.join(", "); .join(", ");
poiIcon = poiIcons.defaultAddress; poiIcon = poiIcons.defaultAddress;
break; break;
case "street": case "street":
case "secondary": case "secondary":
name = this.description; name = address.description;
alternativeName = [this.postalCode, this.locality, this.country] alternativeName = [address.postalCode, address.locality, address.country]
.filter((zone) => zone) .filter((zone) => zone)
.join(", "); .join(", ");
poiIcon = poiIcons.defaultStreet; poiIcon = poiIcons.defaultStreet;
@ -92,38 +106,47 @@ export class Address implements IAddress {
case "zone": case "zone":
case "city": case "city":
case "administrative": case "administrative":
name = this.postalCode name = address.postalCode
? `${this.description} (${this.postalCode})` ? `${address.description} (${address.postalCode})`
: this.description; : address.description;
alternativeName = [this.region, this.country] alternativeName = [address.region, address.country]
.filter((zone) => zone) .filter((zone) => zone)
.join(", "); .join(", ");
poiIcon = poiIcons.defaultAdministrative; poiIcon = poiIcons.defaultAdministrative;
break; break;
default: default:
// POI // POI
name = this.description; name = address.description;
alternativeName = ""; alternativeName = "";
if (this.street && this.street.trim()) { if (address.street && address.street.trim()) {
alternativeName = `${this.street}`; alternativeName = `${address.street}`;
if (this.locality) { if (address.locality) {
alternativeName += ` (${this.locality})`; alternativeName += ` (${address.locality})`;
} }
} else if (this.locality && this.locality.trim()) { } else if (address.locality && address.locality.trim()) {
alternativeName = `${this.locality}, ${this.region}, ${this.country}`; alternativeName = `${address.locality}, ${address.region}, ${address.country}`;
} else if (this.region && this.region.trim()) { } else if (address.region && address.region.trim()) {
alternativeName = `${this.region}, ${this.country}`; alternativeName = `${address.region}, ${address.country}`;
} else if (this.country && this.country.trim()) { } else if (address.country && address.country.trim()) {
alternativeName = this.country; alternativeName = address.country;
} }
poiIcon = this.iconForPOI; poiIcon = iconForAddress(address);
break; break;
} }
return { name, alternativeName, poiIcon }; return { name, alternativeName, poiIcon };
} }
get fullName(): string { export function iconForAddress(address: IAddress): IPOIIcon {
const { name, alternativeName } = this.poiInfos; if (address.type == null) {
return poiIcons.default;
}
const type = address.type.split(":").pop() || "";
if (poiIcons[type]) return poiIcons[type];
return poiIcons.default;
}
export function addressFullName(address: IAddress): string {
const { name, alternativeName } = addressToPoiInfos(address);
if (name && alternativeName) { if (name && alternativeName) {
return `${name}, ${alternativeName}`; return `${name}, ${alternativeName}`;
} }
@ -132,13 +155,3 @@ export class Address implements IAddress {
} }
return ""; return "";
} }
get iconForPOI(): IPOIIcon {
if (this.type == null) {
return poiIcons.default;
}
const type = this.type.split(":").pop() || "";
if (poiIcons[type]) return poiIcons[type];
return poiIcons.default;
}
}

View File

@ -0,0 +1,7 @@
export interface ISentryConfiguration {
dsn: string;
organization?: string;
project?: string;
host?: string;
tracesSampleRate: number;
}

View File

@ -6,6 +6,18 @@ export interface IOAuthProvider {
label: string; label: string;
} }
export interface IKeyValueConfig {
key: string;
value: string;
type: "boolean" | "integer" | "string";
}
export interface IAnalyticsConfig {
id: string;
enabled: boolean;
configuration: IKeyValueConfig[];
}
export interface IConfig { export interface IConfig {
name: string; name: string;
description: string; description: string;
@ -83,7 +95,6 @@ export interface IConfig {
features: { features: {
eventCreation: boolean; eventCreation: boolean;
groups: boolean; groups: boolean;
koenaConnect: boolean;
}; };
restrictions: { restrictions: {
onlyAdminCanCreateGroups: boolean; onlyAdminCanCreateGroups: boolean;
@ -110,4 +121,5 @@ export interface IConfig {
exportFormats: { exportFormats: {
eventParticipants: string[]; eventParticipants: string[];
}; };
analytics: IAnalyticsConfig[];
} }

View File

@ -3,6 +3,7 @@ import { InstanceFollowStatus } from "./enums";
export interface IInstance { export interface IInstance {
domain: string; domain: string;
hasRelay: boolean; hasRelay: boolean;
relayAddress: string | null;
followerStatus: InstanceFollowStatus; followerStatus: InstanceFollowStatus;
followedStatus: InstanceFollowStatus; followedStatus: InstanceFollowStatus;
personCount: number; personCount: number;

1
js/src/typings/matomo.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module "vue-matomo";

View File

@ -140,4 +140,5 @@ export const SELECTED_PROVIDERS: { [key: string]: string } = {
google: "Google", google: "Google",
keycloak: "Keycloak", keycloak: "Keycloak",
ldap: "LDAP", ldap: "LDAP",
cas: "CAS",
}; };

View File

@ -1,14 +1,7 @@
@import "~bulma/sass/utilities/functions.sass"; @import "~bulma/sass/utilities/functions.sass";
@import "~bulma/sass/utilities/initial-variables.sass"; @import "~bulma/sass/utilities/initial-variables.sass";
@import "~bulma/sass/utilities/derived-variables.sass"; @import "~bulma/sass/utilities/derived-variables.sass";
// chapril colors
$chapril_blue: #2e5281;
$chapril_blue_light: #bcd0e5;
$white: mix(#fff, #bcd0e5);
$whitest: #fff;
$chapril_orange: #ff5e00;
$chapril_grey: #5f5f5f;
// other
$bleuvert: #1e7d97; $bleuvert: #1e7d97;
$jaune: #ffd599; $jaune: #ffd599;
$violet: #424056; $violet: #424056;
@ -27,45 +20,44 @@ $violet-3: #3c376e;
/** /**
* Borders * Borders
*/ */
$borders: mix($chapril_blue, #d7d6de); $borders: #d7d6de;
$backgrounds: mix($chapril_blue, #ecebf2); $backgrounds: #ecebf2;
/** /**
* Text * Text
*/ */
$purple-1: mix($chapril_blue, #757199); $purple-1: #757199;
/** /**
* Background * Background
*/ */
$purple-2: mix($chapril_blue, #cdcaea); $purple-2: #cdcaea;
$purple-3: mix($chapril_blue, #e6e4f4); $purple-3: #e6e4f4;
$orange-2: mix($chapril_blue, #ed8d07); $orange-2: #ed8d07;
$orange-3: mix($chapril_blue, #d35204); $orange-3: #d35204;
$yellow-1: mix($chapril_blue, #fff1e8); $yellow-1: #ffd599;
$yellow-2: mix($chapril_blue, #fff1de); $yellow-2: #fff1de;
$yellow-3: mix($chapril_blue, #fff8f6); $yellow-3: #fbd5cb;
$yellow-4: mix($chapril_blue, #b4f0ff); $yellow-4: #f7ba30;
$primary: $chapril_blue; $primary: $bleuvert;
$primary-invert: findColorInvert($primary); $primary-invert: findColorInvert($primary);
$secondary: lighten($chapril_blue, 20%); $secondary: $jaune;
$secondary-invert: findColorInvert($secondary); $secondary-invert: findColorInvert($secondary);
$background-color: mix($chapril_blue, $violet-2); $background-color: $violet-2;
$background-color-darker: darken($background-color, 10%);
$success: mix($chapril_blue, #0d8758); $success: #0d8758;
$success-invert: findColorInvert($success); $success-invert: findColorInvert($success);
$info: mix($chapril_blue, #36bcd4); $info: #36bcd4;
$info-invert: findColorInvert($info); $info-invert: findColorInvert($info);
$danger: mix($chapril_blue, #cd2026); $danger: #cd2026;
$danger-invert: findColorInvert($danger); $danger-invert: findColorInvert($danger);
$link: $primary; $link: $primary;
$link-invert: $primary-invert; $link-invert: $primary-invert;
$text: mix($chapril_blue, $violet-1); $text: $violet-1;
$grey: #757575; $grey: #757575;
$colors: map-merge( $colors: map-merge(
@ -109,14 +101,18 @@ $navbar-height: 4rem;
// Footer // Footer
$footer-padding: 3rem 1.5rem 1rem; $footer-padding: 3rem 1.5rem 1rem;
$footer-background-color: $violet-2; $footer-background-color: $background-color;
$footer-text-color: mix(#000, $violet-2);
$body-background-color: mix($chapril_blue, #efeef4); $body-background-color: #efeef4;
$fullhd-enabled: false; $fullhd-enabled: false;
$hero-body-padding-medium: 6rem 1.5rem; $hero-body-padding-medium: 6rem 1.5rem;
$title-color: $chapril_blue; main > .container {
background: $body-background-color;
min-height: 70vh;
}
$title-color: #3c376e;
$title-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, $title-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif; serif;
$title-weight: 700; $title-weight: 700;
@ -124,7 +120,7 @@ $title-size: 40px;
$title-sub-size: 45px; $title-sub-size: 45px;
$title-sup-size: 30px; $title-sup-size: 30px;
$subtitle-color: $chapril_grey; $subtitle-color: #3a384c;
$subtitle-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, $subtitle-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif; serif;
$subtitle-weight: 400; $subtitle-weight: 400;
@ -142,7 +138,7 @@ $subtitle-sup-size: 15px;
//$input-border-color: #dbdbdb; //$input-border-color: #dbdbdb;
$breadcrumb-item-color: $primary; $breadcrumb-item-color: $primary;
$checkbox-background-color: #fff; $checkbox-background-color: #fff;
$title-color: $chapril_blue; $title-color: $violet-3;
:root { :root {
--color-primary: 30 125 151; --color-primary: 30 125 151;

View File

@ -4,7 +4,7 @@
<p class="modal-card-title">{{ $t("Pick an identity") }}</p> <p class="modal-card-title">{{ $t("Pick an identity") }}</p>
</header> </header>
<section class="modal-card-body"> <section class="modal-card-body">
<div class="list is-hoverable"> <div class="list is-hoverable list-none">
<a <a
class="list-item" class="list-item"
v-for="identity in identities" v-for="identity in identities"
@ -12,7 +12,7 @@
:class="{ :class="{
'is-active': currentIdentity && identity.id === currentIdentity.id, 'is-active': currentIdentity && identity.id === currentIdentity.id,
}" }"
@click="changeCurrentIdentity(identity)" @click="currentIdentity = identity"
> >
<div class="media"> <div class="media">
<img <img
@ -60,10 +60,11 @@ export default class IdentityPicker extends Vue {
identities: IActor[] = []; identities: IActor[] = [];
currentIdentity: IActor = this.value; get currentIdentity(): IActor {
return this.value;
}
changeCurrentIdentity(identity: IActor): void { set currentIdentity(identity: IActor) {
this.currentIdentity = identity;
this.$emit("input", identity); this.$emit("input", identity);
} }
} }

View File

@ -1,160 +0,0 @@
<template>
<section class="container">
<div v-if="person">
<div class="card-image" v-if="person.banner">
<figure class="image">
<img :src="person.banner.url" />
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48" v-if="person.avatar">
<img :src="person.avatar.url" />
</figure>
</div>
<div class="media-content">
<p class="title">{{ person.name }}</p>
<p class="subtitle">@{{ person.preferredUsername }}</p>
</div>
</div>
<div class="content">
<vue-simple-markdown :source="person.summary"></vue-simple-markdown>
</div>
<b-dropdown hoverable has-link aria-role="list">
<button class="button is-primary" slot="trigger">
{{ $t("Public feeds") }}
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('atom', true)">
{{ $t("Public RSS/Atom Feed") }}
</a>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('ics', true)">
{{ $t("Public iCal Feed") }}
</a>
</b-dropdown-item>
</b-dropdown>
<b-dropdown
hoverable
has-link
aria-role="list"
v-if="person.feedTokens.length > 0"
>
<button class="button is-info" slot="trigger">
{{ $t("Private feeds") }}
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('atom', false)">
{{ $t("RSS/Atom Feed") }}
</a>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('ics', false)">
{{ $t("iCal Feed") }}
</a>
</b-dropdown-item>
</b-dropdown>
<a
class="button"
v-if="currentActor.id === person.id"
@click="createToken"
>
{{ $t("Create token") }}
</a>
</div>
<section v-if="person.organizedEvents.length > 0">
<h2 class="subtitle">
{{ $t("Organized") }}
</h2>
<div class="columns">
<EventCard
v-for="event in person.organizedEvents"
:event="event"
:options="{ hideDetails: true, organizerActor: person }"
:key="event.uuid"
class="column is-one-third"
/>
</div>
<div class="field is-grouped">
<p class="control">
<a
class="button"
@click="deleteProfile()"
v-if="currentActor && currentActor.id === person.id"
>
{{ $t("Delete") }}
</a>
</p>
</div>
</section>
</div>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import EventCard from "@/components/Event/EventCard.vue";
import { FETCH_PERSON, CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import { MOBILIZON_INSTANCE_HOST } from "../../api/_entrypoint";
import { IPerson } from "../../types/actor";
import { CREATE_FEED_TOKEN_ACTOR } from "../../graphql/feed_tokens";
@Component({
apollo: {
person: {
query: FETCH_PERSON,
variables() {
return {
username: this.$route.params.name,
};
},
},
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
},
components: {
EventCard,
},
metaInfo() {
return {
title: this.$t("Profile") as string,
};
},
})
export default class Profile extends Vue {
@Prop({ type: String, required: true }) name!: string;
person!: IPerson;
currentActor!: IPerson;
feedUrls(format: "ics" | "webcal:" | "atom", isPublic = true): string {
let url = format === "ics" ? "webcal:" : "";
url += `//${MOBILIZON_INSTANCE_HOST}/`;
if (isPublic === true) {
url += `@${this.person.preferredUsername}/feed/`;
} else {
url += `events/going/${this.person.feedTokens[0].token}/`;
}
return url + (format === "ics" ? "ics" : "atom");
}
async createToken(): Promise<void> {
const { data } = await this.$apollo.mutate({
mutation: CREATE_FEED_TOKEN_ACTOR,
variables: { actor_id: this.person.id },
});
this.person.feedTokens.push(data);
}
}
</script>

View File

@ -14,8 +14,8 @@
}, },
]" ]"
/> />
<div class="actor-card"> <div>
<p v-if="group.suspended"> <p v-if="group.suspended" class="mx-auto max-w-sm block mb-2">
<actor-card <actor-card
:actor="group" :actor="group"
:full="true" :full="true"
@ -24,6 +24,7 @@
/> />
</p> </p>
<router-link <router-link
class="mx-auto max-w-sm block mb-2"
v-else v-else
:to="{ :to="{
name: RouteName.GROUP, name: RouteName.GROUP,
@ -572,16 +573,3 @@ export default class AdminGroupProfile extends Vue {
} }
} }
</script> </script>
<style lang="scss" scoped>
table,
section {
margin: 2rem 0;
}
.actor-card {
background: #fff;
padding: 1.5rem;
border-radius: 10px;
}
</style>

View File

@ -95,7 +95,7 @@
> >
<b-button <b-button
size="is-small" size="is-small"
v-if="!user.confirmedAt || !user.disabled" v-if="!user.confirmedAt || user.disabled"
@click="isConfirmationModalActive = true" @click="isConfirmationModalActive = true"
type="is-text" type="is-text"
icon-left="check" icon-left="check"

View File

@ -8,7 +8,9 @@
]" ]"
/> />
<h1 class="text-2xl">{{ instance.domain }}</h1> <h1 class="text-2xl">{{ instance.domain }}</h1>
<div class="grid md:grid-cols-4 gap-2 content-center text-center mt-2"> <div
class="grid md:grid-cols-2 xl:grid-cols-4 gap-2 content-center text-center mt-2"
>
<div class="bg-gray-50 rounded-xl p-8"> <div class="bg-gray-50 rounded-xl p-8">
<router-link <router-link
:to="{ :to="{
@ -64,8 +66,11 @@
<span class="text-sm block">{{ $t("Uploaded media size") }}</span> <span class="text-sm block">{{ $t("Uploaded media size") }}</span>
</div> </div>
</div> </div>
<div class="mt-3 grid md:grid-cols-2 gap-4" v-if="instance.hasRelay"> <div class="mt-3 grid xl:grid-cols-2 gap-4">
<div class="border bg-white p-6 shadow-md rounded-md"> <div
class="border bg-white p-6 shadow-md rounded-md"
v-if="instance.hasRelay"
>
<button <button
@click="removeInstanceFollow" @click="removeInstanceFollow"
v-if="instance.followedStatus == InstanceFollowStatus.APPROVED" v-if="instance.followedStatus == InstanceFollowStatus.APPROVED"
@ -88,7 +93,10 @@
{{ $t("Follow instance") }} {{ $t("Follow instance") }}
</button> </button>
</div> </div>
<div class="border bg-white p-6 shadow-md rounded-md"> <div v-else class="md:h-48 py-16 text-center opacity-50">
{{ $t("Only Mobilizon instances can be followed") }}
</div>
<div class="border bg-white p-6 shadow-md rounded-md flex flex-col gap-2">
<button <button
@click="acceptInstance" @click="acceptInstance"
v-if="instance.followerStatus == InstanceFollowStatus.PENDING" v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
@ -98,19 +106,16 @@
</button> </button>
<button <button
@click="rejectInstance" @click="rejectInstance"
v-else-if="instance.followerStatus != InstanceFollowStatus.NONE" v-if="instance.followerStatus != InstanceFollowStatus.NONE"
class="bg-red-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto" class="bg-red-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
> >
{{ $t("Reject follow") }} {{ $t("Reject follow") }}
</button> </button>
<p v-else> <p v-if="instance.followerStatus == InstanceFollowStatus.NONE">
{{ $t("This instance doesn't follow yours.") }} {{ $t("This instance doesn't follow yours.") }}
</p> </p>
</div> </div>
</div> </div>
<div v-else class="md:h-48 py-16 text-center opacity-50">
{{ $t("Only Mobilizon instances can be followed") }}
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -124,7 +129,6 @@ import {
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { formatBytes } from "@/utils/datetime"; import { formatBytes } from "@/utils/datetime";
import RouteName from "@/router/name"; import RouteName from "@/router/name";
import { SnackbarProgrammatic as Snackbar } from "buefy";
import { IInstance } from "@/types/instance.model"; import { IInstance } from "@/types/instance.model";
import { ApolloCache, gql, Reference } from "@apollo/client/core"; import { ApolloCache, gql, Reference } from "@apollo/client/core";
import { InstanceFollowStatus } from "@/types/enums"; import { InstanceFollowStatus } from "@/types/enums";
@ -154,38 +158,61 @@ export default class Instance extends Vue {
async acceptInstance(): Promise<void> { async acceptInstance(): Promise<void> {
try { try {
const { instance } = this;
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: ACCEPT_RELAY, mutation: ACCEPT_RELAY,
variables: { variables: {
address: `relay@${this.domain}`, address: this.instance.relayAddress,
},
update(cache: ApolloCache<any>) {
cache.writeFragment({
id: cache.identify(instance as unknown as Reference),
fragment: gql`
fragment InstanceFollowerStatus on Instance {
followerStatus
}
`,
data: {
followerStatus: InstanceFollowStatus.APPROVED,
}, },
}); });
} catch (e: any) { },
if (e.message) {
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
}); });
} catch (error: any) {
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
this.$notifier.error(error.graphQLErrors[0].message);
} }
} }
} }
/**
* Reject instance follow
*/
async rejectInstance(): Promise<void> { async rejectInstance(): Promise<void> {
try { try {
const { instance } = this;
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: REJECT_RELAY, mutation: REJECT_RELAY,
variables: { variables: {
address: `relay@${this.domain}`, address: this.instance.relayAddress,
},
update(cache: ApolloCache<any>) {
cache.writeFragment({
id: cache.identify(instance as unknown as Reference),
fragment: gql`
fragment InstanceFollowerStatus on Instance {
followerStatus
}
`,
data: {
followerStatus: InstanceFollowStatus.NONE,
}, },
}); });
} catch (e: any) { },
if (e.message) {
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
}); });
} catch (error: any) {
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
this.$notifier.error(error.graphQLErrors[0].message);
} }
} }
} }
@ -199,24 +226,23 @@ export default class Instance extends Vue {
domain: this.domain, domain: this.domain,
}, },
}); });
} catch (err: any) { } catch (error: any) {
if (err.message) { if (error.graphQLErrors && error.graphQLErrors.length > 0) {
Snackbar.open({ this.$notifier.error(error.graphQLErrors[0].message);
message: err.message,
type: "is-danger",
position: "is-bottom",
});
} }
} }
} }
/**
* Stop following instance
*/
async removeInstanceFollow(): Promise<void> { async removeInstanceFollow(): Promise<void> {
const { instance } = this; const { instance } = this;
try { try {
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: REMOVE_RELAY, mutation: REMOVE_RELAY,
variables: { variables: {
address: `relay@${this.domain}`, address: this.instance.relayAddress,
}, },
update(cache: ApolloCache<any>) { update(cache: ApolloCache<any>) {
cache.writeFragment({ cache.writeFragment({
@ -232,13 +258,9 @@ export default class Instance extends Vue {
}); });
}, },
}); });
} catch (e: any) { } catch (error: any) {
if (e.message) { if (error.graphQLErrors && error.graphQLErrors.length > 0) {
Snackbar.open({ this.$notifier.error(error.graphQLErrors[0].message);
message: e.message,
type: "is-danger",
position: "is-bottom",
});
} }
} }
} }

View File

@ -21,6 +21,11 @@
<b-button type="is-primary" native-type="submit">{{ <b-button type="is-primary" native-type="submit">{{
$t("Add an instance") $t("Add an instance")
}}</b-button> }}</b-button>
<b-loading
:is-full-page="true"
v-model="followInstanceLoading"
:can-cancel="false"
/>
</p> </p>
</b-field> </b-field>
</b-field> </b-field>
@ -124,6 +129,17 @@
</p> </p>
</div> </div>
</router-link> </router-link>
<b-pagination
v-show="instances.total > INSTANCES_PAGE_LIMIT"
:total="instances.total"
v-model="instancePage"
:per-page="INSTANCES_PAGE_LIMIT"
:aria-next-label="$t('Next page')"
:aria-previous-label="$t('Previous page')"
:aria-page-label="$t('Page')"
:aria-current-label="$t('Current page')"
>
</b-pagination>
</div> </div>
<div v-else-if="instances && instances.elements.length == 0"> <div v-else-if="instances && instances.elements.length == 0">
<empty-content icon="lan-disconnect" :inline="true"> <empty-content icon="lan-disconnect" :inline="true">
@ -160,9 +176,10 @@ import {
InstanceFilterFollowStatus, InstanceFilterFollowStatus,
InstanceFollowStatus, InstanceFollowStatus,
} from "@/types/enums"; } from "@/types/enums";
import { SnackbarProgrammatic as Snackbar } from "buefy";
const { isNavigationFailure, NavigationFailureType } = VueRouter; const { isNavigationFailure, NavigationFailureType } = VueRouter;
const INSTANCES_PAGE_LIMIT = 10;
@Component({ @Component({
apollo: { apollo: {
instances: { instances: {
@ -171,7 +188,7 @@ const { isNavigationFailure, NavigationFailureType } = VueRouter;
variables() { variables() {
return { return {
page: this.instancePage, page: this.instancePage,
limit: 10, limit: INSTANCES_PAGE_LIMIT,
filterDomain: this.filterDomain, filterDomain: this.filterDomain,
filterFollowStatus: this.followStatus, filterFollowStatus: this.followStatus,
}; };
@ -190,6 +207,8 @@ const { isNavigationFailure, NavigationFailureType } = VueRouter;
export default class Follows extends Vue { export default class Follows extends Vue {
RouteName = RouteName; RouteName = RouteName;
followInstanceLoading = false;
newRelayAddress = ""; newRelayAddress = "";
instances!: Paginate<IInstance>; instances!: Paginate<IInstance>;
@ -204,6 +223,8 @@ export default class Follows extends Vue {
InstanceFollowStatus = InstanceFollowStatus; InstanceFollowStatus = InstanceFollowStatus;
INSTANCES_PAGE_LIMIT = INSTANCES_PAGE_LIMIT;
data(): Record<string, unknown> { data(): Record<string, unknown> {
return { return {
debouncedUpdateDomainFilter: debounce(this.updateDomainFilter, 500), debouncedUpdateDomainFilter: debounce(this.updateDomainFilter, 500),
@ -242,6 +263,7 @@ export default class Follows extends Vue {
async followInstance(e: Event): Promise<void> { async followInstance(e: Event): Promise<void> {
e.preventDefault(); e.preventDefault();
this.followInstanceLoading = true;
const domain = this.newRelayAddress.trim(); // trim to fix copy and paste domain name spaces and tabs const domain = this.newRelayAddress.trim(); // trim to fix copy and paste domain name spaces and tabs
try { try {
await this.$apollo.mutate<{ relayFollowings: Paginate<IFollower> }>({ await this.$apollo.mutate<{ relayFollowings: Paginate<IFollower> }>({
@ -251,19 +273,19 @@ export default class Follows extends Vue {
}, },
}); });
this.newRelayAddress = ""; this.newRelayAddress = "";
this.followInstanceLoading = false;
this.$router.push({ this.$router.push({
name: RouteName.INSTANCE, name: RouteName.INSTANCE,
params: { domain }, params: { domain },
}); });
} catch (err: any) { } catch (error: any) {
if (err.message) { if (error.message) {
Snackbar.open({ if (error.graphQLErrors && error.graphQLErrors.length > 0) {
message: err.message, this.$notifier.error(error.graphQLErrors[0].message);
type: "is-danger",
position: "is-bottom",
});
} }
} }
this.followInstanceLoading = false;
}
} }
private async pushRouter(args: Record<string, string>): Promise<void> { private async pushRouter(args: Record<string, string>): Promise<void> {

View File

@ -615,6 +615,7 @@ import {
EventJoinOptions, EventJoinOptions,
EventStatus, EventStatus,
EventVisibility, EventVisibility,
GroupVisibility,
MemberRole, MemberRole,
ParticipantRole, ParticipantRole,
} from "@/types/enums"; } from "@/types/enums";
@ -638,6 +639,7 @@ import {
LOGGED_USER_DRAFTS, LOGGED_USER_DRAFTS,
PERSON_STATUS_GROUP, PERSON_STATUS_GROUP,
} from "../../graphql/actor"; } from "../../graphql/actor";
import { FETCH_GROUP } from "../../graphql/group";
import { import {
displayNameAndUsername, displayNameAndUsername,
IActor, IActor,
@ -718,6 +720,21 @@ const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
); );
}, },
}, },
group: {
query: FETCH_GROUP,
fetchPolicy: "cache-and-network",
variables() {
return {
name: this.event?.attributedTo?.preferredUsername,
};
},
skip() {
return (
!this.event?.attributedTo ||
!this.event?.attributedTo?.preferredUsername
);
},
},
}, },
metaInfo() { metaInfo() {
return { return {
@ -736,6 +753,8 @@ export default class EditEvent extends Vue {
@Prop({ type: Boolean, default: false }) isDuplicate!: boolean; @Prop({ type: Boolean, default: false }) isDuplicate!: boolean;
group!: IGroup;
currentActor!: IActor; currentActor!: IActor;
loggedUser!: IUser; loggedUser!: IUser;
@ -781,6 +800,16 @@ export default class EditEvent extends Vue {
} }
} }
@Watch("group")
updateEventVisibility(group: IGroup): void {
if (!this.isUpdate && group.visibility == GroupVisibility.UNLISTED) {
this.event.visibility = EventVisibility.UNLISTED;
}
if (!this.isUpdate && group.visibility == GroupVisibility.PUBLIC) {
this.event.visibility = EventVisibility.PUBLIC;
}
}
private initializeEvent() { private initializeEvent() {
const roundUpTo15Minutes = (time: Date) => { const roundUpTo15Minutes = (time: Date) => {
time.setMilliseconds(Math.round(time.getMilliseconds() / 1000) * 1000); time.setMilliseconds(Math.round(time.getMilliseconds() / 1000) * 1000);

View File

@ -24,10 +24,14 @@
:actor="event.organizerActor" :actor="event.organizerActor"
:inline="true" :inline="true"
> >
<i18n path="By {username}" dir="auto"> <i18n
<span dir="ltr" slot="username" path="By {username}"
>@{{ usernameWithDomain(event.organizerActor) }}</span dir="auto"
class="block truncate max-w-xs md:max-w-sm"
> >
<span dir="ltr" slot="username">{{
displayName(event.organizerActor)
}}</span>
</i18n> </i18n>
</popover-actor-card> </popover-actor-card>
</div> </div>
@ -36,9 +40,23 @@
:actor="event.attributedTo" :actor="event.attributedTo"
:inline="true" :inline="true"
> >
<i18n path="By {group}" dir="auto"> <i18n
<span dir="ltr" slot="group" path="By {group}"
>@{{ usernameWithDomain(event.attributedTo) }}</span dir="auto"
class="block truncate max-w-xs md:max-w-sm"
>
<router-link
:to="{
name: RouteName.GROUP,
params: {
preferredUsername: usernameWithDomain(
event.attributedTo
),
},
}"
dir="ltr"
slot="group"
>{{ displayName(event.attributedTo) }}</router-link
> >
</i18n> </i18n>
</popover-actor-card> </popover-actor-card>
@ -474,7 +492,13 @@ import {
} from "../../graphql/event"; } from "../../graphql/event";
import { CURRENT_ACTOR_CLIENT, PERSON_STATUS_GROUP } from "../../graphql/actor"; import { CURRENT_ACTOR_CLIENT, PERSON_STATUS_GROUP } from "../../graphql/actor";
import { EventModel, IEvent } from "../../types/event.model"; import { EventModel, IEvent } from "../../types/event.model";
import { IActor, IPerson, Person, usernameWithDomain } from "../../types/actor"; import {
displayName,
IActor,
IPerson,
Person,
usernameWithDomain,
} from "../../types/actor";
import { GRAPHQL_API_ENDPOINT } from "../../api/_entrypoint"; import { GRAPHQL_API_ENDPOINT } from "../../api/_entrypoint";
import DateCalendarIcon from "../../components/Event/DateCalendarIcon.vue"; import DateCalendarIcon from "../../components/Event/DateCalendarIcon.vue";
import MultiCard from "../../components/Event/MultiCard.vue"; import MultiCard from "../../components/Event/MultiCard.vue";
@ -659,6 +683,8 @@ export default class Event extends EventMixin {
usernameWithDomain = usernameWithDomain; usernameWithDomain = usernameWithDomain;
displayName = displayName;
RouteName = RouteName; RouteName = RouteName;
observer!: IntersectionObserver; observer!: IntersectionObserver;

View File

@ -18,7 +18,7 @@
<h1 class="title" v-if="group"> <h1 class="title" v-if="group">
{{ {{
$t("{group}'s events", { $t("{group}'s events", {
group: group.name || group.preferredUsername, group: displayName(group),
}) })
}} }}
</h1> </h1>
@ -43,23 +43,42 @@
<subtitle> <subtitle>
{{ showPassedEvents ? $t("Past events") : $t("Upcoming events") }} {{ showPassedEvents ? $t("Past events") : $t("Upcoming events") }}
</subtitle> </subtitle>
<b-switch v-model="showPassedEvents">{{ $t("Past events") }}</b-switch> <b-switch class="mb-4" v-model="showPassedEvents">{{
$t("Past events")
}}</b-switch>
<grouped-multi-event-minimalist-card <grouped-multi-event-minimalist-card
:events="group.organizedEvents.elements" :events="group.organizedEvents.elements"
:isCurrentActorMember="isCurrentActorMember" :isCurrentActorMember="isCurrentActorMember"
/> />
<b-message <empty-content
v-if=" v-if="
group.organizedEvents.elements.length === 0 && group.organizedEvents.elements.length === 0 &&
$apollo.loading === false $apollo.loading === false
" "
type="is-danger" icon="calendar"
:inline="true"
:center="true"
> >
{{ $t("No events found") }} {{ $t("No events found") }}
</b-message> <template v-if="group.domain !== null">
<div class="mt-4">
<p>
{{
$t(
"This group is a remote group, it's possible the original instance has more informations."
)
}}
</p>
<b-button type="is-text" tag="a" :href="group.url">
{{ $t("View the group profile on the original instance") }}
</b-button>
</div>
</template>
</empty-content>
<b-pagination <b-pagination
class="mt-4"
:total="group.organizedEvents.total" :total="group.organizedEvents.total"
v-model="eventsPage" v-model="page"
:per-page="EVENTS_PAGE_LIMIT" :per-page="EVENTS_PAGE_LIMIT"
:aria-next-label="$t('Next page')" :aria-next-label="$t('Next page')"
:aria-previous-label="$t('Previous page')" :aria-previous-label="$t('Previous page')"
@ -72,16 +91,15 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { mixins } from "vue-class-component";
import RouteName from "@/router/name"; import RouteName from "@/router/name";
import Subtitle from "@/components/Utils/Subtitle.vue"; import Subtitle from "@/components/Utils/Subtitle.vue";
import GroupedMultiEventMinimalistCard from "@/components/Event/GroupedMultiEventMinimalistCard.vue"; import GroupedMultiEventMinimalistCard from "@/components/Event/GroupedMultiEventMinimalistCard.vue";
import { PERSON_MEMBERSHIPS } from "@/graphql/actor"; import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import GroupMixin from "@/mixins/group";
import { IMember } from "@/types/actor/member.model"; import { IMember } from "@/types/actor/member.model";
import { FETCH_GROUP_EVENTS } from "@/graphql/event"; import { FETCH_GROUP_EVENTS } from "@/graphql/event";
import { displayName, usernameWithDomain } from "../../types/actor"; import EmptyContent from "../../components/Utils/EmptyContent.vue";
import { displayName, IGroup, usernameWithDomain } from "../../types/actor";
const EVENTS_PAGE_LIMIT = 10; const EVENTS_PAGE_LIMIT = 10;
@ -107,13 +125,15 @@ const EVENTS_PAGE_LIMIT = 10;
name: this.$route.params.preferredUsername, name: this.$route.params.preferredUsername,
beforeDateTime: this.showPassedEvents ? new Date() : null, beforeDateTime: this.showPassedEvents ? new Date() : null,
afterDateTime: this.showPassedEvents ? null : new Date(), afterDateTime: this.showPassedEvents ? null : new Date(),
organisedEventsPage: this.eventsPage, organisedEventsPage: this.page,
organisedEventsLimit: EVENTS_PAGE_LIMIT, organisedEventsLimit: EVENTS_PAGE_LIMIT,
}; };
}, },
update: (data) => data.group,
}, },
}, },
components: { components: {
EmptyContent,
Subtitle, Subtitle,
GroupedMultiEventMinimalistCard, GroupedMultiEventMinimalistCard,
}, },
@ -123,15 +143,27 @@ const EVENTS_PAGE_LIMIT = 10;
const { group } = this; const { group } = this;
return { return {
title: this.$t("{group} events", { title: this.$t("{group} events", {
group: group?.name || usernameWithDomain(group), group: displayName(group),
}) as string, }) as string,
}; };
}, },
}) })
export default class GroupEvents extends mixins(GroupMixin) { export default class GroupEvents extends Vue {
group!: IGroup;
memberships!: IMember[]; memberships!: IMember[];
eventsPage = 1; get page(): number {
return parseInt((this.$route.query.page as string) || "1", 10);
}
set page(page: number) {
this.$router.push({
name: RouteName.GROUP_EVENTS,
query: { ...this.$route.query, page: page.toString() },
});
this.$apollo.queries.group.refetch();
}
usernameWithDomain = usernameWithDomain; usernameWithDomain = usernameWithDomain;
@ -149,14 +181,11 @@ export default class GroupEvents extends mixins(GroupMixin) {
} }
get showPassedEvents(): boolean { get showPassedEvents(): boolean {
return ( return this.$route.query.future === "false";
this.$route.query.future !== undefined &&
this.$route.query.future.toString() === "false"
);
} }
set showPassedEvents(value: boolean) { set showPassedEvents(value: boolean) {
this.$router.push({ query: { future: this.showPassedEvents.toString() } }); this.$router.replace({ query: { future: (!value).toString() } });
} }
} }
</script> </script>

View File

@ -27,11 +27,11 @@
<div class="title-container"> <div class="title-container">
<h1 v-if="group.name">{{ group.name }}</h1> <h1 v-if="group.name">{{ group.name }}</h1>
<b-skeleton v-else :animated="true" /> <b-skeleton v-else :animated="true" />
<small <span
dir="ltr" dir="ltr"
class="has-text-grey-dark" class="has-text-grey-dark"
v-if="group.preferredUsername" v-if="group.preferredUsername"
>@{{ usernameWithDomain(group) }}</small >@{{ usernameWithDomain(group) }}</span
> >
<b-skeleton v-else :animated="true" /> <b-skeleton v-else :animated="true" />
<br /> <br />
@ -78,7 +78,7 @@
> >
</p> </p>
</div> </div>
<div class="buttons"> <div class="flex gap-2">
<b-button <b-button
outlined outlined
icon-left="timeline-text" icon-left="timeline-text"
@ -101,78 +101,123 @@
}" }"
>{{ $t("Group settings") }}</b-button >{{ $t("Group settings") }}</b-button
> >
<b-tooltip <b-dropdown
v-if=" aria-role="list"
(!isCurrentActorAGroupMember || previewPublic) && trap-focus
group.openness === Openness.INVITE_ONLY v-show="showJoinButton && showFollowButton"
"
:label="$t('This group is invite-only')"
position="is-bottom"
>
<b-button disabled type="is-primary">{{
$t("Join group")
}}</b-button></b-tooltip
> >
<template #trigger>
<b-button <b-button
v-else-if=" :label="$t('Follow')"
((!isCurrentActorAGroupMember &&
!isCurrentActorAPendingGroupMember) ||
previewPublic) &&
currentActor.id
"
@click="joinGroup"
@keyup.enter="joinGroup"
type="is-primary" type="is-primary"
:disabled="previewPublic" icon-left="rss"
>{{ $t("Join group") }}</b-button icon-right="menu-down"
/>
</template>
<b-dropdown-item
aria-role="listitem"
class="p-0"
custom
:focusable="false"
:disabled="
isCurrentActorPendingFollow && currentActor.id !== undefined
"
> >
<button class="media py-4 px-2 w-full" @click="followGroup">
<b-icon class="media-left" icon="rss" />
<div class="media-content">
<h3 class="font-medium text-lg">{{ $t("Follow") }}</h3>
<p class="whitespace-normal md:whitespace-nowrap text-sm">
{{ $t("Get informed of the upcoming public events") }}
</p>
<p
v-if="
doesGroupManuallyApprovesFollowers &&
!isCurrentActorPendingFollow
"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{
$t(
"Follow requests will be approved by a group moderator"
)
}}
</p>
<p
v-if="isCurrentActorPendingFollow && currentActor.id"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{ $t("Follow request pending approval") }}
</p>
</div>
</button>
</b-dropdown-item>
<b-dropdown-item
aria-role="listitem"
class="p-0 border-t border-solid"
custom
:focusable="false"
:disabled="
isGroupInviteOnly || isCurrentActorAPendingGroupMember
"
>
<button class="media py-4 px-2 w-full" @click="joinGroup">
<b-icon
class="media-left"
icon="account-multiple-plus"
></b-icon>
<div class="media-content">
<h3 class="font-medium text-lg">{{ $t("Join") }}</h3>
<div v-if="showJoinButton">
<p
class="whitespace-normal md:whitespace-nowrap text-sm"
>
{{
$t(
"Become part of the community and start organizing events"
)
}}
</p>
<p
v-if="isGroupInviteOnly"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{ $t("This group is invite-only") }}
</p>
<p
v-if="
areGroupMembershipsModerated &&
!isCurrentActorAPendingGroupMember
"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{
$t(
"Membership requests will be approved by a group moderator"
)
}}
</p>
<p
v-if="isCurrentActorAPendingGroupMember"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{ $t("Your membership is pending approval") }}
</p>
</div>
</div>
</button>
</b-dropdown-item>
</b-dropdown>
<b-button <b-button
outlined outlined
v-else-if="isCurrentActorAPendingGroupMember" v-if="isCurrentActorAPendingGroupMember"
@click="leaveGroup" @click="leaveGroup"
@keyup.enter="leaveGroup" @keyup.enter="leaveGroup"
type="is-primary" type="is-primary"
>{{ $t("Cancel membership request") }}</b-button >{{ $t("Cancel membership request") }}</b-button
> >
<b-button
tag="router-link"
:to="{
name: RouteName.GROUP_JOIN,
params: { preferredUsername: usernameWithDomain(group) },
}"
v-else-if="!isCurrentActorAGroupMember || previewPublic"
:disabled="previewPublic"
type="is-primary"
>{{ $t("Join group") }}</b-button
>
<b-button
v-if="
((!isCurrentActorFollowing && !isCurrentActorAGroupMember) ||
previewPublic) &&
!isCurrentActorPendingFollow &&
currentActor.id
"
@click="followGroup"
@keyup.enter="followGroup"
type="is-primary"
:disabled="isCurrentActorPendingFollow"
>{{ $t("Follow") }}</b-button
>
<b-button
tag="router-link"
:to="{
name: RouteName.GROUP_FOLLOW,
params: { preferredUsername: usernameWithDomain(group) },
}"
v-else-if="
!isCurrentActorPendingFollow &&
!isCurrentActorFollowing &&
previewPublic
"
:disabled="previewPublic"
type="is-primary"
>{{ $t("Follow") }}</b-button
>
<b-button <b-button
outlined outlined
v-if="isCurrentActorPendingFollow && currentActor.id" v-if="isCurrentActorPendingFollow && currentActor.id"
@ -192,12 +237,20 @@
v-if="isCurrentActorFollowing" v-if="isCurrentActorFollowing"
@click="toggleFollowNotify" @click="toggleFollowNotify"
@keyup.enter="toggleFollowNotify" @keyup.enter="toggleFollowNotify"
class="notification-button p-1.5"
outlined
:icon-left=" :icon-left="
isCurrentActorFollowingNotify isCurrentActorFollowingNotify
? 'bell-outline' ? 'bell-outline'
: 'bell-off-outline' : 'bell-off-outline'
" "
></b-button> >
<span class="sr-only">{{
isCurrentActorFollowingNotify
? $t("Activate notifications")
: $t("Deactivate notifications")
}}</span>
</b-button>
<b-button <b-button
outlined outlined
icon-left="share" icon-left="share"
@ -308,28 +361,6 @@
) )
}} }}
</b-message> </b-message>
<b-message
v-if="
!isCurrentActorAGroupMember &&
!isCurrentActorAPendingGroupMember &&
!isCurrentActorPendingFollow &&
!isCurrentActorFollowing
"
type="is-info"
has-icon
class="m-3"
>
<i18n
path="Following the group will allow you to be informed of the {group_upcoming_public_events}, whereas joining the group means you will {access_to_group_private_content_as_well}, including group discussions, group resources and members-only posts."
>
<b slot="group_upcoming_public_events">{{
$t("group's upcoming public events")
}}</b>
<b slot="access_to_group_private_content_as_well">{{
$t("access to the group's private content as well")
}}</b>
</i18n>
</b-message>
</div> </div>
</header> </header>
</div> </div>
@ -506,6 +537,12 @@
$t("View full profile") $t("View full profile")
}}</a> }}</a>
</b-message> </b-message>
<event-metadata-block
:title="$t('About')"
v-if="group.summary && group.summary !== '<p></p>'"
>
<div dir="auto" v-html="group.summary" />
</event-metadata-block>
<event-metadata-block :title="$t('Members')" icon="account-group"> <event-metadata-block :title="$t('Members')" icon="account-group">
{{ {{
$tc("{count} members", group.members.total, { $tc("{count} members", group.members.total, {
@ -521,9 +558,10 @@
" "
> >
<div class="address-wrapper"> <div class="address-wrapper">
<span v-if="!physicalAddress">{{ <span
$t("No address defined") v-if="!physicalAddress || !addressFullName(physicalAddress)"
}}</span> >{{ $t("No address defined") }}</span
>
<div class="address" v-if="physicalAddress"> <div class="address" v-if="physicalAddress">
<div> <div>
<address dir="auto"> <address dir="auto">
@ -553,17 +591,6 @@
</div> </div>
</aside> </aside>
<div class="main-content"> <div class="main-content">
<section>
<subtitle>{{ $t("About") }}</subtitle>
<div
dir="auto"
v-html="group.summary"
v-if="group.summary && group.summary !== '<p></p>'"
/>
<empty-content v-else-if="group" icon="image-text" :inline="true">
{{ $t("This group doesn't have a description yet.") }}
</empty-content>
</section>
<section> <section>
<subtitle>{{ $t("Upcoming events") }}</subtitle> <subtitle>{{ $t("Upcoming events") }}</subtitle>
<div <div
@ -577,9 +604,15 @@
class="organized-event" class="organized-event"
/> />
</div> </div>
<empty-content v-else-if="group" icon="calendar" :inline="true"> <empty-content
v-else-if="group"
icon="calendar"
:inline="true"
description-classes="flex flex-col items-stretch"
>
{{ $t("No public upcoming events") }} {{ $t("No public upcoming events") }}
<template #desc v-if="isCurrentActorFollowing"> <template #desc>
<template v-if="isCurrentActorFollowing">
<i18n <i18n
class="has-text-grey-dark" class="has-text-grey-dark"
path="You will receive notifications about this group's public activity depending on %{notification_settings}." path="You will receive notifications about this group's public activity depending on %{notification_settings}."
@ -591,20 +624,37 @@
> >
</i18n> </i18n>
</template> </template>
<b-button
tag="router-link"
class="my-2 self-center"
type="is-text"
:to="{
name: RouteName.GROUP_EVENTS,
params: { preferredUsername: usernameWithDomain(group) },
query: { future: false },
}"
>{{ $t("View past events") }}</b-button
>
</template>
</empty-content> </empty-content>
<b-skeleton animated v-else-if="$apollo.loading"></b-skeleton> <b-skeleton animated v-else-if="$apollo.loading"></b-skeleton>
<router-link <div class="flex justify-center">
<b-button
tag="router-link"
class="my-4"
type="is-text"
v-if="organizedEvents.total > 0" v-if="organizedEvents.total > 0"
:to="{ :to="{
name: RouteName.GROUP_EVENTS, name: RouteName.GROUP_EVENTS,
params: { preferredUsername: usernameWithDomain(group) }, params: { preferredUsername: usernameWithDomain(group) },
query: { future: organizedEvents.elements.length > 0 }, query: { future: organizedEvents.elements.length > 0 },
}" }"
>{{ $t("View all events") }}</router-link >{{ $t("View all events") }}</b-button
> >
</div>
</section> </section>
<section> <section class="flex flex-col items-stretch">
<subtitle>{{ $t("Latest posts") }}</subtitle> <subtitle class="ml-0">{{ $t("Latest posts") }}</subtitle>
<multi-post-list-item <multi-post-list-item
v-if=" v-if="
@ -624,13 +674,16 @@
{{ $t("No posts yet") }} {{ $t("No posts yet") }}
</empty-content> </empty-content>
<b-skeleton animated v-else-if="$apollo.loading"></b-skeleton> <b-skeleton animated v-else-if="$apollo.loading"></b-skeleton>
<router-link <b-button
class="self-center my-2"
v-if="posts.total > 0" v-if="posts.total > 0"
tag="router-link"
type="is-text"
:to="{ :to="{
name: RouteName.POSTS, name: RouteName.POSTS,
params: { preferredUsername: usernameWithDomain(group) }, params: { preferredUsername: usernameWithDomain(group) },
}" }"
>{{ $t("View all posts") }}</router-link >{{ $t("View all posts") }}</b-button
> >
</section> </section>
</div> </div>
@ -687,7 +740,7 @@ import DiscussionListItem from "@/components/Discussion/DiscussionListItem.vue";
import MultiPostListItem from "@/components/Post/MultiPostListItem.vue"; import MultiPostListItem from "@/components/Post/MultiPostListItem.vue";
import ResourceItem from "@/components/Resource/ResourceItem.vue"; import ResourceItem from "@/components/Resource/ResourceItem.vue";
import FolderItem from "@/components/Resource/FolderItem.vue"; import FolderItem from "@/components/Resource/FolderItem.vue";
import { Address } from "@/types/address.model"; import { Address, addressFullName } from "@/types/address.model";
import Invitations from "@/components/Group/Invitations.vue"; import Invitations from "@/components/Group/Invitations.vue";
import addMinutes from "date-fns/addMinutes"; import addMinutes from "date-fns/addMinutes";
import { CONFIG } from "@/graphql/config"; import { CONFIG } from "@/graphql/config";
@ -768,6 +821,8 @@ export default class Group extends mixins(GroupMixin) {
displayName = displayName; displayName = displayName;
addressFullName = addressFullName;
PostVisibility = PostVisibility; PostVisibility = PostVisibility;
Openness = Openness; Openness = Openness;
@ -788,11 +843,19 @@ export default class Group extends mixins(GroupMixin) {
} }
async joinGroup(): Promise<void> { async joinGroup(): Promise<void> {
if (!this.currentActor?.id) {
this.$router.push({
name: RouteName.GROUP_JOIN,
params: { preferredUsername: usernameWithDomain(this.group) },
});
return;
}
try {
const [group, currentActorId] = [ const [group, currentActorId] = [
usernameWithDomain(this.group), usernameWithDomain(this.group),
this.currentActor.id, this.currentActor.id,
]; ];
this.$apollo.mutate({ await this.$apollo.mutate({
mutation: JOIN_GROUP, mutation: JOIN_GROUP,
variables: { variables: {
groupId: this.group.id, groupId: this.group.id,
@ -807,6 +870,11 @@ export default class Group extends mixins(GroupMixin) {
}, },
], ],
}); });
} catch (error: any) {
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
this.$notifier.error(error.graphQLErrors[0].message);
}
}
} }
protected async openLeaveGroupModal(): Promise<void> { protected async openLeaveGroupModal(): Promise<void> {
@ -852,6 +920,13 @@ export default class Group extends mixins(GroupMixin) {
} }
async followGroup(): Promise<void> { async followGroup(): Promise<void> {
if (!this.currentActor?.id) {
this.$router.push({
name: RouteName.GROUP_FOLLOW,
params: { preferredUsername: usernameWithDomain(this.group) },
});
return;
}
try { try {
const [group, currentActorId] = [ const [group, currentActorId] = [
usernameWithDomain(this.group), usernameWithDomain(this.group),
@ -1070,6 +1145,41 @@ export default class Group extends mixins(GroupMixin) {
}), }),
}; };
} }
get showFollowButton(): boolean {
return (
(!this.isCurrentActorFollowing || this.previewPublic) &&
this.currentActor?.id !== undefined
);
}
get showJoinButton(): boolean {
return (
(!this.isCurrentActorAGroupMember || this.previewPublic) &&
this.currentActor?.id !== undefined
);
}
get isGroupInviteOnly(): boolean {
return (
(!this.isCurrentActorAGroupMember || this.previewPublic) &&
this.group?.openness === Openness.INVITE_ONLY
);
}
get areGroupMembershipsModerated(): boolean {
return (
(!this.isCurrentActorAGroupMember || this.previewPublic) &&
this.group?.openness === Openness.MODERATED
);
}
get doesGroupManuallyApprovesFollowers(): boolean {
return (
(!this.isCurrentActorAGroupMember || this.previewPublic) &&
this.group?.manuallyApprovesFollowers
);
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -1206,6 +1316,7 @@ div.container {
.media-content { .media-content {
h2 { h2 {
color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, font-family: "Liberation Sans", "Helvetica Neue", Roboto,
Helvetica, Arial, serif; Helvetica, Arial, serif;
font-size: 1.5rem; font-size: 1.5rem;
@ -1361,4 +1472,7 @@ div.container {
height: 60vh; height: 60vh;
width: 100%; width: 100%;
} }
button.button.notification-button ::v-deep span.icon.is-small {
margin: 0 !important;
}
</style> </style>

View File

@ -259,6 +259,7 @@ export default class GroupSettings extends mixins(GroupMixin) {
@Watch("group") @Watch("group")
async watchUpdateGroup(oldGroup: IGroup, newGroup: IGroup): Promise<void> { async watchUpdateGroup(oldGroup: IGroup, newGroup: IGroup): Promise<void> {
try {
if ( if (
oldGroup?.avatar !== undefined && oldGroup?.avatar !== undefined &&
oldGroup?.avatar !== newGroup?.avatar oldGroup?.avatar !== newGroup?.avatar
@ -271,6 +272,10 @@ export default class GroupSettings extends mixins(GroupMixin) {
) { ) {
this.bannerFile = await buildFileFromIMedia(this.group.banner); this.bannerFile = await buildFileFromIMedia(this.group.banner);
} }
} catch (e) {
// Catch errors while building media
console.error(e);
}
this.editableGroup = { ...this.group }; this.editableGroup = { ...this.group };
} }

View File

@ -15,7 +15,9 @@
v-if="post.draft" v-if="post.draft"
>{{ $t("Draft") }}</b-tag >{{ $t("Draft") }}</b-tag
> >
<h1 class="title" :lang="post.language">{{ post.title }}</h1> <h1 class="title text-3xl" :lang="post.language">
{{ post.title }}
</h1>
</div> </div>
<p class="metadata"> <p class="metadata">
<router-link <router-link
@ -441,7 +443,6 @@ article.post {
h1.title { h1.title {
margin: 0; margin: 0;
font-weight: 500; font-weight: 500;
font-size: 38px;
font-family: "Roboto", "Helvetica", "Arial", serif; font-family: "Roboto", "Helvetica", "Arial", serif;
} }

View File

@ -9,10 +9,7 @@
<b-icon icon="folder" /> <b-icon icon="folder" />
{{ $t("New folder") }} {{ $t("New folder") }}
</b-dropdown-item> </b-dropdown-item>
<b-dropdown-item <b-dropdown-item aria-role="listitem" @click="createLinkModal">
aria-role="listitem"
@click="createLinkResourceModal = true"
>
<b-icon icon="link" /> <b-icon icon="link" />
{{ $t("New link") }} {{ $t("New link") }}
</b-dropdown-item> </b-dropdown-item>
@ -124,7 +121,11 @@
<section class="modal-card-body"> <section class="modal-card-body">
<form @submit.prevent="renameResource"> <form @submit.prevent="renameResource">
<b-field :label="$t('Title')"> <b-field :label="$t('Title')">
<b-input aria-required="true" v-model="updatedResource.title" /> <b-input
ref="resourceRenameInput"
aria-required="true"
v-model="updatedResource.title"
/>
</b-field> </b-field>
<b-button native-type="submit">{{ <b-button native-type="submit">{{
@ -154,12 +155,17 @@
:active.sync="createResourceModal" :active.sync="createResourceModal"
has-modal-card has-modal-card
:close-button-aria-label="$t('Close')" :close-button-aria-label="$t('Close')"
trap-focus
> >
<div class="modal-card"> <div class="modal-card">
<section class="modal-card-body"> <section class="modal-card-body">
<b-message type="is-danger" v-if="modalError">
{{ modalError }}
</b-message>
<form @submit.prevent="createResource"> <form @submit.prevent="createResource">
<b-field :label="$t('Title')" label-for="new-resource-title"> <b-field :label="$t('Title')" label-for="new-resource-title">
<b-input <b-input
ref="modalNewResourceInput"
aria-required="true" aria-required="true"
v-model="newResource.title" v-model="newResource.title"
id="new-resource-title" id="new-resource-title"
@ -179,6 +185,7 @@
class="link-resource-modal" class="link-resource-modal"
aria-modal aria-modal
:close-button-aria-label="$t('Close')" :close-button-aria-label="$t('Close')"
trap-focus
> >
<div class="modal-card"> <div class="modal-card">
<section class="modal-card-body"> <section class="modal-card-body">
@ -193,6 +200,7 @@
required required
v-model="newResource.resourceUrl" v-model="newResource.resourceUrl"
@blur="previewResource" @blur="previewResource"
ref="modalNewResourceLinkInput"
/> />
</b-field> </b-field>
@ -355,6 +363,12 @@ export default class Resources extends Mixins(ResourceMixin) {
put: true, put: true,
}; };
$refs!: {
resourceRenameInput: any;
modalNewResourceInput: HTMLElement;
modalNewResourceLinkInput: HTMLElement;
};
mapServiceTypeToIcon = mapServiceTypeToIcon; mapServiceTypeToIcon = mapServiceTypeToIcon;
get page(): number { get page(): number {
@ -458,15 +472,25 @@ export default class Resources extends Mixins(ResourceMixin) {
} }
} }
createFolderModal(): void { async createLinkModal(): Promise<void> {
this.newResource.type = "folder"; this.createLinkResourceModal = true;
this.createResourceModal = true; await this.$nextTick();
this.$refs.modalNewResourceLinkInput.focus();
} }
createResourceFromProvider(provider: IProvider): void { async createFolderModal(): Promise<void> {
this.newResource.type = "folder";
this.createResourceModal = true;
await this.$nextTick();
this.$refs.modalNewResourceInput.focus();
}
async createResourceFromProvider(provider: IProvider): Promise<void> {
this.newResource.resourceUrl = Resources.generateFullResourceUrl(provider); this.newResource.resourceUrl = Resources.generateFullResourceUrl(provider);
this.newResource.type = provider.software; this.newResource.type = provider.software;
this.createResourceModal = true; this.createResourceModal = true;
await this.$nextTick();
this.$refs.modalNewResourceInput.focus();
} }
static generateFullResourceUrl(provider: IProvider): string { static generateFullResourceUrl(provider: IProvider): string {
@ -549,10 +573,12 @@ export default class Resources extends Mixins(ResourceMixin) {
} }
} }
handleRename(resource: IResource): void { async handleRename(resource: IResource): Promise<void> {
console.log("handleRename");
this.renameModal = true; this.renameModal = true;
this.updatedResource = { ...resource }; this.updatedResource = { ...resource };
await this.$nextTick();
this.$refs.resourceRenameInput.focus();
this.$refs.resourceRenameInput.$el.querySelector("input").select();
} }
handleMove(resource: IResource): void { handleMove(resource: IResource): void {

View File

@ -93,6 +93,7 @@
</b-select> </b-select>
</b-field> </b-field>
<b-field <b-field
v-if="config"
expanded expanded
:label="$t('Category')" :label="$t('Category')"
label-for="category" label-for="category"
@ -564,8 +565,6 @@ export default class Search extends Vue {
} }
private prepareLocation(value: string | undefined): void { private prepareLocation(value: string | undefined): void {
console.info("geohash location", this.location);
if (value !== undefined) { if (value !== undefined) {
// decode // decode
const latlon = ngeohash.decode(value); const latlon = ngeohash.decode(value);

View File

@ -18,7 +18,9 @@
$t( $t(
"Error while login with {provider}. Retry or login another way.", "Error while login with {provider}. Retry or login another way.",
{ {
provider: $route.query.provider, provider:
SELECTED_PROVIDERS[$route.query.provider] ||
"unknown provider",
} }
) )
}}</b-message }}</b-message
@ -31,7 +33,9 @@
$t( $t(
"Error while login with {provider}. This login provider doesn't exist.", "Error while login with {provider}. This login provider doesn't exist.",
{ {
provider: $route.query.provider, provider:
SELECTED_PROVIDERS[$route.query.provider] ||
"unknown provider",
} }
) )
}}</b-message }}</b-message
@ -123,7 +127,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { Route } from "vue-router"; import { Route } from "vue-router";
import { ICurrentUser } from "@/types/current-user.model"; import { ICurrentUser } from "@/types/current-user.model";
import { LoginError, LoginErrorCode } from "@/types/enums"; import { LoginError, LoginErrorCode } from "@/types/enums";
@ -136,6 +140,7 @@ import {
initializeCurrentActor, initializeCurrentActor,
NoIdentitiesException, NoIdentitiesException,
saveUserData, saveUserData,
SELECTED_PROVIDERS,
} from "../../utils/auth"; } from "../../utils/auth";
import { ILogin } from "../../types/login.model"; import { ILogin } from "../../types/login.model";
import { import {
@ -189,6 +194,8 @@ export default class Login extends Vue {
password: "", password: "",
}; };
redirect: string | undefined = "";
errors: string[] = []; errors: string[] = [];
rules = { rules = {
@ -196,6 +203,8 @@ export default class Login extends Vue {
email: validateEmailField, email: validateEmailField,
}; };
SELECTED_PROVIDERS = SELECTED_PROVIDERS;
submitted = false; submitted = false;
mounted(): void { mounted(): void {
@ -204,6 +213,12 @@ export default class Login extends Vue {
const { query } = this.$route; const { query } = this.$route;
this.errorCode = query.code as LoginErrorCode; this.errorCode = query.code as LoginErrorCode;
this.redirect = query.redirect as string | undefined;
// Already-logged-in and accessing /login
if (this.currentUser.isLoggedIn) {
this.$router.push("/");
}
} }
async loginAction(e: Event): Promise<Route | void> { async loginAction(e: Event): Promise<Route | void> {
@ -230,14 +245,14 @@ export default class Login extends Vue {
saveUserData(data.login); saveUserData(data.login);
await this.setupClientUserAndActors(data.login); await this.setupClientUserAndActors(data.login);
if (this.$route.query.redirect) { if (this.redirect) {
this.$router.push(this.$route.query.redirect as string); this.$router.push(this.redirect as string);
return; return;
} }
if (window.localStorage) { if (window.localStorage) {
window.localStorage.setItem("welcome-back", "yes"); window.localStorage.setItem("welcome-back", "yes");
} }
this.$router.push({ name: RouteName.HOME }); this.$router.replace({ name: RouteName.HOME });
return; return;
} catch (err: any) { } catch (err: any) {
this.submitted = false; this.submitted = false;
@ -276,13 +291,6 @@ export default class Login extends Vue {
} }
} }
@Watch("currentUser")
redirectToHomepageIfAlreadyLoggedIn(): Promise<Route> | void {
if (this.currentUser.isLoggedIn) {
return this.$router.push("/");
}
}
get hasCaseWarning(): boolean { get hasCaseWarning(): boolean {
return this.credentials.email !== this.credentials.email.toLowerCase(); return this.credentials.email !== this.credentials.email.toLowerCase();
} }

View File

@ -46,7 +46,7 @@ export default class ProviderValidate extends Vue {
id: userId, id: userId,
email: userEmail, email: userEmail,
isLoggedIn: true, isLoggedIn: true,
role: ICurrentUserRole.USER, role: userRole,
}, },
}); });
const { data } = await this.$apollo.query<{ loggedUser: IUser }>({ const { data } = await this.$apollo.query<{ loggedUser: IUser }>({

View File

@ -16,6 +16,9 @@ module.exports = {
secondary: withOpacityValue("--color-secondary"), secondary: withOpacityValue("--color-secondary"),
"violet-title": withOpacityValue("--color-violet-title"), "violet-title": withOpacityValue("--color-violet-title"),
}, },
lineClamp: {
10: "10",
},
}, },
}, },
plugins: [require("@tailwindcss/line-clamp")], plugins: [require("@tailwindcss/line-clamp")],

View File

@ -13,6 +13,7 @@ export const defaultResolvers = {
id: "67", id: "67",
preferredUsername: "someone", preferredUsername: "someone",
name: "Personne", name: "Personne",
avatar: null,
__typename: "CurrentActor", __typename: "CurrentActor",
}), }),
}, },

View File

@ -67,7 +67,7 @@ exports[`CommentTree renders an empty comment tree 1`] = `
</article> </article>
</form> </form>
<transition-group-stub tag="div" name="comment-empty-list"> <transition-group-stub tag="div" name="comment-empty-list">
<empty-content-stub icon="comment" inline="true"><span>No comments yet</span></empty-content-stub> <empty-content-stub icon="comment" descriptionclasses="" inline="true"><span>No comments yet</span></empty-content-stub>
</transition-group-stub> </transition-group-stub>
</div> </div>
`; `;

View File

@ -3,7 +3,7 @@
exports[`PostListItem renders post list item with basic informations 1`] = ` exports[`PostListItem renders post list item with basic informations 1`] = `
<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto"> <a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto">
<!----> <!---->
<div class="title-info-wrapper has-text-grey-dark"> <div class="title-info-wrapper has-text-grey-dark px-1">
<h3 lang="en" class="post-minimalist-title"> <h3 lang="en" class="post-minimalist-title">
My Blog Post My Blog Post
</h3> </h3>
@ -17,7 +17,7 @@ exports[`PostListItem renders post list item with basic informations 1`] = `
exports[`PostListItem renders post list item with publisher name 1`] = ` exports[`PostListItem renders post list item with publisher name 1`] = `
<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto"> <a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto">
<!----> <!---->
<div class="title-info-wrapper has-text-grey-dark"> <div class="title-info-wrapper has-text-grey-dark px-1">
<h3 lang="en" class="post-minimalist-title"> <h3 lang="en" class="post-minimalist-title">
My Blog Post My Blog Post
</h3> </h3>
@ -31,7 +31,7 @@ exports[`PostListItem renders post list item with publisher name 1`] = `
exports[`PostListItem renders post list item with tags 1`] = ` exports[`PostListItem renders post list item with tags 1`] = `
<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto"> <a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto">
<!----> <!---->
<div class="title-info-wrapper has-text-grey-dark"> <div class="title-info-wrapper has-text-grey-dark px-1">
<h3 lang="en" class="post-minimalist-title"> <h3 lang="en" class="post-minimalist-title">
My Blog Post My Blog Post
</h3> </h3>

View File

@ -22,7 +22,7 @@ import { InMemoryCache } from "@apollo/client/cache";
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Buefy); localVue.use(Buefy);
config.mocks.$t = (key: string): string => key; config.mocks.$t = (key: string): string => key;
const $router = { push: jest.fn() }; const $router = { push: jest.fn(), replace: jest.fn() };
describe("Render login form", () => { describe("Render login form", () => {
let wrapper: Wrapper<Vue>; let wrapper: Wrapper<Vue>;
@ -125,9 +125,9 @@ describe("Render login form", () => {
await flushPromises(); await flushPromises();
expect(currentUser?.email).toBe("some@email.tld"); expect(currentUser?.email).toBe("some@email.tld");
expect(currentUser?.id).toBe("1"); expect(currentUser?.id).toBe("1");
expect(jest.isMockFunction(wrapper.vm.$router.push)).toBe(true); expect(jest.isMockFunction(wrapper.vm.$router.replace)).toBe(true);
await flushPromises(); await flushPromises();
expect($router.push).toHaveBeenCalledWith({ name: RouteName.HOME }); expect($router.replace).toHaveBeenCalledWith({ name: RouteName.HOME });
}); });
it("handles a login error", async () => { it("handles a login error", async () => {

View File

@ -54,7 +54,6 @@ export const configMock = {
__typename: "Features", __typename: "Features",
eventCreation: true, eventCreation: true,
groups: true, groups: true,
koenaConnect: false,
}, },
restrictions: { restrictions: {
__typename: "Restrictions", __typename: "Restrictions",
@ -123,6 +122,8 @@ export const configMock = {
enabled: true, enabled: true,
publicKey: "", publicKey: "",
}, },
eventCategories: [],
analytics: [],
}, },
}, },
}; };

View File

@ -6,6 +6,14 @@ module.exports = {
// remove the prefetch plugin // remove the prefetch plugin
config.plugins.delete("prefetch"); config.plugins.delete("prefetch");
}, },
configureWebpack: (config) => {
const miniCssExtractPlugin = config.plugins.find(
(plugin) => plugin.constructor.name === "MiniCssExtractPlugin"
);
if (miniCssExtractPlugin) {
miniCssExtractPlugin.options.linkType = false;
}
},
pwa: { pwa: {
themeColor: "#ffd599", //not required for service worker, but place theme color here if manifest.json doesn't change the color themeColor: "#ffd599", //not required for service worker, but place theme color here if manifest.json doesn't change the color
workboxPluginMode: "InjectManifest", workboxPluginMode: "InjectManifest",

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Flag do
Enum.each(Users.list_moderators(), fn moderator -> Enum.each(Users.list_moderators(), fn moderator ->
moderator moderator
|> Admin.report(report) |> Admin.report(report)
|> Mailer.send_email_later() |> Mailer.send_email()
end) end)
{:ok, activity, report} {:ok, activity, report}

View File

@ -28,7 +28,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Update do
Logger.debug("updating an activity") Logger.debug("updating an activity")
Logger.debug(inspect(args)) Logger.debug(inspect(args))
case Managable.update(old_entity, args, additional) do case Managable.update(old_entity, args, Map.put(additional, :local, local)) do
{:ok, entity, update_data} -> {:ok, entity, update_data} ->
{:ok, activity} = create_activity(update_data, local) {:ok, activity} = create_activity(update_data, local)
maybe_federate(activity) maybe_federate(activity)

View File

@ -228,13 +228,13 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|> Enum.uniq() |> Enum.uniq()
end end
defp add_event_contacts(%Event{contacts: contacts}) do defp add_event_contacts(%Event{contacts: contacts}) when is_list(contacts) do
contacts contacts
|> Enum.map(& &1.url) |> Enum.map(& &1.url)
|> Enum.uniq() |> Enum.uniq()
end end
defp add_event_contacts(%Event{}), do: [] defp add_event_contacts(_), do: []
defp process_mention({_, mentioned_actor}), do: mentioned_actor.url defp process_mention({_, mentioned_actor}), do: mentioned_actor.url

View File

@ -148,7 +148,7 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
{:error, :http_error} {:error, :http_error}
{:error, error} -> {:error, error} ->
Logger.warn("Could not fetch actor at fetch #{url}, #{inspect(error)}") Logger.info("Could not fetch actor at #{url}, #{inspect(error)}")
{:error, :http_error} {:error, :http_error}
end end
end end

View File

@ -8,7 +8,9 @@ defmodule Mobilizon.Federation.ActivityPub.Publisher do
alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay, Transmogrifier, Visibility} alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay, Transmogrifier, Visibility}
alias Mobilizon.Federation.HTTPSignatures.Signature alias Mobilizon.Federation.HTTPSignatures.Signature
require Logger require Logger
import Mobilizon.Federation.ActivityPub.Utils, only: [remote_actors: 1]
import Mobilizon.Federation.ActivityPub.Utils,
only: [remote_actors: 1, create_full_domain_string: 1]
@doc """ @doc """
Publish an activity to all appropriated audiences inboxes Publish an activity to all appropriated audiences inboxes
@ -77,7 +79,7 @@ defmodule Mobilizon.Federation.ActivityPub.Publisher do
Tesla.Env.result() Tesla.Env.result()
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
Logger.info("Federating #{id} to #{inbox}") Logger.info("Federating #{id} to #{inbox}")
%URI{host: host, path: path} = URI.parse(inbox) %URI{path: path} = uri = URI.new!(inbox)
digest = Signature.build_digest(json) digest = Signature.build_digest(json)
date = Signature.generate_date_header() date = Signature.generate_date_header()
@ -87,7 +89,7 @@ defmodule Mobilizon.Federation.ActivityPub.Publisher do
signature = signature =
Signature.sign(actor, %{ Signature.sign(actor, %{
"(request-target)": "post #{path}", "(request-target)": "post #{path}",
host: host, host: create_full_domain_string(uri),
"content-length": byte_size(json), "content-length": byte_size(json),
digest: digest, digest: digest,
date: date date: date
@ -108,6 +110,9 @@ defmodule Mobilizon.Federation.ActivityPub.Publisher do
@spec convert_followers_in_recipients(list(String.t())) :: {list(String.t()), list(String.t())} @spec convert_followers_in_recipients(list(String.t())) :: {list(String.t()), list(String.t())}
defp convert_followers_in_recipients(recipients) do defp convert_followers_in_recipients(recipients) do
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, follower_actors} = acc -> Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, follower_actors} = acc ->
if is_nil(recipient) do
acc
else
case Actors.get_actor_by_followers_url(recipient) do case Actors.get_actor_by_followers_url(recipient) do
%Actor{} = group -> %Actor{} = group ->
{Enum.filter(recipients, fn recipient -> recipient != group.followers_url end), {Enum.filter(recipients, fn recipient -> recipient != group.followers_url end),
@ -116,6 +121,7 @@ defmodule Mobilizon.Federation.ActivityPub.Publisher do
nil -> nil ->
acc acc
end end
end
end) end)
end end
@ -126,6 +132,9 @@ defmodule Mobilizon.Federation.ActivityPub.Publisher do
@spec convert_members_in_recipients(list(String.t())) :: {list(String.t()), list(Actor.t())} @spec convert_members_in_recipients(list(String.t())) :: {list(String.t()), list(Actor.t())}
defp convert_members_in_recipients(recipients) do defp convert_members_in_recipients(recipients) do
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, member_actors} = acc -> Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, member_actors} = acc ->
if is_nil(recipient) do
acc
else
case Actors.get_group_by_members_url(recipient) do case Actors.get_group_by_members_url(recipient) do
# If the group is local just add external members # If the group is local just add external members
%Actor{domain: domain} = group when is_nil(domain) -> %Actor{domain: domain} = group when is_nil(domain) ->
@ -140,6 +149,7 @@ defmodule Mobilizon.Federation.ActivityPub.Publisher do
_ -> _ ->
acc acc
end end
end
end) end)
end end
end end

View File

@ -10,6 +10,8 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
alias Mobilizon.Federation.ActivityPub.{Fetcher, Relay, Transmogrifier, Utils} alias Mobilizon.Federation.ActivityPub.{Fetcher, Relay, Transmogrifier, Utils}
require Logger require Logger
@collection_element_task_processing_time 60_000
@doc """ @doc """
Refresh a remote profile Refresh a remote profile
""" """
@ -158,7 +160,7 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
items items
|> Enum.map(fn item -> Task.async(fn -> handling_element(item) end) end) |> Enum.map(fn item -> Task.async(fn -> handling_element(item) end) end)
|> Task.await_many() |> Task.await_many(@collection_element_task_processing_time)
Logger.debug("Finished processing a collection") Logger.debug("Finished processing a collection")
:ok :ok
@ -189,8 +191,8 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
defp process_collection(_, _), do: :error defp process_collection(_, _), do: :error
# If we're handling an activity # If we're handling an activity
@spec handling_element(map()) :: {:ok, any, struct} | :error @spec handling_element(map() | String.t()) ::
@spec handling_element(String.t()) :: {:ok, struct} | {:ok, atom, struct} | {:error, any()} {:ok, any, struct} | {:ok, struct} | {:ok, atom, struct} | {:error, any()} | :error
defp handling_element(%{"type" => activity_type} = data) defp handling_element(%{"type" => activity_type} = data)
when activity_type in ["Create", "Update", "Delete"] do when activity_type in ["Create", "Update", "Delete"] do
object = get_in(data, ["object"]) object = get_in(data, ["object"])

View File

@ -14,9 +14,9 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Transmogrifier} alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Transmogrifier}
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Federation.WebFinger alias Mobilizon.Federation.WebFinger
alias Mobilizon.Service.Workers.Background
alias Mobilizon.GraphQL.API.Follows alias Mobilizon.GraphQL.API.Follows
alias Mobilizon.Service.Workers.Background
import Mobilizon.Federation.ActivityPub.Utils, only: [create_full_domain_string: 1]
require Logger require Logger
@ -172,14 +172,17 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
defp fetch_actor("http://" <> address), do: fetch_actor(address) defp fetch_actor("http://" <> address), do: fetch_actor(address)
defp fetch_actor(address) do defp fetch_actor(address) do
%URI{host: host} = URI.parse("http://" <> address) %URI{host: host} = uri = URI.parse("http://" <> address)
cond do cond do
String.contains?(address, "@") -> String.contains?(address, "@") ->
check_actor(address) check_actor(address)
!is_nil(host) -> !is_nil(host) ->
check_actor("relay@#{host}") uri
|> create_full_domain_string()
|> then(&Kernel.<>("relay@", &1))
|> check_actor()
true -> true ->
{:error, :bad_url} {:error, :bad_url}

View File

@ -41,7 +41,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
params = %{ params = %{
reporter_id: params["reporter"].id, reporter_id: params["reporter"].id,
reported_id: params["reported"].id, reported_id: params["reported"].id,
comments_ids: params["comments"] |> Enum.map(& &1.id), comments_ids:
if(params["comments"], do: params["comments"] |> Enum.map(& &1.id), else: []),
content: params["content"] || "", content: params["content"] || "",
additional: %{ additional: %{
"cc" => [params["reported"].url] "cc" => [params["reported"].url]
@ -91,6 +92,9 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
# Object already exists # Object already exists
{:ok, nil, comment} {:ok, nil, comment}
end end
{:error, err} ->
{:error, err}
end end
end end
@ -403,6 +407,13 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
Actions.Update.update(old_actor, object_data, false, %{updater_actor: author}) do Actions.Update.update(old_actor, object_data, false, %{updater_actor: author}) do
{:ok, activity, new_actor} {:ok, activity, new_actor}
else else
{:error, :update_not_allowed} ->
Logger.warn("Activity tried to update an actor that's local or not a group",
activity: params
)
:error
e -> e ->
Sentry.capture_message("Error while handling an Update activity", Sentry.capture_message("Error while handling an Update activity",
extra: %{params: params} extra: %{params: params}
@ -611,11 +622,16 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:error, :unknown_actor} {:error, :unknown_actor}
{:ok, %Actor{} = actor} -> {:ok, %Actor{} = actor} ->
# If the actor itself is being deleted, no need to check anything other than the object being remote
if remote_actor_is_being_deleted(data) do
Actions.Delete.delete(actor, actor, false)
else
case is_group_object_gone(object_id) do case is_group_object_gone(object_id) do
{:ok, object} -> # The group object is no longer there, we can remove the element
{:ok, entity} ->
if Utils.origin_check_from_id?(actor_url, object_id) || if Utils.origin_check_from_id?(actor_url, object_id) ||
Permission.can_delete_group_object?(actor, object) do Permission.can_delete_group_object?(actor, entity) do
Actions.Delete.delete(object, actor, false) Actions.Delete.delete(entity, actor, false)
else else
Logger.warn("Object origin check failed") Logger.warn("Object origin check failed")
:error :error
@ -627,6 +643,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end end
end end
end end
end
def handle_incoming( def handle_incoming(
%{"type" => "Move", "object" => %{"type" => type} = object, "actor" => _actor} = data %{"type" => "Move", "object" => %{"type" => type} = object, "actor" => _actor} = data
@ -850,8 +867,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
# Handle incoming `Accept` activities wrapping a `Join` activity on an event # Handle incoming `Accept` activities wrapping a `Join` activity on an event
defp do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do defp do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do
case get_participant(join_object, actor_accepting) do case get_participant(join_object, actor_accepting) do
{:ok, participant} -> {:ok, activity, participant} ->
do_handle_incoming_accept_join_event(participant, actor_accepting) do_handle_incoming_accept_join_event(participant, actor_accepting, activity)
{:error, _err} -> {:error, _err} ->
case get_member(join_object) do case get_member(join_object) do
@ -870,17 +887,22 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end end
end end
defp do_handle_incoming_accept_join_event(%Participant{role: :participant}, _actor) do defp do_handle_incoming_accept_join_event(
%Participant{role: :participant} = participant,
_actor,
activity
) do
Logger.debug( Logger.debug(
"Tried to handle an Accept activity on a Join activity with a event object but the participant is already validated" "Tried to handle an Accept activity on a Join activity with a event object but the participant is already validated"
) )
nil {:ok, activity, participant}
end end
defp do_handle_incoming_accept_join_event( defp do_handle_incoming_accept_join_event(
%Participant{role: role, event: event} = participant, %Participant{role: role, event: event} = participant,
%Actor{} = actor_accepting %Actor{} = actor_accepting,
_activity
) )
when role in [:not_approved, :rejected] do when role in [:not_approved, :rejected] do
with %Event{} = event <- Events.get_event_with_preload!(event.id), with %Event{} = event <- Events.get_event_with_preload!(event.id),
@ -932,7 +954,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
# Handle incoming `Reject` activities wrapping a `Join` activity on an event # Handle incoming `Reject` activities wrapping a `Join` activity on an event
defp do_handle_incoming_reject_join(join_object, %Actor{} = actor_accepting) do defp do_handle_incoming_reject_join(join_object, %Actor{} = actor_accepting) do
with {:join_event, {:ok, %Participant{event: event, role: role} = participant}} with {:join_event, {:ok, _activity, %Participant{event: event, role: role} = participant}}
when role != :rejected <- when role != :rejected <-
{:join_event, get_participant(join_object, actor_accepting)}, {:join_event, get_participant(join_object, actor_accepting)},
{:event, %Event{} = event} <- {:event, Events.get_event_with_preload!(event.id)}, {:event, %Event{} = event} <- {:event, Events.get_event_with_preload!(event.id)},
@ -943,7 +965,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
:ok <- Participation.send_emails_to_local_user(participant) do :ok <- Participation.send_emails_to_local_user(participant) do
{:ok, activity, participant} {:ok, activity, participant}
else else
{:join_event, {:ok, %Participant{role: :rejected}}} -> {:join_event, {:ok, _activity, %Participant{role: :rejected}}} ->
Logger.warn( Logger.warn(
"Tried to handle an Reject activity on a Join activity with a event object but the participant is already rejected" "Tried to handle an Reject activity on a Join activity with a event object but the participant is already rejected"
) )
@ -1040,18 +1062,18 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end end
end end
defp get_participant(join_object, %Actor{} = actor_accepting, loop \\ 1) do defp get_participant(join_object, %Actor{} = actor_accepting, loop \\ 1, activity \\ nil) do
with join_object_id when not is_nil(join_object_id) <- Utils.get_url(join_object), with join_object_id when not is_nil(join_object_id) <- Utils.get_url(join_object),
{:not_found, %Participant{} = participant} <- {:not_found, %Participant{} = participant} <-
{:not_found, Events.get_participant_by_url(join_object_id)} do {:not_found, Events.get_participant_by_url(join_object_id)} do
{:ok, participant} {:ok, activity, participant}
else else
{:not_found, _err} -> {:not_found, _err} ->
with true <- is_map(join_object), with true <- is_map(join_object),
true <- loop < 2, true <- loop < 2,
true <- Utils.are_same_origin?(actor_accepting.url, join_object["id"]), true <- Utils.are_same_origin?(actor_accepting.url, join_object["id"]),
{:ok, _activity, %Participant{url: participant_url}} <- handle_incoming(join_object) do {:ok, activity, %Participant{url: participant_url}} <- handle_incoming(join_object) do
get_participant(participant_url, actor_accepting, 2) get_participant(participant_url, actor_accepting, 2, activity)
else else
_ -> _ ->
{:error, "Participant URL not found"} {:error, "Participant URL not found"}
@ -1199,4 +1221,9 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
moderator.domain == group.domain moderator.domain == group.domain
end end
end end
defp remote_actor_is_being_deleted(%{"object" => object} = data) do
object_id = Utils.get_url(object)
Utils.get_actor(data) == object_id and not Utils.are_same_origin?(object_id, Endpoint.url())
end
end end

View File

@ -1,7 +1,7 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Actors do defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
@moduledoc false @moduledoc false
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower, Member, MemberRole} alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission, Relay} alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission, Relay}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream
@ -45,6 +45,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
def update(%Actor{} = old_actor, args, additional) do def update(%Actor{} = old_actor, args, additional) do
updater_actor = Map.get(args, :updater_actor) || Map.get(additional, :updater_actor) updater_actor = Map.get(args, :updater_actor) || Map.get(additional, :updater_actor)
if Map.get(additional, :local, false) == true or not match?(%Actor{domain: nil}, old_actor) or
match?(%Actor{type: :Group}, old_actor) do
case Actors.update_actor(old_actor, args) do case Actors.update_actor(old_actor, args) do
{:ok, %Actor{} = new_actor} -> {:ok, %Actor{} = new_actor} ->
GroupActivity.insert_activity(new_actor, GroupActivity.insert_activity(new_actor,
@ -65,6 +67,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
{:error, %Ecto.Changeset{} = err} -> {:error, %Ecto.Changeset{} = err} ->
{:error, err} {:error, err}
end end
else
{:error, :update_not_allowed}
end
end end
@public_ap "https://www.w3.org/ns/activitystreams#Public" @public_ap "https://www.w3.org/ns/activitystreams#Public"
@ -244,7 +249,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
Actor.t(), Actor.t(),
ActivityStreams.t(), ActivityStreams.t(),
Member.t(), Member.t(),
MemberRole.t() atom()
) :: ) ::
{:ok, ActivityStreams.t(), Member.t()} {:ok, ActivityStreams.t(), Member.t()}
defp approve_if_default_role_is_member( defp approve_if_default_role_is_member(

View File

@ -3,7 +3,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events, as: EventsManager alias Mobilizon.Events, as: EventsManager
alias Mobilizon.Events.{Event, Participant, ParticipantRole} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission} alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream
@ -191,7 +191,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
Event.t(), Event.t(),
ActivityStreams.t(), ActivityStreams.t(),
Participant.t(), Participant.t(),
ParticipantRole.t() atom()
) :: {:ok, ActivityStreams.t(), Participant.t()} | {:accept, any()} ) :: {:ok, ActivityStreams.t(), Participant.t()} | {:accept, any()}
defp approve_if_default_role_is_participant(event, activity_data, participant, role) do defp approve_if_default_role_is_participant(event, activity_data, participant, role) do
case event do case event do
@ -217,7 +217,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
end end
end end
@spec do_approve(Event.t(), ActivityStreams.t(), Particpant.t(), ParticipantRole.t(), map()) :: @spec do_approve(Event.t(), ActivityStreams.t(), Particpant.t(), atom(), map()) ::
{:accept, any} | {:ok, ActivityStreams.t(), Participant.t()} {:accept, any} | {:ok, ActivityStreams.t(), Participant.t()}
defp do_approve(event, activity_data, participant, role, additionnal) do defp do_approve(event, activity_data, participant, role, additionnal) do
cond do cond do
@ -267,12 +267,22 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
Map.merge(args, %{ Map.merge(args, %{
description: description, description: description,
mentions: mentions, mentions: mentions,
tags: tags # Exclude tags with length > 40
tags: Enum.filter(tags, &exclude_too_long_tags/1)
}) })
else else
args args
end end
# Make sure we don't have duplicate (with different casing) tags
args =
Map.update(
args,
:tags,
[],
&Enum.uniq_by(&1, fn tag -> tag |> tag_to_string() |> String.downcase() end)
)
# Check that we can only allow anonymous participation if our instance allows it # Check that we can only allow anonymous participation if our instance allows it
{_, options} = {_, options} =
Map.get_and_update( Map.get_and_update(
@ -292,4 +302,16 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|> Map.update(:tags, [], &ConverterUtils.fetch_tags/1) |> Map.update(:tags, [], &ConverterUtils.fetch_tags/1)
|> Map.update(:contacts, [], &ConverterUtils.fetch_actors/1) |> Map.update(:contacts, [], &ConverterUtils.fetch_actors/1)
end end
@spec exclude_too_long_tags(%{title: String.t()} | String.t()) :: boolean()
defp exclude_too_long_tags(tag) do
tag
|> tag_to_string()
|> String.length()
|> Kernel.<(40)
end
@spec tag_to_string(%{title: String.t()} | String.t()) :: String.t()
defp tag_to_string(%{title: tag}), do: tag
defp tag_to_string(tag), do: tag
end end

View File

@ -1,7 +1,7 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Members do defmodule Mobilizon.Federation.ActivityPub.Types.Members do
@moduledoc false @moduledoc false
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member, MemberRole} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Federation.ActivityPub.Actions alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
@ -89,8 +89,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
@spec check_admins_left?( @spec check_admins_left?(
String.t() | integer, String.t() | integer,
String.t() | integer, String.t() | integer,
MemberRole.t(), atom(),
MemberRole.t() atom()
) :: boolean ) :: boolean
defp check_admins_left?(member_id, group_id, current_role, updated_role) do defp check_admins_left?(member_id, group_id, current_role, updated_role) do
Actors.is_only_administrator?(member_id, group_id) && current_role == :administrator && Actors.is_only_administrator?(member_id, group_id) && current_role == :administrator &&

View File

@ -337,8 +337,9 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
message: "Object contains an actor object with invalid type: #{inspect(type)}" message: "Object contains an actor object with invalid type: #{inspect(type)}"
end end
def get_actor(%{"actor" => nil, "attributedTo" => nil}) do def get_actor(%{"actor" => nil, "attributedTo" => nil} = object) do
raise ArgumentError, message: "Object contains both actor and attributedTo fields being null" raise ArgumentError,
message: "Object contains both actor and attributedTo fields being null: #{inspect(object)}"
end end
def get_actor(%{"actor" => _}) do def get_actor(%{"actor" => _}) do
@ -671,8 +672,15 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """ @doc """
Converts PEM encoded keys to a public key representation Converts PEM encoded keys to a public key representation
""" """
@spec pem_to_public_key_pem(String.t()) :: String.t()
def pem_to_public_key_pem(pem) do
public_key = pem_to_public_key(pem)
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
:public_key.pem_encode([public_key])
end
@spec pem_to_public_key(String.t()) :: {:RSAPublicKey, any(), any()} @spec pem_to_public_key(String.t()) :: {:RSAPublicKey, any(), any()}
def pem_to_public_key(pem) do defp pem_to_public_key(pem) do
[key_code] = :public_key.pem_decode(pem) [key_code] = :public_key.pem_decode(pem)
key = :public_key.pem_entry_decode(key_code) key = :public_key.pem_entry_decode(key_code)
@ -685,14 +693,8 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
end end
end end
@spec pem_to_public_key_pem(String.t()) :: String.t() @spec make_signature(Actor.t(), String.t(), DateTime.t()) :: list({atom(), String.t()})
def pem_to_public_key_pem(pem) do defp make_signature(actor, id, date) do
public_key = pem_to_public_key(pem)
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
:public_key.pem_encode([public_key])
end
def make_signature(actor, id, date) do
uri = URI.parse(id) uri = URI.parse(id)
signature = signature =
@ -779,4 +781,16 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
params params
end end
end end
@schemes_with_no_port ["http", "https"]
def create_full_domain_string(%URI{host: host, port: nil}), do: host
def create_full_domain_string(%URI{host: host, port: port}) do
if port in Enum.map(@schemes_with_no_port, &URI.default_port/1) do
host
else
"#{host}:#{port}"
end
end
end end

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