- Improved list discussion items UI on the group panel

- Fixed 'unsafe-inline' being in CSP
 - Fixed group discussions with deleted comments
 -----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCAAdFiEExMITpfxOHHCvHn8FoGG53eDKB3MFAmDfELkACgkQoGG53eDK
 B3N88BAAjJrjwqoR4L25n0waHxITI7fIn4cJokZlMs5n+kb4KhngakzU9bJ8p++H
 jZb5YCC6zLe4KKRwMvPeLpH1MD6NGya9xYtZr2ub40PxaNAIFiV/9/GJ/PS91ipS
 RaWGA67TtJN9oMWeOWnozRR5+gs831/t5wmxJxJmiMh5QXL9kGV1FQf/WIE9iU4u
 dvajcHAJBTyKJjXmoUtdifsJVc2QLH513l6/EX+9NggaJTRiSgRtJGLpOqZ9i5ax
 Bq9OmWhAGetwK/RGzbrmEn+mJ2XmtQ9Dk+LJ6dIaRhEZt1vPXia5AnfiOvovgVhi
 +RqRta2wT3YWSOIfRr7WnwNVsf9ygufSTa7idVZEx/4tiBSgE0R4+9nQLvhd9q1v
 +DQo+V9PIKUn0RIVVR2fa6I/W7viy6TuC2D1Faegf4GubeQNDqC6UuQwZkik0B2z
 Js9Lwpll/HHQk5fjBBzdH8ri6pLAtmSAmADTBqXmYhHhPxlYl7Mb68DlTp9Y7KBB
 GoG8JpIRMH1Z601VrDN2gu49BLd+v5gJrCxqClMwfT4iK09sudJS8Fl5CV3o7Xv1
 4XyYc+1k2vPjW9RXg6a+sbNndXESWF4fDCFM+5qwbLvasanW4uijDSDgD8er/pKd
 2IU3nneRcaZg1mwPwrIOciiafrxxNSOFUFOiCkJjb5EDY4cjAlQ=
 =AdSP
 -----END PGP SIGNATURE-----

Merge tag '1.2.3' into chapril

- Improved list discussion items UI on the group panel

- Fixed 'unsafe-inline' being in CSP
- Fixed group discussions with deleted comments
This commit is contained in:
Tykayn 2021-07-16 22:45:10 +02:00 committed by tykayn
commit 973ae96f1a
475 changed files with 26886 additions and 12437 deletions

1
.gitignore vendored
View File

@ -26,6 +26,7 @@ priv/data/*
!priv/data/.gitkeep
priv/errors/*
!priv/errors/.gitkeep
priv/cert/
.vscode/
cover/
site/

View File

@ -16,7 +16,7 @@ variables:
# DB Variables for Postgres / Postgis
POSTGRES_DB: mobilizon_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ""
POSTGRES_PASSWORD: postgres
POSTGRES_HOST: postgres
# DB Variables for Mobilizon
MOBILIZON_DATABASE_USERNAME: $POSTGRES_USER
@ -61,7 +61,7 @@ lint-elixir:
- exit $EXITVALUE
lint-front:
image: node:14
image: node:16
stage: check
before_script:
- export EXITVALUE=0
@ -73,7 +73,7 @@ lint-front:
build-frontend:
stage: build-js
image: node:14
image: node:16
before_script:
- apt update
- apt install -y --no-install-recommends python build-essential webp imagemagick gifsicle jpegoptim optipng pngquant
@ -100,10 +100,27 @@ deps:
needs:
- install
exunit-1.11:
stage: test
image: tcitworld/mobilizon-ci:legacy
services:
- name: postgis/postgis:11-3.0
alias: postgres
variables:
MIX_ENV: test
before_script:
- mix deps.clean --all
- mix deps.get
- mix ecto.create
- mix ecto.migrate
script:
- mix coveralls
allow_failure: true
exunit:
stage: test
services:
- name: mdillon/postgis:11
- name: postgis/postgis:13-3.1
alias: postgres
variables:
MIX_ENV: test
@ -140,7 +157,7 @@ jest:
# cypress:
# stage: test
# services:
# - name: mdillon/postgis:11
# - name: postgis/postgis:13.3
# alias: postgres
# variables:
# MIX_ENV=e2e
@ -197,7 +214,7 @@ build-docker-master:
build-docker-tag:
<<: *docker
rules:
rules: &tag-rules
- if: '$CI_PROJECT_NAMESPACE != "framasoft"'
when: never
- if: $CI_COMMIT_TAG
@ -235,34 +252,38 @@ package-app-dev:
release-upload:
stage: upload
image: curlimages/curl:latest
rules:
- if: $CI_COMMIT_TAG
script: |
APP_VERSION="${CI_COMMIT_TAG}"
APP_ASSET="${CI_PROJECT_NAME}_${APP_VERSION}_${ARCH}.tar.gz"
image: framasoft/yakforms-assets-deploy:latest
rules: *tag-rules
script:
- APP_VERSION="${CI_COMMIT_TAG}"
- APP_ASSET="${CI_PROJECT_NAME}_${APP_VERSION}_${ARCH}.tar.gz"
echo "Artifact: ${APP_ASSET}"
tar czf ${APP_ASSET} -C release mobilizon
ls -al ${APP_ASSET}
- 'echo "Artifact: ${APP_ASSET}"'
- tar czf ${APP_ASSET} -C release mobilizon
- ls -al ${APP_ASSET}
curl --silent --show-error --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file "${APP_ASSET}" ${PACKAGE_REGISTRY_URL}/${APP_VERSION}/${APP_ASSET}
- eval `ssh-agent -s`
- ssh-add <(echo "${DEPLOYEMENT_KEY}" | base64 --decode -i)
- echo "put -r ${APP_ASSET}" | sftp -o "VerifyHostKeyDNS yes" ${DEPLOYEMENT_USER}@${DEPLOYEMENT_HOST}:public/
artifacts:
expire_in: 1 day
when: on_success
paths:
- mobilizon_*.tar.gz
# release-create:
# stage: deploy
# image: registry.gitlab.com/gitlab-org/release-cli:latest
# rules:
# - if: $CI_COMMIT_TAG
# dependencies: []
# cache: {}
# script: |
# APP_VERSION="${CI_COMMIT_TAG}"
# APP_ASSET="${CI_PROJECT_NAME}_${APP_VERSION}_${ARCH}.tar.gz"
# release-cli create --name "$CI_PROJECT_TITLE v$CI_COMMIT_TAG" \
# --tag-name "$CI_COMMIT_TAG" \
# --assets-link "{\"name\":\"${APP_ASSET}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${APP_VERSION}/${APP_ASSET}\"}"
release-create:
stage: deploy
image: registry.gitlab.com/gitlab-org/release-cli:latest
rules: *tag-rules
before_script:
- apk --no-cache add gawk sed grep
script: |
APP_VERSION="${CI_COMMIT_TAG}"
APP_ASSET="${CI_PROJECT_NAME}_${APP_VERSION}_${ARCH}.tar.gz"
CHANGELOG=$(awk -v version="$APP_VERSION" '/^## / { printit = $2 == version }; printit' CHANGELOG.md | grep -v "## $APP_VERSION" | sed '1{/^$/d}')
ENDPOINT="https://packages.joinmobilizon.org"
release-cli create --name "$CI_COMMIT_TAG" \
--description "$CHANGELOG" \
--tag-name "$CI_COMMIT_TAG" \
--assets-link "{\"name\":\"${APP_ASSET}\",\"url\":\"${ENDPOINT}/${APP_ASSET}\"}"

View File

@ -3,4 +3,8 @@
752C0E897CA81ACD81F4BB215FA5F8E4
23412CF16549E4E88366DC9DECF39071
81C1F600C5809C7029EE32DE4818CD7D
81C1F600C5809C7029EE32DE4818CD7D
155A1FB53DE39EC8EFCFD7FB94EA823D
73B351E4CB3AF715AD450A085F5E6304
BBACD7F0BACD4A6D3010C26604671692
6D4D4A4821B93BCFAC9CDBB367B34C4B

View File

@ -5,7 +5,179 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.1.4 - 19-05-2021
## 1.2.3 - 2021-07-02
### Changed
- Improved list discussion items UI on the group panel
### Fixed
- Fixed 'unsafe-inline' being in CSP
- Fixed group discussions with deleted comments
## 1.2.2 - 2021-07-01
### Changed
- Improved UI for participations when message is too long
### Fixed
- Fixed pictures without metadata information in post display
- Fixed crash when trying to notify activities not from groups
- Fixed imagemagick missing from Dockerfile
- Fixed push notifications for group, members & post activities
- Fixed ellipsis in DiscussionListView
- Fixed submission button for posts not visible on mobile
- Fixed remote profile suspension
### Translations
- Spanish
## 1.2.1 - 2021-06-29
### Fixed
- Fixed Docker image missing libc (which is required by newer OTP versions at runtime)
- Fixed compatibility check in Notification section for service workers
## 1.2.0 - 2021-06-29
### Added
- **Notifications for various group and event activity, both by email and browser push notifications. Daily and weekly digests are also available.**
- Possibility for an event organizer to announce a (public) comment, triggering notifications for participants
- Add a snackbar message to manually reload the UI when updates are available
- Add blurhash support for some banners
- Added basic metadata (start time & physical address) in the opengraph preview
### Changed
- **Interface improvements to events, comments, homepage and group pages**
- **Various improvements to mobile views**
- Make JWT access tokens short-lived
- Disabled Cldr warning that the `Cldr.Plug.AcceptLanguage` plug didn't many any known locale
- Replaced GraphiQL web interface with graphql-playground
### Removed
- Internet Explorer and other older browsers support. This allows us to provide lighter builds.
### Fixed
- Fixed compatibility for previous OTP versions
- Fixed the "member joined" activity event not being displayed in the group activity timeline
- Fixed relay and anonymous actor telling they automatically approve followers
- Fixed mix tasks showing output from all error levels
- Fixed missing metadata on some pages
- Fixed some config values being defined at compile-time instead of runtime
- Fixed missing pagination for group resources
- Fixed missing `.ics` suffix for email event attachments
- Fixed missing unique index on posts URL
- Fixed creating events from group page not always auto-selecting the correct organizer actor
- Fixed error when deleting actor with type different from Person or Group
- Fixed not defaulting to UTC timezone when user has no tz setting in their activity recaps
- Fixed Sentry loading itself even if not configured
- Fixed showing proper message when anonymous participation was confirmed but just wasn't saved in browser
- Fixed editing some event properties
- Fixed group image ratio in admin dashboard
- Fix GraphiQL CSP headers
### Translations
- Finnish
- French
- Galician
- Italian
- Occitan
- Russian
- Spanish
- Swedish
## 1.2.0-beta.3 - 2021-06-27
### Added
- Allow sending notifications to event organizer when new comment is posted
- Allow sending comment announcements notifications to anonymous participants as well
### Changed
- Disabled Cldr warning that the `Cldr.Plug.AcceptLanguage` plug didn't many any known locale
### Fixed
- Fixed error when deleting actor with type different from Person or Group
- Fixed not defaulting to UTC timezone when user has no tz setting in their activity recaps
- Fixed Sentry loading itself even if not configured
- Fixed showing proper message when anonymous participation was confirmed but just wasn't saved in browser
- Fixed editing some event properties
### Translations
- Persian (New!)
- Spanish
## 1.2.0-beta.2 - 2021-06-26
### Added
- Added basic metadata (start time & physical address) in the opengraph preview
- Made mentions trigger notifications
- Allow to send activity digests
- Mix task to generate web push keypair
### Fixed
- Fixed missing unique index on posts URL
- Fixed creating events from group page not always auto-selecting the correct organizer actor
### Translations
- French
- Spanish
## 1.2.0-beta.1 - 2021-06-21
### Added
- **Notifications for various group and event activity, both by email and browser push notifications**
- Possibility for an event organizer to announce a (public) comment, triggering notifications for participants
- Add a snackbar message to manually reload the UI when updates are available
- Add blurhash support for some banners
### Changed
- **Interface improvements to events, comments, homepage and group pages**
- **Various improvements to mobile views**
- Make JWT access tokens short-lived
### Removed
- Internet Explorer and other older browsers support. This allows us to provide lighter builds.
### Fixed
- Fixed compatibility for previous OTP versions
- Fixed the "member joined" activity event not being displayed in the group activity timeline
- Fixed relay and anonymous actor telling they automatically approve followers
- Fixed mix tasks showing output from all error levels
- Fixed missing metadata on some pages
- Fixed some config values being defined at compile-time instead of runtime
- Fixed missing pagination for group resources
- Fixed missing `.ics` suffix for email event attachments
### Translations
- Finnish
- Galician
- Italian
- Occitan
- Russian
- Spanish
- Swedish
## 1.1.4 - 2021-05-19
### Fixes
@ -21,7 +193,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Galician
- Italian
## 1.1.3 - 03-05-2021
## 1.1.3 - 2021-05-03
### Changed
@ -40,7 +212,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Russian
- Spanish
## 1.1.2 - 28-04-2021
## 1.1.2 - 2021-04-28
### Changed
@ -65,7 +237,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Slovenian
- Russian
## 1.1.1 - 22-04-2021
## 1.1.1 - 2021-04-22
### Changed
@ -97,7 +269,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed editing an user's email in CLI
- Fixed suspended actors being refreshed
### Translations
- Gaelic
@ -108,7 +279,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Slovenian
- Spanish
## 1.1.0 - 31-03-2021
## 1.1.0 - 2021-03-31
This version introduces a new way to install and host Mobilizon : Elixir releases. This is the new default way of installing Mobilizon. Please read [UPGRADE.md](./UPGRADE.md#upgrading-from-10-to-11) for details on how to migrate to Elixir binary releases or stay on source install.
@ -204,7 +375,7 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
- Slovenian
- Spanish
## 1.1.0-rc.3 - 30-03-2021
## 1.1.0-rc.3 - 2021-03-30
### Changed
@ -215,7 +386,7 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
- Fixed parsing the IP from the MOBILIZON_INSTANCE_LISTEN_IP env variable for Docker
- Fixed release startup in Docker container
## 1.1.0-rc.2 - 30-03-2021
## 1.1.0-rc.2 - 2021-03-30
### Added
@ -239,7 +410,7 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
- Hungarian
- Russian
- Spanish
## 1.1.0-rc.1 - 29-03-2021
## 1.1.0-rc.1 - 2021-03-29
### Added
@ -283,17 +454,17 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
- Slovenian
- Spanish
## 1.1.0-beta.6 - 17-03-2021
## 1.1.0-beta.6 - 2021-03-17
### Fixed
- Fixed a typo in range/radius showing the wrong radius for close events on homepage
## 1.1.0-beta.5 - 17-03-2021
## 1.1.0-beta.5 - 2021-03-17
### Fixed
- Fixed a typo in range/radius preventing close events from showing up
## 1.1.0-beta.4 - 17-03-2021
## 1.1.0-beta.4 - 2021-03-17
### Fixed
@ -301,13 +472,13 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
- Fixed location field not showing in preferences if setting not already set
- Fixed lasts events published order on the homepage
## 1.1.0-beta.3 - 16-03-2021
## 1.1.0-beta.3 - 2021-03-16
### Fixed
- Handle ActivityPub Fetcher returning text that's not JSON
- Fix accessing a group profile when not a member
## 1.1.0-beta.2 - 16-03-2021
## 1.1.0-beta.2 - 2021-03-16
### Fixed
- Fixed geospatial configuration only being evaluated at compile-time, not at runtime
@ -315,7 +486,7 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
### Translations
- Slovenian
## 1.1.0-beta.1 - 10-03-2021
## 1.1.0-beta.1 - 2021-03-10
This version introduces a new way to install and host Mobilizon : Elixir releases. This is the new default way of installing Mobilizon. Please read [UPGRADE.md](./UPGRADE.md#upgrading-from-10-to-11) for details on how to migrate to Elixir binary releases or stay on source install.
@ -371,7 +542,7 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
- Spanish
- Russian
## 1.0.7 - 27-02-2021
## 1.0.7 - 2021-02-27
### Fixed
@ -381,7 +552,7 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
- Fixed search form display
- Fixed wrong year in CHANGELOG.md
## 1.0.6 - 04-02-2021
## 1.0.6 - 2021-02-04
### Added
@ -393,13 +564,13 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
- Fixed sending events & posts to group followers
- Fixed redirection after deleting an event
## 1.0.5 - 27-01-2021
## 1.0.5 - 2021-01-27
### Fixed
- Fixed duplicate entries in search with empty search query
## 1.0.4 - 26-01-2021
## 1.0.4 - 2021-02-26
### Added
@ -446,7 +617,7 @@ This version introduces a new way to install and host Mobilizon : Elixir releas
- Spanish
- Swedish
## 1.0.3 - 18-12-2020
## 1.0.3 - 2020-12-18
**This release adds new migrations, be sure to run them before restarting Mobilizon**

View File

@ -4,9 +4,9 @@ module.exports = {
service: {
name: "Mobilizon",
// URL to the GraphQL API
url: "http://localhost:4000/api",
localSchemaFile: "./schema.graphql",
},
// Files processed by the extension
includes: ["src/**/*.vue", "src/**/*.js"],
includes: ["js/src/**/*.vue", "js/src/**/*.js"],
},
};

