Merge branch 'upstream_1.0.3' into chapril

This commit is contained in:
tykayn 2021-01-10 10:36:16 +01:00 committed by Baptiste Lemoine
commit 247c76e038
489 changed files with 28317 additions and 15550 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
js/.gitignore vendored
View File

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

View File

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

View File

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

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

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 725 KiB

After

Width:  |  Height:  |  Size: 725 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

3291
js/schema.graphql Normal file

File diff suppressed because it is too large Load Diff

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

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

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

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

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,11 +16,21 @@
checkable
checkbox-position="left"
>
<b-table-column field="actor.id" label="ID" width="40" numeric v-slot="props">{{
props.row.actor.id
}}</b-table-column>
<b-table-column
field="actor.id"
label="ID"
width="40"
numeric
v-slot="props"
>{{ props.row.actor.id }}</b-table-column
>
<b-table-column field="actor.type" :label="$t('Type')" width="80" v-slot="props">
<b-table-column
field="actor.type"
:label="$t('Type')"
width="80"
v-slot="props"
>
<b-icon icon="lan" v-if="RelayMixin.isInstance(props.row.actor)" />
<b-icon icon="account-circle" v-else />
</b-table-column>
@ -33,26 +43,39 @@
centered
v-slot="props"
>
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`">{{
props.row.approved ? $t("Accepted") : $t("Pending")
}}</span>
<span
:class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
>{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
>
</b-table-column>
<b-table-column field="actor.domain" :label="$t('Domain')" sortable>
<template v-slot:default="props">
<a @click="toggle(props.row)" v-if="RelayMixin.isInstance(props.row.actor)">{{
props.row.actor.domain
}}</a>
<a
@click="toggle(props.row)"
v-if="RelayMixin.isInstance(props.row.actor)"
>{{ props.row.actor.domain }}</a
>
<a @click="toggle(props.row)" v-else>{{
`${props.row.actor.preferredUsername}@${props.row.actor.domain}`
}}</a>
</template>
</b-table-column>
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable v-slot="props">
<span :title="$options.filters.formatDateTimeString(props.row.updatedAt)">{{
formatDistanceToNow(new Date(props.row.updatedAt), { locale: $dateFnsLocale })
}}</span></b-table-column
<b-table-column
field="targetActor.updatedAt"
:label="$t('Date')"
sortable
v-slot="props"
>
<span
:title="$options.filters.formatDateTimeString(props.row.updatedAt)"
>{{
formatDistanceToNow(new Date(props.row.updatedAt), {
locale: $dateFnsLocale,
})
}}</span
></b-table-column
>
<template slot="detail" slot-scope="props">
@ -143,7 +166,11 @@ export default class Followers extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowers.refetch();
this.checkedRows = [];
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}
@ -158,7 +185,11 @@ export default class Followers extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowers.refetch();
this.checkedRows = [];
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}

View File

@ -1,13 +1,22 @@
<template>
<div>
<form @submit="followRelay">
<b-field :label="$t('Add an instance')" custom-class="add-relay" horizontal>
<b-field
:label="$t('Add an instance')"
custom-class="add-relay"
horizontal
>
<b-field grouped expanded size="is-large">
<p class="control">
<b-input v-model="newRelayAddress" :placeholder="$t('Ex: mobilizon.fr')" />
<b-input
v-model="newRelayAddress"
:placeholder="$t('Ex: mobilizon.fr')"
/>
</p>
<p class="control">
<b-button type="is-primary" native-type="submit">{{ $t("Add an instance") }}</b-button>
<b-button type="is-primary" native-type="submit">{{
$t("Add an instance")
}}</b-button>
</p>
</b-field>
</b-field>
@ -29,12 +38,25 @@
checkable
checkbox-position="left"
>
<b-table-column field="targetActor.id" label="ID" width="40" numeric v-slot="props">{{
props.row.targetActor.id
}}</b-table-column>
<b-table-column
field="targetActor.id"
label="ID"
width="40"
numeric
v-slot="props"
>{{ props.row.targetActor.id }}</b-table-column
>
<b-table-column field="targetActor.type" :label="$t('Type')" width="80" v-slot="props">
<b-icon icon="lan" v-if="RelayMixin.isInstance(props.row.targetActor)" />
<b-table-column
field="targetActor.type"
:label="$t('Type')"
width="80"
v-slot="props"
>
<b-icon
icon="lan"
v-if="RelayMixin.isInstance(props.row.targetActor)"
/>
<b-icon icon="account-circle" v-else />
</b-table-column>
@ -46,26 +68,39 @@
centered
v-slot="props"
>
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`">{{
props.row.approved ? $t("Accepted") : $t("Pending")
}}</span>
<span
:class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
>{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
>
</b-table-column>
<b-table-column field="targetActor.domain" :label="$t('Domain')" sortable>
<template v-slot:default="props">
<a @click="toggle(props.row)" v-if="RelayMixin.isInstance(props.row.targetActor)">{{
props.row.targetActor.domain
}}</a>
<a
@click="toggle(props.row)"
v-if="RelayMixin.isInstance(props.row.targetActor)"
>{{ props.row.targetActor.domain }}</a
>
<a @click="toggle(props.row)" v-else>{{
`${props.row.targetActor.preferredUsername}@${props.row.targetActor.domain}`
}}</a>
</template>
</b-table-column>
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable v-slot="props">
<span :title="$options.filters.formatDateTimeString(props.row.updatedAt)">{{
formatDistanceToNow(new Date(props.row.updatedAt), { locale: $dateFnsLocale })
}}</span></b-table-column
<b-table-column
field="targetActor.updatedAt"
:label="$t('Date')"
sortable
v-slot="props"
>
<span
:title="$options.filters.formatDateTimeString(props.row.updatedAt)"
>{{
formatDistanceToNow(new Date(props.row.updatedAt), {
locale: $dateFnsLocale,
})
}}</span
></b-table-column
>
<template slot="detail" slot-scope="props">
@ -103,7 +138,6 @@ import { SnackbarProgrammatic as Snackbar } from "buefy";
import { formatDistanceToNow } from "date-fns";
import { ADD_RELAY, REMOVE_RELAY } from "../../graphql/admin";
import { IFollower } from "../../types/actor/follower.model";
import { Paginate } from "../../types/paginate";
import RelayMixin from "../../mixins/relay";
@Component({
@ -127,19 +161,25 @@ export default class Followings extends Mixins(RelayMixin) {
await this.$apollo.mutate({
mutation: ADD_RELAY,
variables: {
address: this.newRelayAddress,
address: this.newRelayAddress.trim(), // trim to fix copy and paste domain name spaces and tabs
},
});
await this.$apollo.queries.relayFollowings.refetch();
this.newRelayAddress = "";
} catch (err) {
Snackbar.open({ message: err.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: err.message,
type: "is-danger",
position: "is-bottom",
});