View File

@ -44,9 +44,6 @@ config :mobilizon, :events, creation: true
# Configures the endpoint
config :mobilizon, Mobilizon.Web.Endpoint,
http: [
transport_options: [socket_opts: [:inet6]]
],
url: [
host: "mobilizon.local",
scheme: "https"
@ -69,6 +66,7 @@ config :mobilizon, Mobilizon.Web.Upload,
uploader: Mobilizon.Web.Upload.Uploader.Local,
filters: [
Mobilizon.Web.Upload.Filter.Dedupe,
Mobilizon.Web.Upload.Filter.AnalyzeMetadata,
Mobilizon.Web.Upload.Filter.Optimize
],
allow_list_mime_types: ["image/gif", "image/jpeg", "image/png", "image/webp"],
@ -115,7 +113,7 @@ config :mobilizon, Mobilizon.Web.Email.Mailer,
# Configures Elixir's Logger
config :logger, :console,
backends: [:console, Sentry.LoggerBackend],
backends: [:console],
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
@ -123,14 +121,19 @@ config :logger, Sentry.LoggerBackend,
level: :warn,
capture_log_messages: true
config :mobilizon, Mobilizon.Web.Auth.Guardian, issuer: "mobilizon"
config :mobilizon, Mobilizon.Web.Auth.Guardian,
issuer: "mobilizon",
token_ttl: %{
"access" => {15, :minutes},
"refresh" => {60, :days}
}
config :guardian, Guardian.DB,
repo: Mobilizon.Storage.Repo,
# default
schema_name: "guardian_tokens",
# store all token types if not set
# token_types: ["refresh_token"],
token_types: ["refresh"],
# default: 60 minutes
sweep_interval: 60
@ -170,6 +173,9 @@ config :phoenix, :format_encoders, json: Jason, "activity-json": Jason
config :phoenix, :json_library, Jason
config :phoenix, :filter_parameters, ["password", "token"]
config :absinthe, schema: Mobilizon.GraphQL.Schema
config :absinthe, Absinthe.Logger, filter_variables: ["token", "password", "secret"]
config :ex_cldr,
default_locale: "en",
default_backend: Mobilizon.Cldr
@ -265,15 +271,15 @@ config :mobilizon, :anonymous,
config :mobilizon, Oban,
repo: Mobilizon.Storage.Repo,
log: false,
queues: [default: 10, search: 5, mailers: 10, background: 5, activity: 5],
queues: [default: 10, search: 5, mailers: 10, background: 5, activity: 5, notifications: 5],
plugins: [
{Oban.Plugins.Cron,
crontab: [
{"@hourly", Mobilizon.Service.Workers.BuildSiteMap, queue: :background},
{"17 4 * * *", Mobilizon.Service.Workers.RefreshGroups, queue: :background},
# To be activated in Mobilizon 1.2
# {"@hourly", Mobilizon.Service.Workers.CleanOrphanMediaWorker, queue: :background},
{"@hourly", Mobilizon.Service.Workers.CleanOrphanMediaWorker, queue: :background},
{"@hourly", Mobilizon.Service.Workers.CleanUnconfirmedUsersWorker, queue: :background},
{"@hourly", Mobilizon.Service.Workers.SendActivityRecapWorker, queue: :notifications},
{"@daily", Mobilizon.Service.Workers.CleanOldActivityWorker, queue: :background}
]},
{Oban.Plugins.Pruner, max_age: 300}
@ -298,6 +304,16 @@ config :mobilizon, :external_resource_providers, %{
"https://docs.google.com/spreadsheets/" => :google_spreadsheets
}
config :mobilizon, Mobilizon.Service.Notifier,
notifiers: [
Mobilizon.Service.Notifier.Email,
Mobilizon.Service.Notifier.Push
]
config :mobilizon, Mobilizon.Service.Notifier.Email, enabled: true
config :mobilizon, Mobilizon.Service.Notifier.Push, enabled: true
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"

View File

@ -24,7 +24,8 @@ config :mobilizon, Mobilizon.Web.Endpoint,
"node_modules/webpack/bin/webpack.js",
"--mode",
"development",
"--watch-stdin",
"--watch",
"--watch-options-stdin",
"--config",
"node_modules/@vue/cli-service/webpack.config.js",
cd: Path.expand("../js", __DIR__)

View File

@ -43,9 +43,6 @@ cond do
File.exists?("./config/#{System.get_env("INSTANCE_CONFIG")}") ->
import_config System.get_env("INSTANCE_CONFIG")
File.exists?("./config/prod.secret.exs") ->
import_config "prod.secret.exs"
true ->
:ok
end

View File

@ -2,13 +2,13 @@
FROM node:16-alpine as assets
RUN apk add --no-cache python3 build-base libwebp-tools bash imagemagick ncurses
WORKDIR /build
COPY js .
RUN yarn install \
&& yarn run build
# Then, build the application binary
FROM elixir:1.11-alpine AS builder
FROM elixir:1.12-alpine AS builder
RUN apk add --no-cache build-base git cmake
@ -45,7 +45,7 @@ LABEL org.opencontainers.image.title="mobilizon" \
org.opencontainers.image.revision=$VCS_REF \
org.opencontainers.image.created=$BUILD_DATE
RUN apk add --no-cache openssl ncurses-libs file postgresql-client
RUN apk add --no-cache openssl ncurses-libs file postgresql-client libgcc libstdc++ imagemagick
RUN mkdir -p /app/uploads && chown nobody:nobody /app/uploads
RUN mkdir -p /etc/mobilizon && chown nobody:nobody /etc/mobilizon

View File

@ -1,7 +1,7 @@
FROM elixir:latest
LABEL maintainer="Thomas Citharel <tcit@tcit.fr>"
ENV REFRESHED_AT=2021-05-19
ENV REFRESHED_AT=2021-06-07
RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 cmake exiftool
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install nodejs -yq
RUN npm install -g yarn wait-on

View File

@ -0,0 +1,28 @@
# We build Elixir manually to have the oldest acceptable version of OTP
FROM erlang:21
LABEL maintainer="Thomas Citharel <tcit@tcit.fr>"
# elixir expects utf8.
ENV ELIXIR_VERSION="v1.11.4" \
LANG=C.UTF-8
RUN set -xe \
&& ELIXIR_DOWNLOAD_URL="https://github.com/elixir-lang/elixir/archive/${ELIXIR_VERSION}.tar.gz" \
&& ELIXIR_DOWNLOAD_SHA256="85c7118a0db6007507313db5bddf370216d9394ed7911fe80f21e2fbf7f54d29" \
&& curl -fSL -o elixir-src.tar.gz $ELIXIR_DOWNLOAD_URL \
&& echo "$ELIXIR_DOWNLOAD_SHA256 elixir-src.tar.gz" | sha256sum -c - \
&& mkdir -p /usr/local/src/elixir \
&& tar -xzC /usr/local/src/elixir --strip-components=1 -f elixir-src.tar.gz \
&& rm elixir-src.tar.gz \
&& cd /usr/local/src/elixir \
&& make install clean
CMD ["iex"]
ENV REFRESHED_AT=2021-06-07
RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 cmake exiftool
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash && apt-get install nodejs -yq
RUN npm install -g yarn wait-on
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN mix local.hex --force && mix local.rebar --force
RUN curl https://dbip.mirror.framasoft.org/files/dbip-city-lite-latest.mmdb --output GeoLite2-City.mmdb -s && mkdir -p /usr/share/GeoIP && mv GeoLite2-City.mmdb /usr/share/GeoIP/

View File

@ -1,3 +1 @@
> 1%
last 2 versions
not dead
> 0.25% and last 2 versions, not dead, not ie 11, not op_mini all, Firefox ESR

View File

@ -8,8 +8,8 @@ module.exports = {
extends: [
"plugin:vue/essential",
"eslint:recommended",
"@vue/prettier",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/prettier/@typescript-eslint",
],

View File

@ -1,6 +1,6 @@
{
"name": "mobilizon",
"version": "1.1.4",
"version": "1.2.3",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
@ -8,12 +8,13 @@
"test:unit": "LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e",
"lint": "vue-cli-service lint",
"build:assets": "vue-cli-service build --modern",
"build:assets": "vue-cli-service build",
"build:pictures": "bash ./scripts/build/pictures.sh"
},
"dependencies": {
"@absinthe/socket": "^0.2.1",
"@absinthe/socket-apollo-link": "^0.2.1",
"@apollo/client": "^3.3.16",
"@mdi/font": "^5.0.45",
"@tiptap/core": "^2.0.0-beta.41",
"@tiptap/extension-blockquote": "^2.0.0-beta.6",
@ -28,15 +29,9 @@
"@tiptap/extension-underline": "^2.0.0-beta.7",
"@tiptap/starter-kit": "^2.0.0-beta.37",
"@tiptap/vue-2": "^2.0.0-beta.21",
"@vue/apollo-option": "^4.0.0-alpha.11",
"apollo-absinthe-upload-link": "^1.5.0",
"apollo-cache": "^1.3.5",
"apollo-cache-inmemory": "^1.6.6",
"apollo-client": "^2.6.10",
"apollo-link": "^1.2.14",
"apollo-link-error": "^1.1.13",
"apollo-link-http": "^1.5.17",
"apollo-link-ws": "^1.0.19",
"apollo-utilities": "^1.3.2",
"blurhash": "^1.1.3",
"buefy": "^0.9.0",
"bulma-divider": "^0.2.0",
"core-js": "^3.6.4",
@ -44,18 +39,18 @@
"graphql": "^15.0.0",
"graphql-tag": "^2.10.3",
"intersection-observer": "^0.12.0",
"jwt-decode": "^3.1.2",
"leaflet": "^1.4.0",
"leaflet.locatecontrol": "^0.73.0",
"leaflet.locatecontrol": "^0.74.0",
"lodash": "^4.17.11",
"ngeohash": "^0.6.3",
"p-debounce": "^4.0.0",
"phoenix": "^1.4.11",
"register-service-worker": "^1.7.1",
"register-service-worker": "^1.7.2",
"tippy.js": "^6.2.3",
"unfetch": "^4.2.0",
"v-tooltip": "^2.1.3",
"vue": "^2.6.11",
"vue-apollo": "^3.0.3",
"vue-class-component": "^7.2.3",
"vue-i18n": "^8.14.0",
"vue-meta": "^2.3.1",
@ -75,39 +70,36 @@
"@types/prosemirror-model": "^1.7.2",
"@types/prosemirror-state": "^1.2.4",
"@types/prosemirror-view": "^1.11.4",
"@types/vuedraggable": "^2.23.0",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"@vue/cli-plugin-babel": "~4.5.13",
"@vue/cli-plugin-e2e-cypress": "~4.5.13",
"@vue/cli-plugin-eslint": "~4.5.13",
"@vue/cli-plugin-pwa": "~4.5.13",
"@vue/cli-plugin-router": "~4.5.13",
"@vue/cli-plugin-typescript": "~4.5.13",
"@vue/cli-plugin-unit-jest": "~4.5.13",
"@vue/cli-service": "~4.5.13",
"@vue/cli-plugin-babel": "~5.0.0-beta.2",
"@vue/cli-plugin-e2e-cypress": "~5.0.0-beta.2",
"@vue/cli-plugin-eslint": "~5.0.0-beta.2",
"@vue/cli-plugin-pwa": "~5.0.0-beta.2",
"@vue/cli-plugin-router": "~5.0.0-beta.2",
"@vue/cli-plugin-typescript": "~5.0.0-beta.2",
"@vue/cli-plugin-unit-jest": "~5.0.0-beta.2",
"@vue/cli-service": "~5.0.0-beta.2",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"@vue/test-utils": "^1.1.0",
"eslint": "^6.7.2",
"eslint": "^7.20.0",
"eslint-plugin-cypress": "^2.10.3",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^6.2.2",
"eslint-plugin-vue": "^7.6.0",
"flush-promises": "^1.0.2",
"jest-junit": "^12.0.0",
"mock-apollo-client": "^0.6",
"mock-apollo-client": "^1.1.0",
"prettier": "^2.2.1",
"prettier-eslint": "^12.0.0",
"sass": "^1.29.0",
"sass-loader": "^8.0.2",
"sass": "^1.34.1",
"sass-loader": "^12.0.0",
"ts-jest": "^26.5.3",
"typescript": "~4.1.5",
"vue-cli-plugin-svg": "~0.1.3",
"vue-i18n-extract": "^1.0.2",
"vue-jest": "^4.0.1",
"vue-template-compiler": "^2.6.11",
"webpack-cli": "^3.3"
},
"resolutions": {
"workbox-webpack-plugin": "5.1.3"
"webpack-cli": "^4.7.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 1015 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><path style="opacity:1;fill:#fea72b;fill-opacity:1;stroke:none;stroke-opacity:1" d="M-5.801-6.164h72.69v72.871h-72.69z"/><g data-name="Calque 2"><g data-name="header"><path d="M26.58 27.06q0 8-4.26 12.3a12.21 12.21 0 0 1-9 3.42 12.21 12.21 0 0 1-9-3.42Q0 35.1 0 27.06q0-8.04 4.26-12.3a12.21 12.21 0 0 1 9-3.42 12.21 12.21 0 0 1 9 3.42q4.32 4.24 4.32 12.3zM13.29 17q-5.67 0-5.67 10.06t5.67 10.08q5.71 0 5.71-10.08T13.29 17z" style="fill:#3a384c;fill-opacity:1" transform="translate(14.627 5.256) scale(1.15671)"/><path d="M9 6.78a7.37 7.37 0 0 1-.6-3 7.37 7.37 0 0 1 .6-3A8.09 8.09 0 0 1 12.83 0a7.05 7.05 0 0 1 3.69.84 7.37 7.37 0 0 1 .6 3 7.37 7.37 0 0 1-.6 3 7.46 7.46 0 0 1-3.87.84A6.49 6.49 0 0 1 9 6.78z" style="fill:#fff" transform="translate(14.627 5.256) scale(1.15671)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 B

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,149 +1 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,16.000000) scale(0.000320,-0.000320)"
fill="#000000" stroke="none">
<path d="M18 46618 c45 -75 122 -207 122 -211 0 -2 25 -45 55 -95 30 -50 55
-96 55 -102 0 -5 5 -10 10 -10 6 0 10 -4 10 -9 0 -5 73 -135 161 -288 89 -153
173 -298 187 -323 14 -25 32 -57 41 -72 88 -149 187 -324 189 -335 2 -7 8 -13
13 -13 5 0 9 -4 9 -10 0 -5 46 -89 103 -187 175 -302 490 -846 507 -876 8 -16
20 -36 25 -45 28 -46 290 -498 339 -585 13 -23 74 -129 136 -236 61 -107 123
-215 137 -240 14 -25 29 -50 33 -56 5 -5 23 -37 40 -70 18 -33 38 -67 44 -75
11 -16 21 -33 63 -109 14 -25 29 -50 33 -56 4 -5 21 -35 38 -65 55 -100 261
-455 269 -465 4 -5 14 -21 20 -35 15 -29 41 -75 103 -180 24 -41 52 -88 60
-105 9 -16 57 -100 107 -185 112 -193 362 -626 380 -660 8 -14 23 -38 33 -55
11 -16 23 -37 27 -45 4 -8 26 -46 48 -85 23 -38 53 -90 67 -115 46 -81 64
-113 178 -310 62 -107 121 -210 132 -227 37 -67 56 -99 85 -148 16 -27 32 -57
36 -65 4 -8 15 -27 25 -42 9 -15 53 -89 96 -165 44 -76 177 -307 296 -513 120
-206 268 -463 330 -570 131 -227 117 -203 200 -348 36 -62 73 -125 82 -140 10
-15 21 -34 25 -42 4 -8 20 -37 36 -65 17 -27 38 -65 48 -82 49 -85 64 -111 87
-153 13 -25 28 -49 32 -55 4 -5 78 -134 165 -285 87 -151 166 -288 176 -305
10 -16 26 -43 35 -59 9 -17 125 -217 257 -445 132 -229 253 -441 270 -471 17
-30 45 -79 64 -108 18 -29 33 -54 33 -57 0 -2 20 -37 44 -77 24 -40 123 -212
221 -383 97 -170 190 -330 205 -355 16 -25 39 -65 53 -90 13 -25 81 -144 152
-265 70 -121 137 -238 150 -260 12 -22 37 -65 55 -95 18 -30 43 -73 55 -95 12
-22 48 -85 80 -140 77 -132 163 -280 190 -330 13 -22 71 -123 130 -225 59
-102 116 -199 126 -217 10 -17 29 -50 43 -72 15 -22 26 -43 26 -45 0 -2 27
-50 60 -106 33 -56 60 -103 60 -105 0 -2 55 -98 90 -155 8 -14 182 -316 239
-414 13 -22 45 -79 72 -124 27 -46 49 -86 49 -89 0 -2 14 -24 30 -48 16 -24
30 -46 30 -49 0 -5 74 -135 100 -176 5 -8 24 -42 43 -75 50 -88 58 -101 262
-455 104 -179 199 -345 213 -370 14 -25 28 -49 32 -55 4 -5 17 -26 28 -45 10
-19 62 -109 114 -200 114 -197 133 -230 170 -295 16 -27 33 -57 38 -65 17 -28
96 -165 103 -180 4 -8 16 -28 26 -45 10 -16 77 -131 148 -255 72 -124 181
-313 243 -420 62 -107 121 -209 131 -227 35 -62 323 -560 392 -678 38 -66 83
-145 100 -175 16 -30 33 -59 37 -65 4 -5 17 -27 29 -47 34 -61 56 -100 90
-156 17 -29 31 -55 31 -57 0 -2 17 -32 39 -67 21 -35 134 -229 251 -433 117
-203 235 -407 261 -451 27 -45 49 -85 49 -88 0 -4 8 -19 19 -34 15 -21 200
-341 309 -533 10 -19 33 -58 51 -87 17 -29 31 -54 31 -56 0 -2 25 -44 55 -94
30 -50 55 -95 55 -98 0 -4 6 -15 14 -23 7 -9 27 -41 43 -71 17 -30 170 -297
342 -594 171 -296 311 -542 311 -547 0 -5 5 -9 10 -9 6 0 10 -4 10 -10 0 -5
22 -47 49 -92 27 -46 58 -99 68 -118 24 -43 81 -140 93 -160 5 -8 66 -114 135
-235 69 -121 130 -227 135 -235 12 -21 259 -447 283 -490 10 -19 28 -47 38
-62 11 -14 19 -29 19 -32 0 -3 37 -69 83 -148 99 -170 305 -526 337 -583 13
-22 31 -53 41 -70 11 -16 22 -37 26 -45 7 -14 82 -146 103 -180 14 -24 181
-311 205 -355 13 -22 46 -80 75 -130 29 -49 64 -110 78 -135 14 -25 51 -88 82
-140 31 -52 59 -102 63 -110 4 -8 18 -33 31 -55 205 -353 284 -489 309 -535
17 -30 45 -78 62 -106 18 -28 36 -60 39 -72 4 -12 12 -22 17 -22 5 0 9 -4 9
-10 0 -5 109 -197 241 -427 133 -230 250 -431 259 -448 51 -90 222 -385 280
-485 37 -63 78 -135 92 -160 14 -25 67 -117 118 -205 51 -88 101 -175 111
-193 34 -58 55 -95 149 -257 51 -88 101 -173 110 -190 9 -16 76 -131 147 -255
72 -124 140 -241 151 -260 61 -108 281 -489 355 -615 38 -66 77 -133 87 -150
35 -63 91 -161 100 -175 14 -23 99 -169 128 -220 54 -97 135 -235 142 -245 4
-5 20 -32 35 -60 26 -48 238 -416 276 -480 10 -16 26 -46 37 -65 30 -53 382
-661 403 -695 10 -16 22 -37 26 -45 4 -8 26 -48 50 -88 24 -41 43 -75 43 -77
0 -2 22 -40 50 -85 27 -45 50 -84 50 -86 0 -3 38 -69 83 -147 84 -142 302
-520 340 -587 10 -19 34 -60 52 -90 18 -30 44 -75 57 -100 14 -25 45 -79 70
-120 25 -41 56 -96 70 -121 14 -25 77 -133 138 -240 62 -107 122 -210 132
-229 25 -43 310 -535 337 -581 11 -19 26 -45 34 -59 17 -32 238 -414 266 -460
11 -19 24 -41 28 -49 3 -7 75 -133 160 -278 84 -146 153 -269 153 -274 0 -5 5
-9 10 -9 6 0 10 -4 10 -10 0 -5 82 -150 181 -322 182 -314 201 -346 240 -415
12 -21 80 -139 152 -263 71 -124 141 -245 155 -270 14 -25 28 -49 32 -55 6 -8
145 -248 220 -380 37 -66 209 -362 229 -395 11 -19 24 -42 28 -49 4 -8 67
-118 140 -243 73 -125 133 -230 133 -233 0 -2 15 -28 33 -57 19 -29 47 -78 64
-108 17 -30 53 -93 79 -139 53 -90 82 -141 157 -272 82 -142 115 -199 381
-659 142 -245 268 -463 281 -485 12 -22 71 -125 132 -230 60 -104 172 -298
248 -430 76 -132 146 -253 156 -270 11 -16 22 -36 26 -44 3 -8 30 -54 60 -103
29 -49 53 -91 53 -93 0 -3 18 -34 40 -70 22 -36 40 -67 40 -69 0 -2 37 -66 81
-142 45 -77 98 -168 119 -204 20 -36 47 -81 58 -100 12 -19 27 -47 33 -62 6
-16 15 -28 20 -28 5 0 9 -4 9 -9 0 -6 63 -118 140 -251 77 -133 140 -243 140
-245 0 -2 18 -33 41 -70 22 -37 49 -83 60 -101 10 -19 29 -51 40 -71 25 -45
109 -189 126 -218 7 -11 17 -29 22 -40 6 -11 22 -38 35 -60 14 -22 37 -62 52
-90 14 -27 35 -62 45 -77 11 -14 19 -29 19 -32 0 -3 18 -35 40 -71 22 -36 40
-67 40 -69 0 -2 19 -35 42 -72 23 -38 55 -94 72 -124 26 -47 139 -244 171
-298 6 -9 21 -36 34 -60 28 -48 37 -51 51 -19 6 12 19 36 29 52 10 17 27 46
38 65 11 19 104 181 208 360 103 179 199 345 213 370 14 25 42 74 64 109 21
34 38 65 38 67 0 2 18 33 40 69 22 36 40 67 40 69 0 3 177 310 199 346 16 26
136 234 140 244 2 5 25 44 52 88 27 44 49 81 49 84 0 2 18 34 40 70 22 36 40
67 40 69 0 2 20 36 43 77 35 58 169 289 297 513 9 17 50 86 90 155 40 69 86
150 103 180 16 30 35 62 41 70 6 8 16 24 22 35 35 64 72 129 167 293 59 100
116 199 127 220 11 20 30 53 41 72 43 72 1070 1850 1121 1940 14 25 65 113
113 195 48 83 96 166 107 185 10 19 28 50 38 68 11 18 73 124 137 235 64 111
175 303 246 427 71 124 173 299 225 390 52 91 116 202 143 248 27 45 49 85 49
89 0 4 6 14 14 22 7 9 28 43 46 76 26 47 251 436 378 655 11 19 29 51 40 70
11 19 101 176 201 348 99 172 181 317 181 323 0 5 5 9 10 9 6 0 10 5 10 11 0
6 8 23 18 37 11 15 32 52 49 82 16 30 130 228 253 440 122 212 234 405 248
430 13 25 39 70 57 100 39 65 69 117 130 225 25 44 50 87 55 95 12 19 78 134
220 380 61 107 129 224 150 260 161 277 222 382 246 425 15 28 47 83 71 123
24 41 43 78 43 83 0 5 4 9 8 9 4 0 13 12 19 28 7 15 23 45 36 67 66 110 277
478 277 483 0 3 6 13 14 21 7 9 27 41 43 71 17 30 45 80 63 110 34 57 375 649
394 685 6 11 16 27 22 35 6 8 26 42 44 75 18 33 41 74 51 90 10 17 24 41 32
55 54 97 72 128 88 152 11 14 19 28 19 30 0 3 79 141 175 308 96 167 175 305
175 308 0 3 6 13 14 21 7 9 26 39 41 66 33 60 276 483 338 587 24 40 46 80 50
88 4 8 13 24 20 35 14 23 95 163 125 215 11 19 52 91 92 160 40 69 80 139 90
155 9 17 103 179 207 360 105 182 200 346 211 365 103 181 463 802 489 845 7
11 15 27 19 35 4 8 29 51 55 95 64 110 828 1433 848 1470 9 17 24 41 33 55 9
14 29 48 45 77 15 28 52 93 82 145 30 51 62 107 71 123 17 30 231 398 400 690
51 88 103 179 115 202 12 23 26 48 32 55 6 7 24 38 40 68 17 30 61 107 98 170
37 63 84 144 103 180 19 36 41 72 48 81 8 8 14 18 14 21 0 4 27 51 59 106 32
55 72 124 89 154 16 29 71 125 122 213 51 88 104 180 118 205 13 25 28 50 32
55 4 6 17 26 28 45 11 19 45 80 77 135 31 55 66 116 77 135 11 19 88 152 171
295 401 694 620 1072 650 1125 11 19 87 152 170 295 83 143 158 273 166 288 9
16 21 36 26 45 6 9 31 52 55 96 25 43 54 94 66 115 11 20 95 164 186 321 91
157 173 299 182 315 9 17 26 46 37 65 12 19 66 114 121 210 56 96 108 186 117
200 8 14 24 40 34 59 24 45 383 664 412 713 5 9 17 29 26 45 15 28 120 210
241 419 36 61 68 117 72 125 4 8 12 23 19 34 35 57 245 420 262 453 11 20 35
61 53 90 17 29 32 54 32 56 0 3 28 51 62 108 33 57 70 119 80 138 10 19 23 42
28 50 5 8 32 53 59 100 27 47 149 258 271 470 122 212 234 405 248 430 30 53
62 108 80 135 6 11 15 27 19 35 4 8 85 150 181 315 96 165 187 323 202 350 31
56 116 202 130 225 5 8 25 42 43 75 19 33 92 159 162 280 149 257 157 271 202
350 19 33 38 67 43 75 9 14 228 392 275 475 12 22 55 96 95 165 40 69 80 139
90 155 24 42 202 350 221 383 9 15 27 47 41 72 14 25 75 131 136 236 61 106
121 210 134 232 99 172 271 470 279 482 5 8 23 40 40 70 18 30 81 141 142 245
60 105 121 210 135 235 14 25 71 124 127 220 56 96 143 247 194 335 51 88 96
167 102 175 14 24 180 311 204 355 23 43 340 590 356 615 5 8 50 87 101 175
171 301 517 898 582 1008 25 43 46 81 46 83 0 2 12 23 27 47 14 23 40 67 56
97 16 30 35 62 42 70 7 8 15 22 18 30 4 8 20 38 37 65 16 28 33 57 37 65 6 12
111 196 143 250 5 8 55 95 112 193 57 98 113 195 126 215 12 20 27 46 32 57 6
11 14 27 20 35 5 8 76 130 156 270 80 140 165 287 187 325 23 39 52 90 66 115
13 25 30 52 37 61 8 8 14 18 14 21 0 4 41 77 92 165 50 87 175 302 276 478
101 176 208 360 236 408 28 49 67 117 86 152 19 35 41 70 48 77 6 6 12 15 12
19 0 7 124 224 167 291 12 21 23 40 23 42 0 2 21 40 46 83 26 43 55 92 64 109
54 95 327 568 354 614 19 30 45 75 59 100 71 128 82 145 89 148 4 2 8 8 8 13
0 5 42 82 94 172 311 538 496 858 518 897 14 25 40 70 58 100 18 30 42 71 53
90 10 19 79 139 152 265 73 127 142 246 153 265 10 19 43 76 72 125 29 50 63
108 75 130 65 116 80 140 87 143 4 2 8 8 8 12 0 8 114 212 140 250 6 8 14 24
20 35 5 11 54 97 108 190 l100 170 -9611 3 c-5286 1 -9614 -1 -9618 -5 -5 -6
-419 -719 -619 -1068 -89 -155 -267 -463 -323 -560 -38 -66 -81 -140 -95 -165
-31 -56 -263 -457 -526 -910 -110 -190 -224 -388 -254 -440 -29 -52 -61 -109
-71 -125 -23 -39 -243 -420 -268 -465 -11 -19 -204 -352 -428 -740 -224 -388
-477 -826 -563 -975 -85 -148 -185 -322 -222 -385 -37 -63 -120 -207 -185
-320 -65 -113 -177 -306 -248 -430 -72 -124 -172 -297 -222 -385 -51 -88 -142
-245 -202 -350 -131 -226 -247 -427 -408 -705 -65 -113 -249 -432 -410 -710
-160 -278 -388 -673 -506 -877 -118 -205 -216 -373 -219 -373 -3 0 -52 82
-109 183 -58 100 -144 250 -192 332 -95 164 -402 696 -647 1120 -85 149 -228
396 -317 550 -212 365 -982 1700 -1008 1745 -10 19 -43 76 -72 125 -29 50 -64
110 -77 135 -14 25 -63 110 -110 190 -47 80 -96 165 -110 190 -14 25 -99 171
-188 325 -89 154 -174 300 -188 325 -13 25 -64 113 -112 195 -48 83 -140 242
-205 355 -65 113 -183 317 -263 454 -79 137 -152 264 -163 282 -50 89 -335
583 -354 614 -12 19 -34 58 -50 85 -15 28 -129 226 -253 440 -124 215 -235
408 -247 430 -12 22 -69 121 -127 220 -58 99 -226 389 -373 645 -148 256 -324
561 -392 678 -67 117 -134 232 -147 255 -13 23 -33 59 -46 80 l-22 37 -9615 0
-9615 0 20 -32z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><path style="opacity:1;fill:#fea72b;fill-opacity:1;stroke:none;stroke-opacity:1" d="M-5.801-6.164h72.69v72.871h-72.69z"/><g data-name="Calque 2"><g data-name="header"><path d="M26.58 27.06q0 8-4.26 12.3a12.21 12.21 0 0 1-9 3.42 12.21 12.21 0 0 1-9-3.42Q0 35.1 0 27.06q0-8.04 4.26-12.3a12.21 12.21 0 0 1 9-3.42 12.21 12.21 0 0 1 9 3.42q4.32 4.24 4.32 12.3zM13.29 17q-5.67 0-5.67 10.06t5.67 10.08q5.71 0 5.71-10.08T13.29 17z" style="fill:#3a384c;fill-opacity:1" transform="translate(14.627 5.256) scale(1.15671)"/><path d="M9 6.78a7.37 7.37 0 0 1-.6-3 7.37 7.37 0 0 1 .6-3A8.09 8.09 0 0 1 12.83 0a7.05 7.05 0 0 1 3.69.84 7.37 7.37 0 0 1 .6 3 7.37 7.37 0 0 1-.6 3 7.46 7.46 0 0 1-3.87.84A6.49 6.49 0 0 1 9 6.78z" style="fill:#fff" transform="translate(14.627 5.256) scale(1.15671)"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -50,6 +50,8 @@ import { initializeCurrentActor } from "./utils/auth";
import { CONFIG } from "./graphql/config";
import { IConfig } from "./types/config.model";
import { ICurrentUser } from "./types/current-user.model";
import jwt_decode, { JwtPayload } from "jwt-decode";
import { refreshAccessToken } from "./apollo/utils";
@Component({
apollo: {
@ -63,6 +65,11 @@ import { ICurrentUser } from "./types/current-user.model";
import(/* webpackChunkName: "editor" */ "./components/Error.vue"),
"mobilizon-footer": Footer,
},
metaInfo() {
return {
titleTemplate: "%s | Mobilizon",
};
},
})
export default class App extends Vue {
config!: IConfig;
@ -71,6 +78,10 @@ export default class App extends Vue {
error: Error | null = null;
online = true;
interval: number | undefined = undefined;
async created(): Promise<void> {
if (await this.initializeCurrentUser()) {
await initializeCurrentActor(this.$apollo.provider.defaultClient);
@ -100,6 +111,92 @@ export default class App extends Vue {
}
return false;
}
mounted(): void {
this.online = window.navigator.onLine;
window.addEventListener("offline", () => {
this.online = false;
this.showOfflineNetworkWarning();
console.debug("offline");
});
window.addEventListener("online", () => {
this.online = true;
console.debug("online");
});
document.addEventListener("refreshApp", (event: Event) => {
this.$buefy.snackbar.open({
queue: false,
indefinite: true,
type: "is-secondary",
actionText: this.$t("Update app") as string,
cancelText: this.$t("Ignore") as string,
message: this.$t("A new version is available.") as string,
onAction: async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const detail = event.detail;
const registration = detail as ServiceWorkerRegistration;
try {
await this.refreshApp(registration);
window.location.reload();
} catch (err) {
console.error(err);
this.$notifier.error(
this.$t(
"An error has occured while refreshing the page."
) as string
);
}
},
});
});
this.interval = setInterval(async () => {
const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN);
if (accessToken) {
const token = jwt_decode<JwtPayload>(accessToken);
if (
token?.exp !== undefined &&
new Date(token.exp * 1000 - 60000) < new Date()
) {
refreshAccessToken(this.$apollo.getClient());
}
}
}, 60000);
}
private async refreshApp(
registration: ServiceWorkerRegistration
): Promise<any> {
const worker = registration.waiting;
if (!worker) {
return Promise.resolve();
}
console.debug("Doing worker.skipWaiting().");
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = (event) => {
console.debug("Done worker.skipWaiting().");
if (event.data.error) {
reject(event.data);
} else {
resolve(event.data);
}
};
console.debug("calling skip waiting");
worker?.postMessage({ type: "skip-waiting" }, [channel.port2]);
});
}
showOfflineNetworkWarning(): void {
this.$notifier.error(this.$t("You are offline") as string);
}
unmounted(): void {
clearInterval(this.interval);
this.interval = undefined;
}
}
</script>

View File

@ -1,12 +1,14 @@
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { CURRENT_USER_CLIENT } from "@/graphql/user";
import { ICurrentUserRole } from "@/types/enums";
import { ApolloCache } from "apollo-cache";
import { NormalizedCacheObject } from "apollo-cache-inmemory";
import { Resolvers } from "apollo-client/core/types";
import { ApolloCache, NormalizedCacheObject } from "@apollo/client/cache";
import { Resolvers } from "@apollo/client/core/types";
export default function buildCurrentUserResolver(
cache: ApolloCache<NormalizedCacheObject>
): Resolvers {
cache.writeData({
cache.writeQuery({
query: CURRENT_USER_CLIENT,
data: {
currentUser: {
__typename: "CurrentUser",
@ -15,6 +17,12 @@ export default function buildCurrentUserResolver(
isLoggedIn: false,
role: ICurrentUserRole.USER,
},
},
});
cache.writeQuery({
query: CURRENT_ACTOR_CLIENT,
data: {
currentActor: {
__typename: "CurrentActor",
id: null,
@ -47,7 +55,7 @@ export default function buildCurrentUserResolver(
},
};
localCache.writeData({ data });
localCache.writeQuery({ data, query: CURRENT_USER_CLIENT });
},
updateCurrentActor: (
_: any,
@ -74,7 +82,7 @@ export default function buildCurrentUserResolver(
},
};
localCache.writeData({ data });
localCache.writeQuery({ data, query: CURRENT_ACTOR_CLIENT });
},
},
};

View File

@ -1,16 +1,100 @@
import {
IntrospectionFragmentMatcher,
NormalizedCacheObject,
} from "apollo-cache-inmemory";
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from "@/constants";
import { REFRESH_TOKEN } from "@/graphql/auth";
import { IFollower } from "@/types/actor/follower.model";
import { IParticipant } from "@/types/participant.model";
import { Paginate } from "@/types/paginate";
import { saveTokenData } from "@/utils/auth";
import { ApolloClient } from "apollo-client";
import {
ApolloClient,
FieldPolicy,
NormalizedCacheObject,
Reference,
TypePolicies,
} from "@apollo/client/core";
import introspectionQueryResultData from "../../fragmentTypes.json";
import { IMember } from "@/types/actor/member.model";
import { IComment } from "@/types/comment.model";
import { IEvent } from "@/types/event.model";
import { IActivity } from "@/types/activity.model";
import uniqBy from "lodash/uniqBy";
export const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
type possibleTypes = { name: string };
type schemaType = {
kind: string;
name: string;
possibleTypes: possibleTypes[];
};
// eslint-disable-next-line no-underscore-dangle
const types = introspectionQueryResultData.__schema.types as schemaType[];
export const possibleTypes = types.reduce((acc, type) => {
if (type.kind === "INTERFACE") {
acc[type.name] = type.possibleTypes.map(({ name }) => name);
}
return acc;
}, {} as Record<string, string[]>);
const replaceMergePolicy = <TExisting = any, TIncoming = any>(
_existing: TExisting,
incoming: TIncoming
): TIncoming => incoming;
export const typePolicies: TypePolicies = {
Discussion: {
fields: {
comments: paginatedLimitPagination<IComment>(),
},
},
Group: {
fields: {
organizedEvents: paginatedLimitPagination([
"afterDatetime",
"beforeDatetime",
]),
activity: paginatedLimitPagination<IActivity>(["type", "author"]),
},
},
Person: {
fields: {
organizedEvents: pageLimitPagination(),
participations: paginatedLimitPagination<IParticipant>(["eventId"]),
memberships: paginatedLimitPagination<IMember>(["group"]),
},
},
Event: {
fields: {
participants: paginatedLimitPagination<IParticipant>(["roles"]),
comments: pageLimitPagination<IComment>(),
relatedEvents: pageLimitPagination<IEvent>(),
options: { merge: replaceMergePolicy },
participantStats: { merge: replaceMergePolicy },
},
},
RootQueryType: {
fields: {
relayFollowers: paginatedLimitPagination<IFollower>(),
relayFollowings: paginatedLimitPagination<IFollower>([
"orderBy",
"direction",
]),
events: paginatedLimitPagination(),
groups: paginatedLimitPagination([
"preferredUsername",
"name",
"domain",
"local",
"suspended",
]),
persons: paginatedLimitPagination([
"preferredUsername",
"name",
"domain",
"local",
"suspended",
]),
},
},
};
export async function refreshAccessToken(
apolloClient: ApolloClient<NormalizedCacheObject>
@ -37,3 +121,66 @@ export async function refreshAccessToken(
return false;
}
}
type KeyArgs = FieldPolicy<any>["keyArgs"];
export function pageLimitPagination<T = Reference>(
keyArgs: KeyArgs = false
): FieldPolicy<T[]> {
return {
keyArgs,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
merge(existing, incoming, { args }) {
if (!incoming) return existing;
if (!existing) return incoming; // existing will be empty the first time
return doMerge(existing as Array<T>, incoming as Array<T>, args);
},
};
}
export function paginatedLimitPagination<T = Paginate<any>>(
keyArgs: KeyArgs = false
): FieldPolicy<Paginate<T>> {
return {
keyArgs,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
merge(existing, incoming, { args }) {
if (!incoming) return existing;
if (!existing) return incoming; // existing will be empty the first time
return {
total: incoming.total,
elements: doMerge(existing.elements, incoming.elements, args),
};
},
};
}
function doMerge<T = any>(
existing: Array<T>,
incoming: Array<T>,
args: Record<string, any> | null
): Array<T> {
const merged = existing && Array.isArray(existing) ? existing.slice(0) : [];
let res;
if (args) {
// Assume an page of 1 if args.page omitted.
const { page = 1, limit = 10 } = args;
for (let i = 0; i < incoming.length; ++i) {
merged[(page - 1) * limit + i] = incoming[i];
}
res = merged;
} else {
// It's unusual (probably a mistake) for a paginated field not
// to receive any arguments, so you might prefer to throw an
// exception here, instead of recovering by appending incoming
// onto the existing array.
res = [...merged, ...incoming];
// eslint-disable-next-line no-underscore-dangle
res = uniqBy(res, (elem: any) => elem.__ref);
}
return res;
}

View File

@ -1,4 +0,0 @@
<svg version="1.1" viewBox="0 0 65.131 65.131" xmlns="http://www.w3.org/2000/svg">
<path d="m28.214 64.754c-6.9441-0.80647-14.478-4.7044-19.429-10.053-8.1024-8.7516-10.823-21.337-7.0178-32.463 3.8465-11.248 12.917-19.153 24.746-21.569 7.2561-1.4817 14.813-0.27619 21.622 3.4495 7.517 4.1126 12.568 10.251 15.291 18.582 5.5678 17.038-4.1941 35.667-21.417 40.87-4.6929 1.4178-8.7675 1.7673-13.795 1.1834zm0.43913-17.263c2.0058-2.7986 3.7663-5.0883 3.9123-5.0883 0.14591 0 1.9109 2.2959 3.9221 5.102 2.0112 2.8061 3.827 5.0577 4.0349 5.0035 0.90081-0.23467 8.2871-5.9034 8.1633-6.265-0.07527-0.21984-1.7555-2.6427-3.7338-5.3842-1.9783-2.7414-3.552-5.0223-3.497-5.0686 0.05497-0.04629 2.8095-0.97845 6.1211-2.0715 3.3117-1.093 6.0224-2.1432 6.0239-2.3338 0.0073-0.92502-2.9094-9.4312-3.283-9.5746-0.23567-0.09043-2.9906 0.68953-6.1221 1.7332-3.1315 1.0437-5.8046 1.8977-5.9404 1.8977-0.13575 0-0.28828-2.9385-0.33895-6.53l-0.09213-6.53h-10.516l-0.09213 6.53c-0.05067 3.5915-0.20809 6.53-0.34982 6.53s-2.9544-0.90204-6.2504-2.0045l-5.9927-2.0045-1.5444 4.6339c-0.8494 2.5487-1.5444 4.866-1.5444 5.1496 0 0.36743 1.7311 1.087 6.0212 2.503 3.3117 1.093 6.0662 2.0252 6.1211 2.0715 0.05497 0.04629-1.5187 2.3272-3.497 5.0686-1.9783 2.7415-3.6605 5.1643-3.7382 5.3842-0.14163 0.40073 7.4833 6.2827 8.1896 6.3175 0.20673 0.01021 2.017-2.2712 4.0228-5.0698z" stroke-width=".33922"/>
<path d="m23.631 51.953c-2.348-1.5418-6.9154-5.1737-7.0535-5.6088-0.06717-0.21164 0.45125-0.99318 3.3654-5.0734 2.269-3.177 3.7767-5.3581 3.7767-5.4637 0-0.03748-1.6061-0.60338-3.5691-1.2576-6.1342-2.0442-8.3916-2.9087-8.5288-3.2663-0.03264-0.08506 0.09511-0.68598 0.28388-1.3354 0.643-2.212 2.7038-8.4123 2.7959-8.4123 0.05052 0 2.6821 0.85982 5.848 1.9107 3.1659 1.0509 5.897 1.9222 6.0692 1.9362 0.3089 0.02514 0.31402 0.01925 0.38295-0.44107 0.09851-0.65784 0.26289-5.0029 0.2633-6.9599 1.87e-4 -0.90267 0.02801-2.5298 0.06184-3.6158l0.0615-1.9746h10.392l0.06492 4.4556c0.06287 4.3148 0.18835 7.8236 0.29865 8.3513 0.0295 0.14113 0.11236 0.2566 0.18412 0.2566 0.07176 0 1.6955-0.50861 3.6084-1.1303 4.5213-1.4693 6.2537-2.0038 7.3969-2.2822 0.87349-0.21269 0.94061-0.21704 1.0505-0.06806 0.45169 0.61222 3.3677 9.2365 3.1792 9.4025-0.33681 0.29628-2.492 1.1048-6.9823 2.6194-5.3005 1.7879-5.1321 1.7279-5.1321 1.8283 0 0.13754 0.95042 1.522 3.5468 5.1666 1.3162 1.8475 2.6802 3.7905 3.0311 4.3176l0.63804 0.95842-0.27216 0.28519c-1.1112 1.1644-7.3886 5.8693-7.8309 5.8693-0.22379 0-1.2647-1.2321-2.9284-3.4663-0.90374-1.2137-2.264-3.0402-3.0228-4.059-0.75878-1.0188-1.529-2.0203-1.7116-2.2256l-0.33201-0.37324-0.32674 0.37324c-0.43918 0.50169-2.226 2.867-3.8064 5.0388-2.1662 2.9767-3.6326 4.8055-3.8532 4.8055-0.05161 0-0.4788-0.25278-0.94931-0.56173z" fill="#fff" stroke-width=".093311"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -19,14 +19,6 @@
margin: 15px auto 30px;
}
main > .container {
background: $whitest;
min-height: 70vh;
}
.step-content {
height: auto;
}
a.out,
.content a,
.ProseMirror a {
@ -35,7 +27,12 @@ a.out,
text-decoration-thickness: 2px;
}
.step-content {
height: auto;
}
main {
> section > .columns {
min-height: 50vh;
}
@ -44,6 +41,10 @@ main {
min-height: 80vh;
}
}
> .container {
background: $whitest;
min-height: 70vh;
}
> #homepage {
background: $whitest;
#featured_events {
@ -73,7 +74,6 @@ $color-black: #000;
.mention {
background: rgba($color-black, 0.1);
color: rgba($color-black, 0.6);
font-size: 0.9rem;
font-weight: bold;
border-radius: 5px;
@ -108,6 +108,8 @@ body {
background: $body-background-color;
font-family: BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, "Segoe UI",
"Fira Sans", "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
overflow-x: hidden;
}
#mobilizon {

View File

@ -45,7 +45,7 @@
</template>
<script lang="ts">
import { Component, Model, Vue, Watch } from "vue-property-decorator";
import { debounce } from "lodash";
import debounce from "lodash/debounce";
import { IPerson } from "@/types/actor";
import { SEARCH_PERSONS } from "@/graphql/search";
import { Paginate } from "@/types/paginate";

View File

@ -12,7 +12,7 @@
<p>
{{ actor.name || `@${usernameWithDomain(actor)}` }}
</p>
<p class="has-text-grey" v-if="actor.name">
<p class="has-text-grey-dark" v-if="actor.name">
@{{ usernameWithDomain(actor) }}
</p>
<div

View File

@ -0,0 +1,48 @@
<template>
<div class="actor-inline">
<div class="actor-avatar">
<figure class="image is-24x24" v-if="actor.avatar">
<img class="is-rounded" :src="actor.avatar.url" alt="" />
</figure>
<b-icon v-else size="is-medium" icon="account-circle" />
</div>
<div class="actor-name">
<p>
{{ actor.name || `@${usernameWithDomain(actor)}` }}
</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import { IActor, usernameWithDomain } from "../../types/actor";
@Component
export default class ActorInline extends Vue {
@Prop({ required: true, type: Object }) actor!: IActor;
usernameWithDomain = usernameWithDomain;
}
</script>
<style lang="scss" scoped>
div.actor-inline {
align-items: flex-start;
display: inline-flex;
text-align: inherit;
align-items: top;
div.actor-avatar {
flex-basis: auto;
flex-grow: 0;
flex-shrink: 0;
margin-right: 0.5rem;
}
div.actor-name {
flex-basis: auto;
flex-grow: 1;
flex-shrink: 1;
text-align: inherit;
}
}
</style>

View File

@ -41,7 +41,7 @@
></popover-actor-card
></i18n
>
<small class="has-text-grey activity-date">{{
<small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString
}}</small>
</div>

View File

@ -27,7 +27,7 @@
></popover-actor-card
></i18n
>
<small class="has-text-grey activity-date">{{
<small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString
}}</small>
</div>

View File

@ -8,7 +8,9 @@
slot="group"
:to="{
name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(activity.object) },
params: {
preferredUsername: subjectParams.group_federated_username,
},
}"
>{{ subjectParams.group_name }}</router-link
>
@ -32,7 +34,7 @@
v-for="detail in details"
:key="detail"
tag="p"
class="has-text-grey"
class="has-text-grey-dark"
>
<popover-actor-card
:actor="activity.author"
@ -61,7 +63,7 @@
subjectParams.old_group_name
}}</b>
</i18n>
<small class="has-text-grey activity-date">{{
<small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString
}}</small>
</div>

View File

@ -18,7 +18,7 @@
></popover-actor-card
>
<b slot="member" v-else>{{
subjectParams.member_preferred_username
subjectParams.member_actor_federated_username
}}</b>
<popover-actor-card
:actor="activity.author"
@ -34,7 +34,7 @@
></popover-actor-card
></i18n
>
<small class="has-text-grey activity-date">{{
<small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString
}}</small>
</div>
@ -83,6 +83,8 @@ export default class MemberActivityItem extends mixins(ActivityMixin) {
return "You added the member {member}.";
}
return "{profile} added the member {member}.";
case ActivityMemberSubject.MEMBER_JOINED:
return "{member} joined the group.";
case ActivityMemberSubject.MEMBER_UPDATED:
if (this.subjectParams.member_role && this.subjectParams.old_role) {
return this.roleUpdate;

View File

@ -27,7 +27,7 @@
></popover-actor-card
></i18n
>
<small class="has-text-grey activity-date">{{
<small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString
}}</small>
</div>

View File

@ -37,7 +37,7 @@
></popover-actor-card
></i18n
>
<small class="has-text-grey activity-date">{{
<small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString
}}</small>
</div>

View File

@ -10,8 +10,13 @@
:show-detail-icon="false"
paginated
backend-pagination
:current-page.sync="page"
:aria-next-label="$t('Next page')"
:aria-previous-label="$t('Previous page')"
:aria-page-label="$t('Page')"
:aria-current-label="$t('Current page')"
:total="relayFollowers.total"
:per-page="perPage"
:per-page="FOLLOWERS_PER_PAGE"
@page-change="onFollowersPageChange"
checkable
checkbox-position="left"
@ -123,14 +128,33 @@
</div>
</template>
<script lang="ts">
import { Component, Mixins } from "vue-property-decorator";
import { Component, Mixins, Ref } from "vue-property-decorator";
import { SnackbarProgrammatic as Snackbar } from "buefy";
import { formatDistanceToNow } from "date-fns";
import { ACCEPT_RELAY, REJECT_RELAY } from "../../graphql/admin";
import {
ACCEPT_RELAY,
REJECT_RELAY,
RELAY_FOLLOWERS,
} from "../../graphql/admin";
import { IFollower } from "../../types/actor/follower.model";
import RelayMixin from "../../mixins/relay";
import RouteName from "@/router/name";
import { Paginate } from "@/types/paginate";
const FOLLOWERS_PER_PAGE = 10;
@Component({
apollo: {
relayFollowers: {
query: RELAY_FOLLOWERS,
variables() {
return {
page: this.page,
limit: FOLLOWERS_PER_PAGE,
};
},
},
},
metaInfo() {
return {
title: this.$t("Followers") as string,
@ -143,14 +167,36 @@ export default class Followers extends Mixins(RelayMixin) {
formatDistanceToNow = formatDistanceToNow;
async acceptRelays(): Promise<void> {
await this.checkedRows.forEach((row: IFollower) => {
relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
checkedRows: IFollower[] = [];
FOLLOWERS_PER_PAGE = FOLLOWERS_PER_PAGE;
@Ref("table") readonly table!: any;
toggle(row: Record<string, unknown>): void {
this.table.toggleDetails(row);
}
get page(): number {
return parseInt((this.$route.query.page as string) || "1", 10);
}
set page(page: number) {
this.pushRouter(RouteName.RELAY_FOLLOWERS, {
page: page.toString(),
});
}
acceptRelays(): void {
this.checkedRows.forEach((row: IFollower) => {
this.acceptRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
});
}
async rejectRelays(): Promise<void> {
await this.checkedRows.forEach((row: IFollower) => {
rejectRelays(): void {
this.checkedRows.forEach((row: IFollower) => {
this.rejectRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
});
}
@ -196,5 +242,19 @@ export default class Followers extends Mixins(RelayMixin) {
get checkedRowsHaveAtLeastOneToApprove(): boolean {
return this.checkedRows.some((checkedRow) => !checkedRow.approved);
}
async onFollowersPageChange(page: number): Promise<void> {
this.page = page;
try {
await this.$apollo.queries.relayFollowers.fetchMore({
variables: {
page: this.page,
limit: FOLLOWERS_PER_PAGE,
},
});
} catch (err) {
console.error(err);
}
}
}
</script>

View File

@ -32,8 +32,13 @@
:show-detail-icon="false"
paginated
backend-pagination
:current-page.sync="page"
:aria-next-label="$t('Next page')"
:aria-previous-label="$t('Previous page')"
:aria-page-label="$t('Page')"
:aria-current-label="$t('Current page')"
:total="relayFollowings.total"
:per-page="perPage"
:per-page="FOLLOWINGS_PER_PAGE"
@page-change="onFollowingsPageChange"
checkable
checkbox-position="left"
@ -127,7 +132,7 @@
</b-button>
</template>
</b-table>
<b-message type="is-danger" v-if="relayFollowings.elements.length === 0">{{
<b-message type="is-danger" v-if="relayFollowings.total === 0">{{
$t("You don't follow any instances yet.")
}}</b-message>
</div>
@ -139,8 +144,26 @@ import { formatDistanceToNow } from "date-fns";
import { ADD_RELAY, REMOVE_RELAY } from "../../graphql/admin";
import { IFollower } from "../../types/actor/follower.model";
import RelayMixin from "../../mixins/relay";
import { RELAY_FOLLOWINGS } from "@/graphql/admin";
import { Paginate } from "@/types/paginate";
import RouteName from "@/router/name";
import { ApolloCache, FetchResult, Reference } from "@apollo/client/core";
import gql from "graphql-tag";
const FOLLOWINGS_PER_PAGE = 10;
@Component({
apollo: {
relayFollowings: {
query: RELAY_FOLLOWINGS,
variables() {
return {
page: this.page,
limit: FOLLOWINGS_PER_PAGE,
};
},
},
},
metaInfo() {
return {
title: this.$t("Followings") as string,
@ -155,16 +178,81 @@ export default class Followings extends Mixins(RelayMixin) {
formatDistanceToNow = formatDistanceToNow;
relayFollowings: Paginate<IFollower> = { elements: [], total: 0 };
FOLLOWINGS_PER_PAGE = FOLLOWINGS_PER_PAGE;
checkedRows: IFollower[] = [];
get page(): number {
return parseInt((this.$route.query.page as string) || "1", 10);
}
set page(page: number) {
this.pushRouter(RouteName.RELAY_FOLLOWINGS, {
page: page.toString(),
});
}
async onFollowingsPageChange(page: number): Promise<void> {
this.page = page;
try {
await this.$apollo.queries.relayFollowings.fetchMore({
variables: {
page: this.page,
limit: FOLLOWINGS_PER_PAGE,
},
});
} catch (err) {
console.error(err);
}
}
async followRelay(e: Event): Promise<void> {
e.preventDefault();
try {
await this.$apollo.mutate({
await this.$apollo.mutate<{ relayFollowings: Paginate<IFollower> }>({
mutation: ADD_RELAY,
variables: {
address: this.newRelayAddress.trim(), // trim to fix copy and paste domain name spaces and tabs
},
update(
cache: ApolloCache<{ relayFollowings: Paginate<IFollower> }>,
{ data }: FetchResult
) {
cache.modify({
fields: {
relayFollowings(
existingFollowings = { elements: [], total: 0 },
{ readField }
) {
const newFollowingRef = cache.writeFragment({
id: `${data?.addRelay.__typename}:${data?.addRelay.id}`,
data: data?.addRelay,
fragment: gql`
fragment NewFollowing on Follower {
id
}
`,
});
if (
existingFollowings.elements.some(
(ref: Reference) =>
readField("id", ref) === data?.addRelay.id
)
) {
return existingFollowings;
}
return {
total: existingFollowings.total + 1,
elements: [newFollowingRef, ...existingFollowings.elements],
};
},
},
broadcast: false,
});
},
});
await this.$apollo.queries.relayFollowings.refetch();
this.newRelayAddress = "";
} catch (err) {
Snackbar.open({
@ -175,21 +263,35 @@ export default class Followings extends Mixins(RelayMixin) {
}
}
async removeRelays(): Promise<void> {
await this.checkedRows.forEach((row: IFollower) => {
this.removeRelay(
`${row.targetActor.preferredUsername}@${row.targetActor.domain}`
);
removeRelays(): void {
this.checkedRows.forEach((row: IFollower) => {
this.removeRelay(row);
});
}
async removeRelay(address: string): Promise<void> {
async removeRelay(follower: IFollower): Promise<void> {
const address = `${follower.targetActor.preferredUsername}@${follower.targetActor.domain}`;
try {
await this.$apollo.mutate({
await this.$apollo.mutate<{ removeRelay: IFollower }>({
mutation: REMOVE_RELAY,
variables: {
address,
},
update(cache: ApolloCache<{ removeRelay: IFollower }>) {
cache.modify({
fields: {
relayFollowings(existingFollowingRefs, { readField }) {
return {
total: existingFollowingRefs.total - 1,
elements: existingFollowingRefs.elements.filter(
(followingRef: Reference) =>
follower.id !== readField("id", followingRef)
),
};
},
},
});
},
});
await this.$apollo.queries.relayFollowings.refetch();
this.checkedRows = [];

View File

@ -1,37 +1,34 @@
<template>
<li :class="{ reply: comment.inReplyToComment }">
<article
class="media"
:class="{ selected: commentSelected }"
:id="commentId"
>
<li
:class="{
reply: comment.inReplyToComment,
announcement: comment.isAnnouncement,
selected: commentSelected,
}"
class="comment-element"
>
<article class="media" :id="commentId">
<popover-actor-card
class="media-left"
:actor="comment.actor"
:inline="true"
v-if="comment.actor"
>
<figure
class="image is-48x48"
class="image is-32x32 media-left"
v-if="!comment.deletedAt && comment.actor.avatar"
>
<img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
</figure>
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
<b-icon class="media-left" v-else icon="account-circle" />
</popover-actor-card>
<div v-else class="media-left">
<figure
class="image is-48x48"
class="image is-32x32"
v-if="!comment.deletedAt && comment.actor.avatar"
>
<img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
</figure>
<b-icon v-else size="is-large" icon="account-circle" />
<b-icon v-else icon="account-circle" />
</div>
<div class="media-content">
<div class="content">
@ -39,23 +36,23 @@
<strong :class="{ organizer: commentFromOrganizer }">{{
comment.actor.name
}}</strong>
<small>@{{ usernameWithDomain(comment.actor) }}</small>
<a class="comment-link has-text-grey" :href="commentURL">
<small>{{
formatDistanceToNow(new Date(comment.updatedAt), {
locale: $dateFnsLocale,
addSuffix: true,
})
}}</small>
</a>
<small>{{ usernameWithDomain(comment.actor) }}</small>
</span>
<a v-else class="comment-link has-text-grey" :href="commentURL">
<a v-else class="comment-link" :href="commentURL">
<span>{{ $t("[deleted]") }}</span>
</a>
<a class="comment-link" :href="commentURL">
<small>{{
formatDistanceToNow(new Date(comment.updatedAt), {
locale: $dateFnsLocale,
addSuffix: true,
})
}}</small>
</a>
<span class="icons" v-if="!comment.deletedAt">
<button
v-if="comment.actor.id === currentActor.id"
@click="$emit('delete-comment', comment)"
@click="deleteComment"
>
<b-icon icon="delete" size="is-small" aria-hidden="true" />
<span class="visually-hidden">{{ $t("Delete") }}</span>
@ -70,7 +67,8 @@
<div v-else>{{ $t("[This comment has been deleted]") }}</div>
<div class="load-replies" v-if="comment.totalReplies">
<p v-if="!showReplies" @click="fetchReplies">
<b-icon icon="chevron-down" /><span>{{
<b-icon icon="chevron-down" class="reply-btn" />
<span class="reply-btn">{{
$tc("View a reply", comment.totalReplies, {
totalReplies: comment.totalReplies,
})
@ -80,8 +78,8 @@
v-else-if="comment.totalReplies && showReplies"
@click="showReplies = false"
>
<b-icon icon="chevron-up" />
<span>{{ $t("Hide replies") }}</span>
<b-icon icon="chevron-up" class="reply-btn" />
<span class="reply-btn">{{ $t("Hide replies") }}</span>
</p>
</div>
</div>
@ -183,7 +181,6 @@ import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import { IPerson, usernameWithDomain } from "../../types/actor";
import { COMMENTS_THREADS, FETCH_THREAD_REPLIES } from "../../graphql/comment";
import { IEvent } from "../../types/event.model";
import ReportModal from "../Report/ReportModal.vue";
import { IReport } from "../../types/report.model";
@ -257,44 +254,20 @@ export default class Comment extends Vue {
this.$emit("create-comment", this.newComment);
this.newComment = new CommentModel();
this.replyTo = false;
this.showReplies = true;
}
async fetchReplies(): Promise<void> {
const parentId = this.comment.id;
const { data } = await this.$apollo.query<{ thread: IComment[] }>({
query: FETCH_THREAD_REPLIES,
variables: {
threadId: parentId,
},
});
if (!data) return;
const { thread } = data;
const eventData = this.$apollo.getClient().readQuery<{ event: IEvent }>({
query: COMMENTS_THREADS,
variables: {
eventUUID: this.event.uuid,
},
});
if (!eventData) return;
const { event } = eventData;
const { comments } = event;
const parentCommentIndex = comments.findIndex(
(oldComment) => oldComment.id === parentId
);
const parentComment = comments[parentCommentIndex];
if (!parentComment) return;
parentComment.replies = thread;
comments[parentCommentIndex] = parentComment;
event.comments = comments;
this.$apollo.getClient().writeQuery({
query: COMMENTS_THREADS,
data: { event },
});
deleteComment(): void {
this.$emit("delete-comment", this.comment);
this.showReplies = false;
}
fetchReplies(): void {
this.showReplies = true;
}
get commentSelected(): boolean {
return this.commentId === this.$route.hash;
return `#${this.commentId}` === this.$route.hash;
}
get commentFromOrganizer(): boolean {
@ -305,13 +278,13 @@ export default class Comment extends Vue {
get commentId(): string {
if (this.comment.originComment)
return `#comment-${this.comment.originComment.uuid}-${this.comment.uuid}`;
return `#comment-${this.comment.uuid}`;
return `comment-${this.comment.originComment.uuid}-${this.comment.uuid}`;
return `comment-${this.comment.uuid}`;
}
get commentURL(): string {
if (!this.comment.local && this.comment.url) return this.comment.url;
return this.commentId;
return `#${this.commentId}`;
}
reportModal(): void {
@ -394,8 +367,53 @@ form.reply {
}
}
.comment-link small:hover {
color: hsl(0, 0%, 21%);
a.comment-link {
text-decoration: none;
margin-left: 5px;
color: $text;
&:hover {
text-decoration: underline;
}
small {
&:hover {
color: hsl(0, 0%, 21%);
}
}
}
.comment-element {
padding: 0.25rem;
border-radius: 5px;
&.announcement {
background: $purple-2;
small {
color: hsl(0, 0%, 21%);
}
}
&.selected {
background-color: $violet-1;
color: $white;
.reply-btn,
small,
strong,
.icons button {
color: $white;
}
a.comment-link:hover {
text-decoration: underline;
text-decoration-color: $white;
small {
color: $purple-3;
}
}
}
.media-left {
margin-right: 0.5rem;
}
}
.root-comment .replies {
@ -422,6 +440,7 @@ form.reply {
}
.media .media-content {
overflow-x: initial;
.content .editor-line {
display: flex;
align-items: center;
@ -447,22 +466,18 @@ form.reply {
& > p > span {
font-weight: bold;
color: $primary;
color: $violet-2;
}
}
.level-item.reply-btn {
font-weight: bold;
color: $primary;
color: $violet-2;
}
article {
border-radius: 4px;
margin-bottom: 5px;
&.selected {
background-color: lighten($secondary, 30%);
}
}
.comment-replies {

View File

@ -17,26 +17,34 @@
</figure>
<div class="media-content">
<div class="field">
<p class="control">
<editor
ref="commenteditor"
mode="comment"
v-model="newComment.text"
/>
</p>
<p class="help is-danger" v-if="emptyCommentError">
{{ $t("Comment text can't be empty") }}
</p>
</div>
<div class="send-comment">
<b-button
native-type="submit"
type="is-primary"
class="comment-button-submit"
>{{ $t("Post a comment") }}</b-button
>
<div class="field">
<p class="control">
<editor
ref="commenteditor"
mode="comment"
v-model="newComment.text"
/>
</p>
<p class="help is-danger" v-if="emptyCommentError">
{{ $t("Comment text can't be empty") }}
</p>
</div>
<div class="field notify-participants" v-if="isEventOrganiser">
<b-switch v-model="newComment.isAnnouncement">{{
$t("Notify participants")
}}</b-switch>
</div>
</div>
</div>
<div class="send-comment">
<b-button
native-type="submit"
type="is-primary"
class="comment-button-submit"
icon-left="send"
:aria-label="$t('Post a comment')"
/>
</div>
</article>
</form>
<b-notification v-else-if="isConnected" :closable="false">{{
@ -66,7 +74,7 @@
@delete-comment="deleteComment"
/>
</transition-group>
<div class="no-comments" key="no-comments">
<div v-else class="no-comments" key="no-comments">
<span>{{ $t("No comments yet") }}</span>
</div>
</transition-group>
@ -82,30 +90,24 @@ import { CommentModel, IComment } from "../../types/comment.model";
import {
CREATE_COMMENT_FROM_EVENT,
DELETE_COMMENT,
COMMENTS_THREADS,
FETCH_THREAD_REPLIES,
COMMENTS_THREADS_WITH_REPLIES,
} from "../../graphql/comment";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import { IPerson } from "../../types/actor";
import { IEvent } from "../../types/event.model";
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
@Component({
apollo: {
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
currentActor: CURRENT_ACTOR_CLIENT,
comments: {
query: COMMENTS_THREADS,
query: COMMENTS_THREADS_WITH_REPLIES,
variables() {
return {
eventUUID: this.event.uuid,
};
},
update(data) {
return data.event.comments.map(
(comment: IComment) => new CommentModel(comment)
);
},
update: (data) => data.event.comments,
skip() {
return !this.event.uuid;
},
@ -156,24 +158,23 @@ export default class CommentTree extends Vue {
inReplyToCommentId: comment.inReplyToComment
? comment.inReplyToComment.id
: null,
isAnnouncement: comment.isAnnouncement,
},
update: (store, { data }) => {
update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => {
if (data == null) return;
const newComment = data.createComment;
// comments are attached to the event, so we can pass it to replies later
newComment.event = this.event;
const newComment = { ...data.createComment, event: this.event };
// we load all existing threads
const commentThreadsData = store.readQuery<{ event: IEvent }>({
query: COMMENTS_THREADS,
query: COMMENTS_THREADS_WITH_REPLIES,
variables: {
eventUUID: this.event.uuid,
},
});
if (!commentThreadsData) return;
const { event } = commentThreadsData;
const { comments: oldComments } = event;
const oldComments = [...event.comments];
// if it's no a root comment, we first need to find
// existing replies and add the new reply to it
@ -184,44 +185,25 @@ export default class CommentTree extends Vue {
);
const parentComment = oldComments[parentCommentIndex];
let oldReplyList: IComment[] = [];
try {
const threadData = store.readQuery<{ thread: IComment[] }>({
query: FETCH_THREAD_REPLIES,
variables: {
threadId: parentComment.id,
},
});
if (!threadData) return;
oldReplyList = threadData.thread;
} catch (e) {
// This simply means there's no loaded replies yet
} finally {
oldReplyList.push(newComment);
// save the updated list of replies (with the one we've just added)
store.writeQuery({
query: FETCH_THREAD_REPLIES,
data: { thread: oldReplyList },
variables: {
threadId: parentComment.id,
},
});
// replace the root comment with has the updated list of replies in the thread list
parentComment.replies = oldReplyList;
event.comments.splice(parentCommentIndex, 1, parentComment);
}
// replace the root comment with has the updated list of replies in the thread list
oldComments.splice(parentCommentIndex, 1, {
...parentComment,
replies: [...parentComment.replies, newComment],
});
} else {
// otherwise it's simply a new thread and we add it to the list
oldComments.push(newComment);
}
// finally we save the thread list
event.comments = oldComments;
store.writeQuery({
query: COMMENTS_THREADS,
data: { event },
query: COMMENTS_THREADS_WITH_REPLIES,
data: {
event: {
...event,
comments: oldComments,
},
},
variables: {
eventUUID: this.event.uuid,
},
@ -249,63 +231,66 @@ export default class CommentTree extends Vue {
variables: {
commentId: comment.id,
},
update: (store, { data }) => {
update: (store: ApolloCache<InMemoryCache>, { data }: FetchResult) => {
if (data == null) return;
const deletedCommentId = data.deleteComment.id;
const commentsData = store.readQuery<{ event: IEvent }>({
query: COMMENTS_THREADS,
query: COMMENTS_THREADS_WITH_REPLIES,
variables: {
eventUUID: this.event.uuid,
},
});
if (!commentsData) return;
const { event } = commentsData;
const { comments: oldComments } = event;
let updatedComments: IComment[] = [...event.comments];
if (comment.originComment) {
// we have deleted a reply to a thread
const localData = store.readQuery<{ thread: IComment[] }>({
query: FETCH_THREAD_REPLIES,
variables: {
threadId: comment.originComment.id,
},
});
if (!localData) return;
const { thread: oldReplyList } = localData;
const replies = oldReplyList.filter(
(reply) => reply.id !== deletedCommentId
);
store.writeQuery({
query: FETCH_THREAD_REPLIES,
variables: {
threadId: comment.originComment.id,
},
data: { thread: replies },
});
const { originComment } = comment;
const parentCommentIndex = oldComments.findIndex(
const parentCommentIndex = updatedComments.findIndex(
(oldComment) => oldComment.id === originComment.id
);
const parentComment = oldComments[parentCommentIndex];
parentComment.replies = replies;
parentComment.totalReplies -= 1;
oldComments.splice(parentCommentIndex, 1, parentComment);
event.comments = oldComments;
const parentComment = updatedComments[parentCommentIndex];
const updatedReplies = parentComment.replies.map((reply) => {
if (reply.id === deletedCommentId) {
return {
...reply,
deletedAt: new Date().toString(),
};
}
return reply;
});
updatedComments.splice(parentCommentIndex, 1, {
...parentComment,
replies: updatedReplies,
totalReplies: parentComment.totalReplies - 1,
});
console.log("updatedComments", updatedComments);
} else {
// we have deleted a thread itself
event.comments = oldComments.filter(
(reply) => reply.id !== deletedCommentId
);
updatedComments = updatedComments.map((reply) => {
if (reply.id === deletedCommentId) {
return {
...reply,
deletedAt: new Date().toString(),
};
}
return reply;
});
}
store.writeQuery({
query: COMMENTS_THREADS,
query: COMMENTS_THREADS_WITH_REPLIES,
variables: {
eventUUID: this.event.uuid,
},
data: { event },
data: {
event: {
...event,
comments: updatedComments,
},
},
});
},
});
@ -322,7 +307,18 @@ export default class CommentTree extends Vue {
return this.comments
.filter((comment) => comment.inReplyToComment == null)
.sort((a, b) => {
if (a.updatedAt && b.updatedAt) {
if (a.isAnnouncement !== b.isAnnouncement) {
return (
(b.isAnnouncement === true ? 1 : 0) -
(a.isAnnouncement === true ? 1 : 0)
);
}
if (a.publishedAt && b.publishedAt) {
return (
new Date(b.publishedAt).getTime() -
new Date(a.publishedAt).getTime()
);
} else if (a.updatedAt && b.updatedAt) {
return (
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
);
@ -376,6 +372,10 @@ form.new-comment {
flex: 1;
padding-right: 10px;
margin-bottom: 0;
&.notify-participants {
margin-top: 0.5rem;
}
}
}
}

View File

@ -1,14 +1,20 @@
<template>
<article class="comment">
<div class="avatar">
<figure class="image is-48x48" v-if="comment.actor.avatar">
<figure
class="image is-48x48"
v-if="comment.actor && comment.actor.avatar"
>
<img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
</figure>
<b-icon v-else size="is-large" icon="account-circle" />
</div>
<div class="body">
<div class="meta">
<span class="first-line name" v-if="!comment.deletedAt">
<span
class="first-line name"
v-if="comment.actor && !comment.deletedAt"
>
<strong>{{ comment.actor.name }}</strong>
<small>@{{ usernameWithDomain(comment.actor) }}</small>
</span>
@ -17,7 +23,11 @@
</span>
<span
class="icons"
v-if="!comment.deletedAt && comment.actor.id === currentActor.id"
v-if="
comment.actor &&
!comment.deletedAt &&
comment.actor.id === currentActor.id
"
>
<b-dropdown aria-role="list">
<b-icon slot="trigger" role="button" icon="dots-horizontal" />
@ -57,7 +67,12 @@
<div v-if="!editMode && !comment.deletedAt" class="text-wrapper">
<div class="description-content" v-html="comment.text"></div>
<p
v-if="comment.insertedAt.getTime() !== comment.updatedAt.getTime()"
v-if="
comment.insertedAt &&
comment.updatedAt &&
new Date(comment.insertedAt).getTime() !==
new Date(comment.updatedAt).getTime()
"
:title="comment.updatedAt | formatDateTimeString"
>
{{

View File

@ -24,7 +24,10 @@
<div class="title-info-wrapper">
<div class="title-and-date">
<p class="discussion-minimalist-title">{{ discussion.title }}</p>
<span :title="actualDate | formatDateTimeString">
<span
class="has-text-grey-dark"
:title="actualDate | formatDateTimeString"
>
{{
formatDistanceToNowStrict(new Date(actualDate), {
locale: $dateFnsLocale,
@ -32,10 +35,13 @@
}}</span
>
</div>
<div class="has-text-grey" v-if="!discussion.lastComment.deletedAt">
<div
class="ellipsis has-text-grey-dark"
v-if="!discussion.lastComment.deletedAt"
>
{{ htmlTextEllipsis }}
</div>
<div v-else class="has-text-grey">
<div v-else class="has-text-grey-dark">
{{ $t("[This comment has been deleted]") }}
</div>
</div>
@ -98,19 +104,19 @@ export default class DiscussionListItem extends Vue {
.discussion-minimalist-title {
color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica,
Arial, serif;
font-size: 1.25rem;
font-weight: 700;
font-family: Roboto, Helvetica, Arial, serif;
font-size: 19px;
font-weight: 600;
flex: 1;
}
}
div.has-text-grey {
div.ellipsis {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
font-size: 15px;
}
}
}

View File

@ -179,7 +179,7 @@
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { Editor, EditorContent, BubbleMenu } from "@tiptap/vue-2";
import { defaultExtensions } from "@tiptap/starter-kit";
import StarterKit from "@tiptap/starter-kit";
import Document from "@tiptap/extension-document";
import Paragraph from "@tiptap/extension-paragraph";
import Text from "@tiptap/extension-text";
@ -241,6 +241,7 @@ export default class EditorComponent extends Vue {
mounted(): void {
this.editor = new Editor({
extensions: [
StarterKit,
Document,
Paragraph,
Text,
@ -253,8 +254,8 @@ export default class EditorComponent extends Vue {
CharacterCount.configure({
limit: this.maxSize,
}),
...defaultExtensions(),
],
injectCSS: false,
content: this.value,
onUpdate: () => {
this.$emit("input", this.editor?.getHTML());

View File

@ -1,10 +1,10 @@
import { UPLOAD_MEDIA } from "@/graphql/upload";
import apolloProvider from "@/vue-apollo";
import ApolloClient from "apollo-client";
import { NormalizedCacheObject } from "apollo-cache-inmemory";
import { ApolloClient } from "@apollo/client/core/ApolloClient";
import { Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import Image from "@tiptap/extension-image";
import { NormalizedCacheObject } from "@apollo/client/cache";
/* eslint-disable class-methods-use-this */

View File

@ -2,11 +2,11 @@ import { SEARCH_PERSONS } from "@/graphql/search";
import { VueRenderer } from "@tiptap/vue-2";
import tippy from "tippy.js";
import MentionList from "./MentionList.vue";
import ApolloClient from "apollo-client";
import { NormalizedCacheObject } from "apollo-cache-inmemory";
import { ApolloClient } from "@apollo/client/core/ApolloClient";
import apolloProvider from "@/vue-apollo";
import { IPerson } from "@/types/actor";
import pDebounce from "p-debounce";
import { NormalizedCacheObject } from "@apollo/client/cache/inmemory/types";
const client = apolloProvider.defaultClient as ApolloClient<NormalizedCacheObject>;

View File

@ -54,7 +54,8 @@
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { LatLng } from "leaflet";
import { debounce, DebouncedFunc } from "lodash";
import debounce from "lodash/debounce";
import { DebouncedFunc } from "lodash";
import { Address, IAddress } from "../../types/address.model";
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
import { CONFIG } from "../../graphql/config";

View File

@ -12,9 +12,17 @@
</docs>
<template>
<time class="datetime-container" :datetime="dateObj.getUTCSeconds()">
<span class="month">{{ month }}</span>
<span class="day">{{ day }}</span>
<time
class="datetime-container"
:class="{ small }"
:datetime="dateObj.getUTCSeconds()"
:style="`--small: ${smallStyle}`"
>
<div class="datetime-container-header" />
<div class="datetime-container-content">
<span class="day">{{ day }}</span>
<span class="month">{{ month }}</span>
</div>
</time>
</template>
<script lang="ts">
@ -26,6 +34,7 @@ export default class DateCalendarIcon extends Vue {
* `date` can be a string or an actual date object.
*/
@Prop({ required: true }) date!: string;
@Prop({ required: false, default: false }) small!: boolean;
get dateObj(): Date {
return new Date(this.$props.date);
@ -38,6 +47,9 @@ export default class DateCalendarIcon extends Vue {
get day(): string {
return this.dateObj.toLocaleString(undefined, { day: "numeric" });
}
get smallStyle(): string {
return this.small ? "1.2" : "2";
}
}
</script>
@ -52,13 +64,28 @@ time.datetime-container {
width: 50px;
padding: 8px;
text-align: center;
overflow-y: hidden;
overflow-x: hidden;
align-items: stretch;
width: calc(40px * var(--small));
box-shadow: 0 0 12px rgba(0, 0, 0, 0.2);
height: calc(40px * var(--small));
background: #fff;
.datetime-container-header {
height: calc(10px * var(--small));
background: #f3425f;
}
.datetime-container-content {
height: calc(30px * var(--small));
}
span {
display: block;
font-weight: 600;
color: $violet-3;
&.month {
color: $danger;
padding: 2px 0;
font-size: 12px;
line-height: 12px;
@ -66,9 +93,8 @@ time.datetime-container {
}
&.day {
color: $violet-3;
font-size: 20px;
line-height: 20px;
font-size: calc(1rem * var(--small));
line-height: calc(1rem * var(--small));
}
}
}

View File

@ -0,0 +1,33 @@
<template>
<div class="banner-container">
<lazy-image-wrapper :picture="picture" />
</div>
</template>
<script lang="ts">
import { IMedia } from "@/types/media.model";
import { Component, Prop, Vue } from "vue-property-decorator";
import LazyImageWrapper from "../Image/LazyImageWrapper.vue";
@Component({
components: {
LazyImageWrapper,
},
})
export default class EventBanner extends Vue {
@Prop({ required: true, default: null })
picture!: IMedia | null;
}
</script>
<style lang="scss" scoped>
.banner-container {
display: flex;
justify-content: center;
height: 30vh;
}
::v-deep img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: 50% 50%;
}
</style>

View File

@ -4,12 +4,11 @@
:to="{ name: 'Event', params: { uuid: event.uuid } }"
>
<div class="card-image">
<figure
class="image is-16by9"
:style="`background-image: url('${
event.picture ? event.picture.url : '/img/mobilizon_default_card.png'
}')`"
>
<figure class="image is-16by9">
<lazy-image-wrapper
:picture="event.picture"
style="height: 100%; position: absolute; top: 0; left: 0; width: 100%"
/>
<div
class="tag-container"
v-if="event.tags || event.status !== EventStatus.CONFIRMED"
@ -34,6 +33,7 @@
<div class="media">
<div class="media-left">
<date-calendar-icon
:small="true"
v-if="!mergedOptions.hideDate"
:date="event.beginsOn"
/>
@ -103,6 +103,7 @@
import { IEvent, IEventCardOptions } from "@/types/event.model";
import { Component, Prop, Vue } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
import { Actor, Person } from "@/types/actor";
import { EventStatus, ParticipantRole } from "@/types/enums";
import RouteName from "../../router/name";
@ -110,6 +111,7 @@ import RouteName from "../../router/name";
@Component({
components: {
DateCalendarIcon,
LazyImageWrapper,
},
})
export default class EventCard extends Vue {
@ -220,6 +222,22 @@ a.card {
.card-content {
padding: 0.5rem;
& > .media {
position: relative;
display: flex;
flex-direction: column;
& > .media-left {
margin-top: -15px;
height: 0;
display: flex;
align-items: flex-end;
align-self: flex-start;
margin-bottom: 15px;
margin-left: 0rem;
}
}
.event-title {
font-size: 1.2rem;
line-height: 1.25rem;

View File

@ -2,53 +2,66 @@
<article class="box">
<div class="identity-header">
<figure class="image is-24x24" v-if="participation.actor.avatar">
<img class="is-rounded" :src="participation.actor.avatar.url" alt="" />
<img
class="is-rounded"
:src="participation.actor.avatar.url"
alt=""
height="24"
width="24"
/>
</figure>
{{ displayNameAndUsername(participation.actor) }}
</div>
<div class="list-card">
<div class="content">
<div class="title-wrapper">
<div class="date-component">
<date-calendar-icon :date="participation.event.beginsOn" />
<div class="date-component">
<date-calendar-icon
:date="participation.event.beginsOn"
:small="true"
/>
</div>
<div class="content-and-actions">
<div class="list-card-content">
<div class="title-wrapper">
<router-link
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
>
<h3 class="title">{{ participation.event.title }}</h3>
</router-link>
</div>
<router-link
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
>
<h3 class="title">{{ participation.event.title }}</h3>
</router-link>
</div>
<div class="participation-actor has-text-grey">
<span>
<b-icon
icon="earth"
v-if="participation.event.visibility === EventVisibility.PUBLIC"
/>
<b-icon
icon="link"
v-else-if="
participation.event.visibility === EventVisibility.UNLISTED
<div class="participation-actor">
<span>
<b-icon
icon="earth"
v-if="participation.event.visibility === EventVisibility.PUBLIC"
/>
<b-icon
icon="link"
v-else-if="
participation.event.visibility === EventVisibility.UNLISTED
"
/>
<b-icon
icon="lock"
v-else-if="
participation.event.visibility === EventVisibility.PRIVATE
"
/>
</span>
<span
v-if="
participation.event.physicalAddress &&
participation.event.physicalAddress.locality
"
/>
<b-icon
icon="lock"
v-else-if="
participation.event.visibility === EventVisibility.PRIVATE
"
/>
</span>
<span
v-if="
participation.event.physicalAddress &&
participation.event.physicalAddress.locality
"
>{{ participation.event.physicalAddress.locality }} -</span
>
<span>
<i18n tag="span" path="Organized by {name}">
>{{ participation.event.physicalAddress.locality }} -</span
>
<i18n
tag="span"
path="Organized by {name}"
v-if="organizerActor.id !== currentActor.id"
>
<popover-actor-card
slot="name"
:actor="organizerActor"
@ -57,154 +70,157 @@
{{ organizerActor.displayName() }}
</popover-actor-card>
</i18n>
</span>
</div>
<div>
<span
class="participant-stats"
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
<span v-else>{{ $t("Organized by you") }}</span>
</div>
<div>
<span
v-if="participation.event.options.maximumAttendeeCapacity !== 0"
class="participant-stats"
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
{{
$tc(
"{available}/{capacity} available places",
participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant,
{
available:
participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant,
capacity:
participation.event.options.maximumAttendeeCapacity,
}
)
}}
</span>
<span v-else>
{{
$tc(
"{count} participants",
participation.event.participantStats.participant,
{
count: participation.event.participantStats.participant,
}
)
}}
</span>
<span v-if="participation.event.participantStats.notApproved > 0">
<b-button
type="is-text"
@click="
gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS,
query: { role: ParticipantRole.NOT_APPROVED },
params: { eventId: participation.event.uuid },
})
"
<span
v-if="participation.event.options.maximumAttendeeCapacity !== 0"
>
{{
$tc(
"{count} requests waiting",
participation.event.participantStats.notApproved,
{ count: participation.event.participantStats.notApproved }
"{available}/{capacity} available places",
participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant,
{
available:
participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant,
capacity:
participation.event.options.maximumAttendeeCapacity,
}
)
}}
</b-button>
</span>
<span v-else>
{{
$tc(
"{count} participants",
participation.event.participantStats.participant,
{
count: participation.event.participantStats.participant,
}
)
}}
</span>
<span v-if="participation.event.participantStats.notApproved > 0">
<b-button
type="is-text"
@click="
gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS,
query: { role: ParticipantRole.NOT_APPROVED },
params: { eventId: participation.event.uuid },
})
"
>
{{
$tc(
"{count} requests waiting",
participation.event.participantStats.notApproved,
{
count: participation.event.participantStats.notApproved,
}
)
}}
</b-button>
</span>
</span>
</span>
</div>
</div>
</div>
<div class="actions">
<b-dropdown aria-role="list" position="is-bottom-left">
<b-button slot="trigger" role="button" icon-right="dots-horizontal">
{{ $t("Actions") }}
</b-button>
<div class="actions">
<b-dropdown aria-role="list" position="is-bottom-left">
<b-button slot="trigger" role="button" icon-right="dots-horizontal">
{{ $t("Actions") }}
</b-button>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.EDIT_EVENT,
params: { eventId: participation.event.uuid },
})
"
>
<b-icon icon="pencil" />
{{ $t("Edit") }}
</b-dropdown-item>
<b-dropdown-item
v-if="participation.role === ParticipantRole.CREATOR"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.DUPLICATE_EVENT,
params: { eventId: participation.event.uuid },
})
"
>
<b-icon icon="content-duplicate" />
{{ $t("Duplicate") }}
</b-dropdown-item>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="openDeleteEventModalWrapper"
>
<b-icon icon="delete" />
{{ $t("Delete") }}
</b-dropdown-item>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS,
params: { eventId: participation.event.uuid },
})
"
>
<b-icon icon="account-multiple-plus" />
{{ $t("Manage participations") }}
</b-dropdown-item>
<b-dropdown-item aria-role="listitem" has-link>
<router-link
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.EDIT_EVENT,
params: { eventId: participation.event.uuid },
})
"
>
<b-icon icon="view-compact" />
{{ $t("View event page") }}
</router-link>
</b-dropdown-item>
</b-dropdown>
<b-icon icon="pencil" />
{{ $t("Edit") }}
</b-dropdown-item>
<b-dropdown-item
v-if="participation.role === ParticipantRole.CREATOR"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.DUPLICATE_EVENT,
params: { eventId: participation.event.uuid },
})
"
>
<b-icon icon="content-duplicate" />
{{ $t("Duplicate") }}
</b-dropdown-item>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="openDeleteEventModalWrapper"
>
<b-icon icon="delete" />
{{ $t("Delete") }}
</b-dropdown-item>
<b-dropdown-item
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
aria-role="listitem"
@click="
gotToWithCheck(participation, {
name: RouteName.PARTICIPATIONS,
params: { eventId: participation.event.uuid },
})
"
>
<b-icon icon="account-multiple-plus" />
{{ $t("Manage participations") }}
</b-dropdown-item>
<b-dropdown-item aria-role="listitem" has-link>
<router-link
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
>
<b-icon icon="view-compact" />
{{ $t("View event page") }}
</router-link>
</b-dropdown-item>
</b-dropdown>
</div>
</div>
</div>
</article>
@ -343,79 +359,72 @@ article.box {
.list-card {
display: flex;
align-items: center;
padding: 0 1.5em;
padding: 0 6px;
position: relative;
flex-direction: column;
.actions {
padding-right: 7.5px;
cursor: pointer;
ul li {
margin: 0 auto;
.is-link {
cursor: pointer;
}
.button.is-text {
text-decoration: none;
::v-deep span:first-child i.mdi::before {
font-size: 24px !important;
}
::v-deep span:last-child {
padding-left: 4px;
}
&:hover {
background: #f5f5f5;
}
}
* {
font-size: 0.8rem;
color: $background-color;
}
}
div.date-component {
align-self: flex-start;
padding: 5px;
position: absolute;
top: 0;
left: 0;
margin-top: 1px;
height: 0;
display: flex;
align-items: flex-end;
margin-bottom: 15px;
margin-left: 0rem;
}
div.content {
flex: 1;
padding: 5px;
.content-and-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
padding-bottom: 1rem;
.participation-actor span,
.participant-stats span {
padding: 0 5px;
button {
height: auto;
padding-top: 0;
}
.actions {
padding-right: 7.5px;
cursor: pointer;
}
div.title-wrapper {
display: flex;
align-items: center;
div.list-card-content {
flex: 1;
padding: 5px;
min-width: 350px;
div.date-component {
flex: 0;
margin-right: 16px;
.participation-actor span,
.participant-stats span {
padding: 0 5px;
button {
height: auto;
padding-top: 0;
}
}
a {
text-decoration: none;
}
div.title-wrapper {
display: flex;
align-items: center;
padding-top: 5px;
.title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 400;
line-height: 1em;
font-size: 1.6em;
padding-bottom: 5px;
margin: auto 0;
a {
text-decoration: none;
padding-bottom: 5px;
}
.title {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 400;
line-height: 1em;
font-size: 1.4em;
padding-bottom: 5px;
margin: auto 0;
}
}
}
}
@ -425,6 +434,7 @@ article.box {
background: $yellow-2;
display: flex;
padding: 5px;
padding-left: calc(48px + 15px);
figure {
padding-right: 3px;

View File

@ -4,7 +4,7 @@
<div class="content column">
<div class="title-wrapper">
<div class="date-component">
<date-calendar-icon :date="event.beginsOn" />
<date-calendar-icon :date="event.beginsOn" :small="true" />
</div>
<router-link
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"

View File

@ -23,6 +23,7 @@ export default class EventMetadataBlock extends Vue {
h2 {
font-size: 1.8rem;
font-weight: 500;
color: $violet;
}
div.eventMetadataBlock {
@ -34,7 +35,7 @@ div.eventMetadataBlock {
overflow: hidden;
&.padding-left {
padding-left: 20px;
padding: 0 20px;
}
}
}

View File

@ -3,7 +3,11 @@
class="event-minimalist-card-wrapper"
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
>
<date-calendar-icon class="calendar-icon" :date="event.beginsOn" />
<date-calendar-icon
class="calendar-icon"
:date="event.beginsOn"
:small="true"
/>
<div class="title-info-wrapper">
<p class="event-minimalist-title">{{ event.title }}</p>
<p v-if="event.physicalAddress" class="has-text-grey">

View File

@ -111,7 +111,8 @@
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { LatLng } from "leaflet";
import { debounce, DebouncedFunc } from "lodash";
import debounce from "lodash/debounce";
import { DebouncedFunc } from "lodash";
import { Address, IAddress } from "../../types/address.model";
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
import { CONFIG } from "../../graphql/config";

View File

@ -66,7 +66,9 @@ export default class OrganizerPicker extends Vue {
return this.value;
}
if (this.currentActor) {
return this.currentActor;
return this.identities.find(
(identity) => identity.id === this.currentActor.id
);
}
return undefined;
}

View File

@ -110,6 +110,7 @@ import { IActor, IGroup, IPerson, usernameWithDomain } from "../../types/actor";
import OrganizerPicker from "./OrganizerPicker.vue";
import {
CURRENT_ACTOR_CLIENT,
IDENTITIES,
LOGGED_USER_MEMBERSHIPS,
} from "../../graphql/actor";
import { Paginate } from "../../types/paginate";
@ -152,6 +153,7 @@ const MEMBER_ROLES = [
},
update: (data) => data.loggedUser.memberships,
},
identities: IDENTITIES,
},
})
export default class OrganizerPickerWrapper extends Vue {
@ -161,6 +163,8 @@ export default class OrganizerPickerWrapper extends Vue {
currentActor!: IPerson;
identities!: IPerson[];
isComponentModalActive = false;
@Prop({ type: Array, required: false, default: () => [] })
@ -186,7 +190,6 @@ export default class OrganizerPickerWrapper extends Vue {
setInitialActor(): void {
if (this.$route.query?.actorId) {
const actorId = this.$route.query?.actorId as string;
this.$router.replace({ query: undefined });
const actor = this.userMemberships.elements.find(
({ parent: { id }, role }) =>
actorId === id && MEMBER_ROLES.includes(role)
@ -200,7 +203,9 @@ export default class OrganizerPickerWrapper extends Vue {
return this.value;
}
if (this.currentActor) {
return this.currentActor;
return this.identities.find(
(identity) => identity.id === this.currentActor.id
);
}
return undefined;
}

View File

@ -30,6 +30,6 @@ export default class RecentEventCardWrapper extends Vue {
</script>
<style lang="scss" scoped>
p.time {
color: $orange-2;
color: $violet-3;
}
</style>

View File

@ -27,8 +27,13 @@
<small class="maximumNumberOfPlacesWarning" v-if="!eventCapacityOK">
{{ $t("All the places have already been taken") }}
</small>
<b-field>
<b-input ref="eventURLInput" :value="event.url" expanded />
<b-field :label="$t('Event URL')" label-for="event-url-text">
<b-input
id="event-url-text"
ref="eventURLInput"
:value="event.url"
expanded
/>
<p class="control">
<b-tooltip
:label="$t('URL copied to clipboard')"
@ -43,20 +48,54 @@
native-type="button"
@click="copyURL"
@keyup.enter="copyURL"
:title="$t('Copy URL to clipboard')"
/>
</b-tooltip>
</p>
</b-field>
<div>
<!-- <b-icon icon="mastodon" size="is-large" type="is-primary" />-->
<a :href="twitterShareUrl" target="_blank" rel="nofollow noopener"
<a
:href="twitterShareUrl"
target="_blank"
rel="nofollow noopener"
title="Twitter"
><b-icon icon="twitter" size="is-large" type="is-primary"
/></a>
<a :href="facebookShareUrl" target="_blank" rel="nofollow noopener"
<a
:href="mastodonShareUrl"
class="mastodon"
target="_blank"
rel="nofollow noopener"
title="Mastodon"
>
<mastodon-logo />
</a>
<a
:href="facebookShareUrl"
target="_blank"
rel="nofollow noopener"
title="Facebook"
><b-icon icon="facebook" size="is-large" type="is-primary"
/></a>
<a :href="linkedInShareUrl" target="_blank" rel="nofollow noopener"
<a
:href="whatsAppShareUrl"
target="_blank"
rel="nofollow noopener"
title="WhatsApp"
><b-icon icon="whatsapp" size="is-large" type="is-primary"
/></a>
<a
:href="telegramShareUrl"
target="_blank"
rel="nofollow noopener"
title="Telegram"
><b-icon icon="telegram" size="is-large" type="is-primary"
/></a>
<a
:href="linkedInShareUrl"
target="_blank"
rel="nofollow noopener"
title="LinkedIn"
><b-icon icon="linkedin" size="is-large" type="is-primary"
/></a>
<a
@ -64,12 +103,15 @@
class="diaspora"
target="_blank"
rel="nofollow noopener"
title="Diaspora"
>
<span data-v-5e15e80a="" class="icon has-text-primary is-large">
<DiasporaLogo alt="diaspora-logo" />
</span>
<diaspora-logo />
</a>
<a :href="emailShareUrl" target="_blank" rel="nofollow noopener"
<a
:href="emailShareUrl"
target="_blank"
rel="nofollow noopener"
title="Email"
><b-icon icon="email" size="is-large" type="is-primary"
/></a>
</div>
@ -82,13 +124,13 @@
import { Component, Prop, Vue, Ref } from "vue-property-decorator";
import { EventStatus, EventVisibility } from "@/types/enums";
import { IEvent } from "../../types/event.model";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import DiasporaLogo from "../../assets/diaspora-icon.svg?inline";
import DiasporaLogo from "../Share/DiasporaLogo.vue";
import MastodonLogo from "../Share/MastodonLogo.vue";
@Component({
components: {
DiasporaLogo,
MastodonLogo,
},
})
export default class ShareEventModal extends Vue {
@ -123,6 +165,16 @@ export default class ShareEventModal extends Vue {
)}&title=${this.event.title}`;
}
get whatsAppShareUrl(): string {
return `https://wa.me/?text=${encodeURIComponent(this.basicTextToEncode)}`;
}
get telegramShareUrl(): string {
return `https://t.me/share/url?url=${encodeURIComponent(
this.event.url
)}&text=${encodeURIComponent(this.event.title)}`;
}
get emailShareUrl(): string {
return `mailto:?to=&body=${this.event.url}&subject=${this.event.title}`;
}
@ -133,6 +185,16 @@ export default class ShareEventModal extends Vue {
)}&url=${encodeURIComponent(this.event.url)}`;
}
get mastodonShareUrl(): string {
return `https://toot.karamoff.dev/?text=${encodeURIComponent(
this.basicTextToEncode
)}`;
}
get basicTextToEncode(): string {
return `${this.event.title}\r\n${this.event.url}`;
}
copyURL(): void {
this.eventURLInput.$refs.input.select();
document.execCommand("copy");
@ -144,8 +206,10 @@ export default class ShareEventModal extends Vue {
}
</script>
<style lang="scss" scoped>
.diaspora span svg {
height: 2rem;
width: 2rem;
.diaspora,
.mastodon {
::v-deep span svg {
width: 2.25rem;
}
}
</style>

View File

@ -28,7 +28,8 @@
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { get, differenceBy } from "lodash";
import get from "lodash/get";
import differenceBy from "lodash/differenceBy";
import { ITag } from "../../types/tag.model";
@Component({

View File

@ -123,12 +123,13 @@ footer.footer {
flex: 1;
max-width: 40rem;
@include mobile {
max-width: 400px;
max-width: 100%;
}
}
div.content {
flex: 1;
padding-top: 10px;
}
ul {
@ -139,6 +140,7 @@ footer.footer {
li {
display: inline-flex;
margin: auto 5px;
padding: 2px 0;
a {
font-size: 1.1rem;
}
@ -151,9 +153,12 @@ footer.footer {
text-decoration-color: $secondary;
}
::v-deep span.select select {
background: $background-color;
color: $white;
::v-deep span.select {
select,
option {
background: $background-color;
color: $white;
}
}
}
</style>

View File

@ -97,6 +97,12 @@ export default class GroupMemberCard extends Vue {
& > div:last-child {
cursor: pointer;
}
.media-content {
::v-deep .tags {
margin-bottom: 0;
}
}
}
.identity-header {

View File

@ -49,26 +49,27 @@ section {
.main-slot {
min-height: 5rem;
padding: 5px;
padding: 2px 5px;
flex: 1;
}
}
div.group-section-title {
--title-color: $violet-2;
display: flex;
align-items: stretch;
background: $secondary;
color: #3a384c;
color: var(--title-color);
&.privateSection {
color: $violet-2;
background: $purple-2;
color: $purple-3;
background: $violet-2;
}
::v-deep & > a {
align-self: center;
margin-right: 5px;
color: $orange-3;
color: var(--title-color);
}
h2 {

View File

@ -30,6 +30,17 @@ import { IGroup } from "@/types/actor";
},
},
},
metaInfo() {
return {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
title: this.$t("Join group {group}", {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
group: this.groupTitle,
}) as string,
};
},
})
export default class JoinGroupWithAccount extends Vue {
@Prop({ type: String, required: true }) preferredUsername!: string;
@ -40,6 +51,10 @@ export default class JoinGroupWithAccount extends Vue {
return this.group?.url;
}
get groupTitle(): undefined | string {
return this.group?.name || this.group?.preferredUsername;
}
sentence = this.$t(
"We will redirect you to your instance in order to interact with this group"
);

View File

@ -0,0 +1,202 @@
<template>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">{{ $t("Share this group") }}</p>
</header>
<section class="modal-card-body is-flex" v-if="group">
<div class="container has-text-centered">
<b-notification
type="is-warning"
v-if="group.visibility !== GroupVisibility.PUBLIC"
:closable="false"
>
{{
$t(
"This group is accessible only through it's link. Be careful where you post this link."
)
}}
</b-notification>
<b-field :label="$t('Group URL')" label-for="group-url-text">
<b-input
id="group-url-text"
ref="groupURLInput"
:value="group.url"
expanded
/>
<p class="control">
<b-tooltip
:label="$t('URL copied to clipboard')"
:active="showCopiedTooltip"
always
type="is-success"
position="is-left"
>
<b-button
type="is-primary"
icon-right="content-paste"
native-type="button"
@click="copyURL"
@keyup.enter="copyURL"
:title="$t('Copy URL to clipboard')"
/>
</b-tooltip>
</p>
</b-field>
<div>
<a
:href="twitterShareUrl"
target="_blank"
rel="nofollow noopener"
title="Twitter"
><b-icon icon="twitter" size="is-large" type="is-primary"
/></a>
<a
:href="mastodonShareUrl"
class="mastodon"
target="_blank"
rel="nofollow noopener"
title="Mastodon"
>
<mastodon-logo />
</a>
<a
:href="facebookShareUrl"
target="_blank"
rel="nofollow noopener"
title="Facebook"
><b-icon icon="facebook" size="is-large" type="is-primary"
/></a>
<a
:href="linkedInShareUrl"
target="_blank"
rel="nofollow noopener"
title="LinkedIn"
><b-icon icon="linkedin" size="is-large" type="is-primary"
/></a>
<a
:href="whatsAppShareUrl"
target="_blank"
rel="nofollow noopener"
title="WhatsApp"
><b-icon icon="whatsapp" size="is-large" type="is-primary"
/></a>
<a
:href="telegramShareUrl"
target="_blank"
rel="nofollow noopener"
title="Telegram"
><b-icon icon="telegram" size="is-large" type="is-primary"
/></a>
<a
title="Diaspora"
:href="diasporaShareUrl"
class="diaspora"
target="_blank"
rel="nofollow noopener"
>
<diaspora-logo />
</a>
<a
:href="emailShareUrl"
target="_blank"
rel="nofollow noopener"
title="Email"
><b-icon icon="email" size="is-large" type="is-primary"
/></a>
</div>
</div>
</section>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Ref } from "vue-property-decorator";
import { GroupVisibility } from "@/types/enums";
import DiasporaLogo from "../Share/DiasporaLogo.vue";
import MastodonLogo from "../Share/MastodonLogo.vue";
import { displayName, IGroup } from "@/types/actor";
@Component({
components: {
DiasporaLogo,
MastodonLogo,
},
})
export default class ShareGroupModal extends Vue {
@Prop({ type: Object, required: true }) group!: IGroup;
@Ref("groupURLInput") readonly groupURLInput!: any;
GroupVisibility = GroupVisibility;
showCopiedTooltip = false;
get twitterShareUrl(): string {
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(
this.group.url
)}&text=${displayName(this.group)}`;
}
get facebookShareUrl(): string {
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
this.group.url
)}`;
}
get linkedInShareUrl(): string {
return `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(
this.group.url
)}&title=${displayName(this.group)}`;
}
get whatsAppShareUrl(): string {
return `https://wa.me/?text=${encodeURIComponent(this.basicTextToEncode)}`;
}
get telegramShareUrl(): string {
return `https://t.me/share/url?url=${encodeURIComponent(
this.group.url
)}&text=${encodeURIComponent(displayName(this.group))}`;
}
get emailShareUrl(): string {
return `mailto:?to=&body=${this.group.url}&subject=${displayName(
this.group
)}`;
}
get diasporaShareUrl(): string {
return `https://share.diasporafoundation.org/?title=${encodeURIComponent(
displayName(this.group)
)}&url=${encodeURIComponent(this.group.url)}`;
}
get mastodonShareUrl(): string {
return `https://toot.karamoff.dev/?text=${encodeURIComponent(
this.basicTextToEncode
)}`;
}
get basicTextToEncode(): string {
return `${displayName(this.group)}\r\n${this.group.url}`;
}
copyURL(): void {
this.groupURLInput.$refs.input.select();
document.execCommand("copy");
this.showCopiedTooltip = true;
setTimeout(() => {
this.showCopiedTooltip = false;
}, 2000);
}
}
</script>
<style lang="scss" scoped>
.diaspora,
.mastodon {
::v-deep span svg {
width: 2.25rem;
}
}
</style>

View File

@ -0,0 +1,34 @@
<template>
<canvas ref="canvas" width="32" height="32" />
</template>
<script lang="ts">
import { decode } from "blurhash";
import { Component, Prop, Ref, Vue } from "vue-property-decorator";
@Component
export default class extends Vue {
@Prop({ type: String, required: true }) hash!: string;
@Prop({ type: Number, default: 1 }) aspectRatio!: string;
@Ref("canvas") readonly canvas!: any;
mounted(): void {
const pixels = decode(this.hash, 32, 32);
const imageData = new ImageData(pixels, 32, 32);
const context = this.canvas.getContext("2d");
context.putImageData(imageData, 0, 0);
}
}
</script>
<style lang="scss" scoped>
canvas {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,117 @@
<template>
<div ref="wrapper" class="wrapper" v-bind="$attrs">
<div class="relative container">
<!-- Show the placeholder as background -->
<blurhash-img
v-if="blurhash"
:hash="blurhash"
:aspect-ratio="height / width"
class="top-0 left-0 transition-opacity duration-500"
:class="isLoaded ? 'opacity-0' : 'opacity-100'"
/>
<!-- Show the real image on the top and fade in after loading -->
<img
ref="image"
:width="width"
:height="height"
class="absolute top-0 left-0 transition-opacity duration-500"
:class="isLoaded ? 'opacity-100' : 'opacity-0'"
alt=""
/>
</div>
</div>
</template>
<script lang="ts">
import { Prop, Component, Vue, Ref, Watch } from "vue-property-decorator";
import BlurhashImg from "./BlurhashImg.vue";
@Component({
components: {
BlurhashImg,
},
})
export default class LazyImage extends Vue {
@Prop({ type: String, required: true }) src!: string;
@Prop({ type: String, required: false, default: null }) blurhash!: string;
@Prop({ type: Number, default: 1 }) width!: number;
@Prop({ type: Number, default: 1 }) height!: number;
inheritAttrs = false;
isLoaded = false;
observer!: IntersectionObserver;
@Ref("wrapper") readonly wrapper!: any;
@Ref("image") image!: any;
mounted(): void {
this.observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.onEnter();
}
});
this.observer.observe(this.wrapper);
}
unmounted(): void {
this.observer.disconnect();
}
onEnter(): void {
// Image is visible (means: has entered the viewport),
// so start loading by setting the src attribute
this.image.src = this.src;
this.image.onload = () => {
// Image is loaded, so start fading in
this.isLoaded = true;
};
}
@Watch("src")
updateImageWithSrcChange(): void {
this.onEnter();
}
}
</script>
<style lang="scss" scoped>
.relative {
position: relative;
}
.absolute {
position: absolute;
}
.top-0 {
top: 0;
}
.left-0 {
left: 0;
}
.opacity-100 {
opacity: 100%;
}
.opacity-0 {
opacity: 0;
}
.transition-opacity {
transition-property: opacity;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.duration-500 {
transition-duration: 0.5s;
}
.wrapper,
.container {
display: flex;
flex: 1;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: 50% 50%;
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<lazy-image
v-if="pictureOrDefault.url !== undefined"
:src="pictureOrDefault.url"
:width="pictureOrDefault.metadata.width"
:height="pictureOrDefault.metadata.height"
:blurhash="pictureOrDefault.metadata.blurhash"
/>
</template>
<script lang="ts">
import { IMedia } from "@/types/media.model";
import { Component, Prop, Vue } from "vue-property-decorator";
import LazyImage from "../Image/LazyImage.vue";
const DEFAULT_CARD_URL = "/img/mobilizon_default_card.png";
const DEFAULT_BLURHASH = "MCHKI4El-P-U}+={R-WWoes,Iu-P=?R,xD";
const DEFAULT_WIDTH = 630;
const DEFAULT_HEIGHT = 350;
const DEFAULT_PICTURE = {
url: DEFAULT_CARD_URL,
metadata: {
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
blurhash: DEFAULT_BLURHASH,
},
};
@Component({
components: {
LazyImage,
},
})
export default class LazyImageWrapper extends Vue {
@Prop({ required: true })
picture!: IMedia | null;
get pictureOrDefault(): Partial<IMedia> {
if (this.picture === null) {
return DEFAULT_PICTURE;
}
return {
url: this?.picture?.url,
metadata: {
width: this?.picture?.metadata?.width,
height: this?.picture?.metadata?.height,
blurhash: this?.picture?.metadata?.blurhash,
},
};
}
}
</script>

View File

@ -60,6 +60,7 @@
tag="a"
href="https://mediation.koena.net/framasoft/mobilizon/"
target="_blank"
rel="noopener"
>
<img
src="/img/koena-a11y.svg"
@ -82,20 +83,34 @@
<search-field @navbar-search="mobileNavbarActive = false" />
</b-navbar-item>
<b-navbar-dropdown v-if="currentActor.id && currentUser.isLoggedIn" right>
<b-navbar-dropdown
v-if="currentActor.id && currentUser.isLoggedIn"
right
collapsible
>
<template
slot="label"
v-if="currentActor"
class="navbar-dropdown-profile"
>
<figure class="image is-32x32" v-if="currentActor.avatar">
<img
class="is-rounded"
alt="avatarUrl"
:src="currentActor.avatar.url"
/>
</figure>
<b-icon v-else icon="account-circle" />
<div class="identity-wrapper">
<div>
<figure class="image is-32x32" v-if="currentActor.avatar">
<img
class="is-rounded"
alt="avatarUrl"
:src="currentActor.avatar.url"
/>
</figure>
<b-icon v-else icon="account-circle" />
</div>
<div class="media-content is-hidden-desktop">
<span>{{ displayName(currentActor) }}</span>
<span class="has-text-grey-dark" v-if="currentActor.name"
>@{{ currentActor.preferredUsername }}</span
>
</div>
</div>
</template>
<!-- No identities dropdown if no identities -->
@ -110,14 +125,19 @@
<span @click="setIdentity(identity)">
<div class="media-left">
<figure class="image is-32x32" v-if="identity.avatar">
<img class="is-rounded" :src="identity.avatar.url" alt />
<img
class="is-rounded"
loading="lazy"
:src="identity.avatar.url"
alt
/>
</figure>
<b-icon v-else size="is-medium" icon="account-circle" />
</div>
<div class="media-content">
<span>{{ identity.displayName() }}</span>
<span class="has-text-grey" v-if="identity.name"
<span>{{ displayName(identity) }}</span>
<span class="has-text-grey-dark" v-if="identity.name"
>@{{ identity.preferredUsername }}</span
>
</div>
@ -131,11 +151,6 @@
:to="{ name: RouteName.UPDATE_IDENTITY }"
>{{ $t("My account") }}</b-navbar-item
>
<!-- <b-navbar-item tag="router-link" :to="{ name: RouteName.CREATE_GROUP }">-->
<!-- {{ $t('Create group') }}-->
<!-- </b-navbar-item>-->
<b-navbar-item
v-if="currentUser.role === ICurrentUserRole.ADMINISTRATOR"
tag="router-link"
@ -182,7 +197,7 @@ import {
IDENTITIES,
UPDATE_DEFAULT_ACTOR,
} from "../graphql/actor";
import { IPerson, Person } from "../types/actor";
import { displayName, IPerson, Person } from "../types/actor";
import { CONFIG } from "../graphql/config";
import { IConfig } from "../types/config.model";
import { ICurrentUser, IUser } from "../types/current-user.model";
@ -240,6 +255,8 @@ export default class NavBar extends Vue {
mobileNavbarActive = false;
displayName = displayName;
@Watch("currentActor")
async initializeListOfIdentities(): Promise<void> {
if (!this.currentUser.isLoggedIn) return;
@ -247,7 +264,9 @@ export default class NavBar extends Vue {
query: IDENTITIES,
});
if (data) {
this.identities = data.identities.map((identity) => new Person(identity));
this.identities = data.identities.map(
(identity: IPerson) => new Person(identity)
);
// If we don't have any identities, the user has validated their account,
// is logging for the first time but didn't create an identity somehow
@ -320,7 +339,7 @@ nav {
cursor: pointer;
span {
display: inherit;
display: flex;
}
&.is-active {
@ -354,5 +373,14 @@ nav {
padding-top: 0.2rem;
}
}
.identity-wrapper {
display: flex;
.media-content span {
display: flex;
color: $violet-2;
}
}
}
</style>

View File

@ -4,7 +4,7 @@
{{ $t("Your participation request is being validated") }}
</h1>
<div v-else>
<div v-if="failed">
<div v-if="failed && participation === undefined">
<b-message
:title="$t('Error while validating participation request')"
type="is-danger"
@ -28,6 +28,22 @@
$t("Your participation still has to be approved by the organisers.")
}}
</p>
<div v-if="failed">
<b-message
:title="
$t(
'Error while updating participation status inside this browser'
)
"
type="is-warning"
>
{{
$t(
"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."
)
}}
</b-message>
</div>
<div class="columns has-text-centered">
<div class="column">
<router-link

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