diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..531d3cc04 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,66 @@ +# Update the VARIANT arg in docker-compose.yml to pick an Elixir version: 1.9, 1.10, 1.10.4 +ARG VARIANT="1.12.3" +FROM elixir:${VARIANT} + +# This Dockerfile adds a non-root user with sudo access. Update the “remoteUser” property in +# devcontainer.json to use it. More info: https://aka.ms/vscode-remote/containers/non-root-user. +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +# Options for common package install script +ARG INSTALL_ZSH="true" +ARG UPGRADE_PACKAGES="true" +ARG COMMON_SCRIPT_SOURCE="https://raw.githubusercontent.com/microsoft/vscode-dev-containers/v0.209.6/script-library/common-debian.sh" +ARG COMMON_SCRIPT_SHA="d35dd1711454156c9a59cc41ebe04fbff681ca0bd304f10fd5b13285d0de13b2" + +# Optional Settings for Phoenix +ARG PHOENIX_VERSION="1.6.2" + +# [Optional] Setup nodejs +ARG NODE_SCRIPT_SOURCE="https://raw.githubusercontent.com/microsoft/vscode-dev-containers/main/script-library/node-debian.sh" +ARG NODE_SCRIPT_SHA="dev-mode" +ARG NODE_VERSION="none" +ENV NVM_DIR=/usr/local/share/nvm +ENV NVM_SYMLINK_CURRENT=true +ENV PATH=${NVM_DIR}/current/bin:${PATH} + +# [Optional, Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" + +# Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. +RUN apt-get update \ + && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends curl ca-certificates 2>&1 \ + && curl -sSL ${COMMON_SCRIPT_SOURCE} -o /tmp/common-setup.sh \ + && ([ "${COMMON_SCRIPT_SHA}" = "dev-mode" ] || (echo "${COMMON_SCRIPT_SHA} */tmp/common-setup.sh" | sha256sum -c -)) \ + && /bin/bash /tmp/common-setup.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" \ + # + # [Optional] Install Node.js for use with web applications + && if [ "$NODE_VERSION" != "none" ]; then \ + curl -sSL ${NODE_SCRIPT_SOURCE} -o /tmp/node-setup.sh \ + && ([ "${NODE_SCRIPT_SHA}" = "dev-mode" ] || (echo "${NODE_SCRIPT_SHA} */tmp/node-setup.sh" | sha256sum -c -)) \ + && /bin/bash /tmp/node-setup.sh "${NVM_DIR}" "${NODE_VERSION}" "${USERNAME}"; \ + fi \ + # + # Install dependencies + && apt-get install -y build-essential \ + # + # Clean up + && apt-get autoremove -y \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* /tmp/common-setup.sh /tmp/node-setup.sh + +RUN su ${USERNAME} -c "mix local.hex --force \ + && mix local.rebar --force \ + && mix archive.install --force hex phx_new ${PHOENIX_VERSION}" + +RUN apt-get update \ + && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends cmake webp bash libncurses6 git python3 inotify-tools \ + && apt-get autoremove -y \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* + +# [Optional] Uncomment this line to install additional package. +# RUN mix ... diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..7ed7bedd6 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,44 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/elixir-phoenix-postgres +{ + "name": "Elixir, Phoenix, Node.js & PostgresSQL (Community)", + "dockerComposeFile": "docker-compose.yml", + "service": "elixir", + "workspaceFolder": "/workspace", + + // Set *default* container specific settings.json values on container create. + "settings": { + "sqltools.connections": [{ + "name": "Container database", + "driver": "PostgreSQL", + "previewLimit": 50, + "server": "localhost", + "port": 5432, + "database": "postgres", + "username": "postgres", + "password": "postgres" + }] + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "jakebecker.elixir-ls", + "mtxr.sqltools", + "mtxr.sqltools-driver-pg" + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [4000, 4001, 5432], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "mix deps.get", + // "runArgs": ["--userns=keep-id", "--privileged"], + // "containerUser": "vscode", + // "containerEnv": { + // "HOME": "/home/vscode", + // }, + // "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,Z", + + // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..bf922850a --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,46 @@ +version: "3.8" + +services: + elixir: + build: + context: . + dockerfile: Dockerfile + args: + # Elixir Version: 1.9, 1.10, 1.10.4, ... + VARIANT: "1.13.1" + # Phoenix Version: 1.4.17, 1.5.4, ... + PHOENIX_VERSION: "1.6.6" + # Node Version: 10, 11, ... + NODE_VERSION: "16" + + volumes: + - ..:/workspace:z + # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. + network_mode: service:db + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + environment: + MOBILIZON_INSTANCE_NAME: My Mobilizon Instance + MOBILIZON_INSTANCE_HOST: localhost + MOBILIZON_INSTANCE_HOST_PORT: 4000 + MOBILIZON_INSTANCE_PORT: 4000 + MOBILIZON_INSTANCE_EMAIL: noreply@mobilizon.me + MOBILIZON_INSTANCE_REGISTRATIONS_OPEN: "true" + MOBILIZON_DATABASE_PASSWORD: postgres + MOBILIZON_DATABASE_USERNAME: postgres + MOBILIZON_DATABASE_DBNAME: mobilizon + MOBILIZON_DATABASE_HOST: db + + db: + image: postgis/postgis:latest + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: app + +volumes: + postgres-data: null diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..5b280f647 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.13 +erlang 24.2 diff --git a/config/config.exs b/config/config.exs index 73c3ae5d6..10a20f43f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -290,6 +290,7 @@ config :mobilizon, Oban, crontab: [ {"@hourly", Mobilizon.Service.Workers.BuildSiteMap, queue: :background}, {"17 4 * * *", Mobilizon.Service.Workers.RefreshGroups, queue: :background}, + {"36 * * * *", Mobilizon.Service.Workers.RefreshInstances, queue: :background}, {"@hourly", Mobilizon.Service.Workers.CleanOrphanMediaWorker, queue: :background}, {"@hourly", Mobilizon.Service.Workers.CleanUnconfirmedUsersWorker, queue: :background}, {"@hourly", Mobilizon.Service.Workers.ExportCleanerWorker, queue: :background}, diff --git a/js/package.json b/js/package.json index b73cf3fc9..14dafc1f7 100644 --- a/js/package.json +++ b/js/package.json @@ -16,6 +16,7 @@ "@absinthe/socket-apollo-link": "^0.2.1", "@apollo/client": "^3.3.16", "@mdi/font": "^6.1.95", + "@tailwindcss/line-clamp": "^0.3.0", "@tiptap/core": "^2.0.0-beta.41", "@tiptap/extension-blockquote": "^2.0.0-beta.25", "@tiptap/extension-bold": "^2.0.0-beta.24", @@ -41,6 +42,7 @@ "@vue-a11y/skip-to": "^2.1.2", "@vue/apollo-option": "4.0.0-alpha.11", "apollo-absinthe-upload-link": "^1.5.0", + "autoprefixer": "^10", "blurhash": "^1.1.3", "buefy": "^0.9.0", "bulma-divider": "^0.2.0", @@ -57,8 +59,10 @@ "ngeohash": "^0.6.3", "p-debounce": "^4.0.0", "phoenix": "^1.6", + "postcss": "^8", "register-service-worker": "^1.7.2", "sanitize-html": "^2.5.3", + "tailwindcss": "^3", "tippy.js": "^6.2.3", "unfetch": "^4.2.0", "v-tooltip": "^2.1.3", @@ -112,6 +116,7 @@ "sass-loader": "^12.0.0", "ts-jest": "27", "typescript": "~4.4.3", + "vue-cli-plugin-tailwind": "^3.0.0-beta.0", "vue-i18n-extract": "^2.0.4", "vue-template-compiler": "^2.6.11", "webpack-cli": "^4.7.0" diff --git a/js/postcss.config.js b/js/postcss.config.js new file mode 100644 index 000000000..12a703d90 --- /dev/null +++ b/js/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/js/src/App.vue b/js/src/App.vue index 521346415..d8a7cd4df 100644 --- a/js/src/App.vue +++ b/js/src/App.vue @@ -216,7 +216,11 @@ export default class App extends Vue { // Set the focus to the router view // https://marcus.io/blog/accessible-routing-vuejs setTimeout(() => { - const focusTarget = this.routerView?.$el as HTMLElement; + const focusTarget = ( + this.routerView?.$refs?.componentFocusTarget !== undefined + ? this.routerView?.$refs?.componentFocusTarget + : this.routerView?.$el + ) as HTMLElement; if (focusTarget) { // Make focustarget programmatically focussable focusTarget.setAttribute("tabindex", "-1"); diff --git a/js/src/apollo/utils.ts b/js/src/apollo/utils.ts index 84ba969e9..07caeb405 100644 --- a/js/src/apollo/utils.ts +++ b/js/src/apollo/utils.ts @@ -70,6 +70,9 @@ export const typePolicies: TypePolicies = { participantStats: { merge: replaceMergePolicy }, }, }, + Instance: { + keyFields: ["domain"], + }, RootQueryType: { fields: { relayFollowers: paginatedLimitPagination(), diff --git a/js/src/assets/logo.svg b/js/src/assets/logo.svg new file mode 100644 index 000000000..32bac7d36 --- /dev/null +++ b/js/src/assets/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/js/src/assets/tailwind.css b/js/src/assets/tailwind.css new file mode 100644 index 000000000..7f393742a --- /dev/null +++ b/js/src/assets/tailwind.css @@ -0,0 +1,5 @@ +@tailwind base; + +@tailwind components; + +@tailwind utilities; diff --git a/js/src/common.scss b/js/src/common.scss index 65301b01b..2d882fb28 100644 --- a/js/src/common.scss +++ b/js/src/common.scss @@ -7,10 +7,6 @@ @import "styles/vue-announcer.scss"; @import "styles/vue-skip-to.scss"; -// a { -// color: $violet-2; -// } - a.out, .content a, .ProseMirror a { @@ -19,18 +15,10 @@ a.out, text-decoration-thickness: 2px; } -// input.input { -// border-color: $input-border-color !important; -// } - .section { padding: 1rem 1% 4rem; } -figure img.is-rounded { - border: 1px solid #cdcaea; -} - $color-black: #000; .mention { diff --git a/js/src/components/Account/ActorCard.vue b/js/src/components/Account/ActorCard.vue index d319bd898..7d48b4fc6 100644 --- a/js/src/components/Account/ActorCard.vue +++ b/js/src/components/Account/ActorCard.vue @@ -1,31 +1,75 @@ - - - diff --git a/js/src/components/Admin/Followers.vue b/js/src/components/Admin/Followers.vue deleted file mode 100644 index d05b6d606..000000000 --- a/js/src/components/Admin/Followers.vue +++ /dev/null @@ -1,262 +0,0 @@ - - diff --git a/js/src/components/Admin/Followings.vue b/js/src/components/Admin/Followings.vue deleted file mode 100644 index 6739aba01..000000000 --- a/js/src/components/Admin/Followings.vue +++ /dev/null @@ -1,311 +0,0 @@ - - diff --git a/js/src/components/Event/EventMetadataSidebar.vue b/js/src/components/Event/EventMetadataSidebar.vue index 19be9ab3a..7964d99d0 100644 --- a/js/src/components/Event/EventMetadataSidebar.vue +++ b/js/src/components/Event/EventMetadataSidebar.vue @@ -34,12 +34,6 @@ class="metadata-organized-by" :title="$t('Organized by')" > - - - - - - + :actor="event.attributedTo" + /> + - - - - + /> diff --git a/js/src/components/Utils/Breadcrumbs.vue b/js/src/components/Utils/Breadcrumbs.vue new file mode 100644 index 000000000..8122314a3 --- /dev/null +++ b/js/src/components/Utils/Breadcrumbs.vue @@ -0,0 +1,69 @@ + + diff --git a/js/src/graphql/admin.ts b/js/src/graphql/admin.ts index acb3f73b1..db6c473e9 100644 --- a/js/src/graphql/admin.ts +++ b/js/src/graphql/admin.ts @@ -70,6 +70,67 @@ export const RELAY_FOLLOWINGS = gql` ${RELAY_FRAGMENT} `; +export const INSTANCE_FRAGMENT = gql` + fragment InstanceFragment on Instance { + domain + hasRelay + followerStatus + followedStatus + eventCount + personCount + groupCount + followersCount + followingsCount + reportsCount + mediaSize + } +`; + +export const INSTANCE = gql` + query instance($domain: ID!) { + instance(domain: $domain) { + ...InstanceFragment + } + } + ${INSTANCE_FRAGMENT} +`; + +export const INSTANCES = gql` + query Instances( + $page: Int + $limit: Int + $orderBy: InstancesSortFields + $direction: String + $filterDomain: String + $filterFollowStatus: InstanceFilterFollowStatus + $filterSuspendStatus: InstanceFilterSuspendStatus + ) { + instances( + page: $page + limit: $limit + orderBy: $orderBy + direction: $direction + filterDomain: $filterDomain + filterFollowStatus: $filterFollowStatus + filterSuspendStatus: $filterSuspendStatus + ) { + total + elements { + ...InstanceFragment + } + } + } + ${INSTANCE_FRAGMENT} +`; +export const ADD_INSTANCE = gql` + mutation addInstance($domain: String!) { + addInstance(domain: $domain) { + ...InstanceFragment + } + } + ${INSTANCE_FRAGMENT} +`; + export const ADD_RELAY = gql` mutation addRelay($address: String!) { addRelay(address: $address) { @@ -190,3 +251,26 @@ export const SAVE_ADMIN_SETTINGS = gql` } ${ADMIN_SETTINGS_FRAGMENT} `; + +export const ADMIN_UPDATE_USER = gql` + mutation AdminUpdateUser( + $id: ID! + $email: String + $role: UserRole + $confirmed: Boolean + $notify: Boolean + ) { + adminUpdateUser( + id: $id + email: $email + role: $role + confirmed: $confirmed + notify: $notify + ) { + id + email + role + confirmedAt + } + } +`; diff --git a/js/src/graphql/report.ts b/js/src/graphql/report.ts index 3fcffe8e8..d25436182 100644 --- a/js/src/graphql/report.ts +++ b/js/src/graphql/report.ts @@ -2,8 +2,13 @@ import gql from "graphql-tag"; import { ACTOR_FRAGMENT } from "./actor"; export const REPORTS = gql` - query Reports($status: ReportStatus, $page: Int, $limit: Int) { - reports(status: $status, page: $page, limit: $limit) { + query Reports( + $status: ReportStatus + $domain: String + $page: Int + $limit: Int + ) { + reports(status: $status, domain: $domain, page: $page, limit: $limit) { total elements { id diff --git a/js/src/graphql/user.ts b/js/src/graphql/user.ts index 8d0a8bce9..6c9c1bb9b 100644 --- a/js/src/graphql/user.ts +++ b/js/src/graphql/user.ts @@ -209,14 +209,30 @@ export const UPDATE_ACTIVITY_SETTING = gql` `; export const LIST_USERS = gql` - query ListUsers($email: String, $page: Int, $limit: Int) { - users(email: $email, page: $page, limit: $limit) { + query ListUsers( + $email: String + $currentSignInIp: String + $page: Int + $limit: Int + $sort: SortableUserField + $direction: SortDirection + ) { + users( + email: $email + currentSignInIp: $currentSignInIp + page: $page + limit: $limit + sort: $sort + direction: $direction + ) { total elements { id email locale confirmedAt + currentSignInIp + currentSignInAt disabled actors { ...ActorFragment diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index d6ead134b..d2ba98d19 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -1260,5 +1260,52 @@ "This profile was not found": "This profile was not found", "Back to profile list": "Back to profile list", "This user was not found": "This user was not found", - "Back to user list": "Back to user list" + "Back to user list": "Back to user list", + "Stop following instance": "Stop following instance", + "Follow instance": "Follow instance", + "Accept follow": "Accept follow", + "Reject follow": "Reject follow", + "This instance doesn't follow yours.": "This instance doesn't follow yours.", + "Only Mobilizon instances can be followed": "", + "Follow a new instance": "Follow a new instance", + "Follow status": "Follow status", + "All": "All", + "Following": "Following", + "Followed": "Followed", + "Followed, pending response": "Followed, pending response", + "Follows us": "Follows us", + "Follows us, pending approval": "Follows us, pending approval", + "No instance found.": "No instance found.", + "No instances match this filter. Try resetting filter fields?": "No instances match this filter. Try resetting filter fields?", + "You haven't interacted with other instances yet.": "You haven't interacted with other instances yet.", + "mobilizon-instance.tld": "mobilizon-instance.tld", + "Report status": "Report status", + "access the corresponding account": "access the corresponding account", + "Organized events": "Organized events", + "Memberships": "Memberships", + "This profile is located on this instance, so you need to {access_the_corresponding_account} to suspend it.": "This profile is located on this instance, so you need to {access_the_corresponding_account} to suspend it.", + "Total number of participations": "Total number of participations", + "Uploaded media total size": "Uploaded media total size", + "0 Bytes": "0 Bytes", + "Change email": "Change email", + "Confirm user": "Confirm user", + "Change role": "Change role", + "The user has been disabled": "The user has been disabled", + "This user doesn't have any profiles": "This user doesn't have any profiles", + "Edit user email": "Edit user email", + "Change user email": "Change user email", + "Previous email": "Previous email", + "Notify the user of the change": "Notify the user of the change", + "Change user role": "Change user role", + "Suspend the account?": "Suspend the account?", + "Do you really want to suspend this account? All of the user's profiles will be deleted.": "Do you really want to suspend this account? All of the user's profiles will be deleted.", + "Suspend the account": "Suspend the account", + "No user matches the filter": "No user matches the filter", + "new@email.com": "new@email.com", + "Other users with the same email domain": "Other users with the same email domain", + "Other users with the same IP address": "Other users with the same IP address", + "IP Address": "IP Address", + "Last seen on": "Last seen on", + "No user matches the filters": "No user matches the filters", + "Reset filters": "Reset filters" } diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index ff8bb1128..5e7b2ecb7 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -288,7 +288,7 @@ "Either the participation request has already been validated, either the validation token is incorrect.": "Soit la demande de participation a déjà été validée, soit le jeton de validation est incorrect.", "Element title": "Titre de l'élement", "Element value": "Valeur de l'élement", - "Email": "Email", + "Email": "Courriel", "Email address": "Adresse email", "Email validate": "Validation de l'email", "Emails usually don't contain capitals, make sure you haven't made a typo.": "Les emails ne contiennent d'ordinaire pas de capitales, assurez-vous de n'avoir pas fait de faute de frappe.", @@ -1260,5 +1260,52 @@ "{timezoneLongName} ({timezoneShortName})": "{timezoneLongName} ({timezoneShortName})", "{title} ({count} todos)": "{title} ({count} todos)", "{username} was invited to {group}": "{username} a été invité à {group}", - "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap" + "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap", + "Stop following instance": "Arrêter de suivre l'instance", + "Follow instance": "Suivre l'instance", + "Accept follow": "Accepter le suivi", + "Reject follow": "Rejetter le suivi", + "This instance doesn't follow yours.": "Cette instance ne suit pas la vôtre.", + "Only Mobilizon instances can be followed": "Seules les instances Mobilizon peuvent être suivies", + "Follow a new instance": "Suivre une nouvelle instance", + "Follow status": "Statut du suivi", + "All": "Toutes", + "Following": "Suivantes", + "Followed": "Suivies", + "Followed, pending response": "Suivie, en attente de la réponse", + "Follows us": "Nous suit", + "Follows us, pending approval": "Nous suit, en attente de validation", + "No instance found": "Aucune instance trouvée", + "No instances match this filter. Try resetting filter fields?": "Aucune instance ne correspond à ce filtre. Essayer de remettre à zéro les champs des filtres ?", + "You haven't interacted with other instances yet.": "Vous n'avez interagi avec encore aucune autre instance.", + "mobilizon-instance.tld": "instance-mobilizon.tld", + "Report status": "Statut du signalement", + "access the corresponding account": "accéder au compte correspondant", + "Organized events": "Événements organisés", + "Memberships": "Adhésions", + "This profile is located on this instance, so you need to {access_the_corresponding_account} to suspend it.": "Ce profil se situe sur cette instance, vous devez donc {access_the_corresponding_account} afin de le suspendre.", + "Total number of participations": "Nombre total de participations", + "Uploaded media total size": "Taille totale des médias téléversés", + "0 Bytes": "0 octets", + "Change email": "Changer l'email", + "Confirm user": "Confirmer l'utilisateur⋅ice", + "Change role": "Changer le role", + "The user has been disabled": "L'utilisateur⋅ice a été désactivé", + "This user doesn't have any profiles": "Cet utilisateur⋅ice n'a aucun profil", + "Edit user email": "Éditer l'email de l'utilisateur⋅ice", + "Change user email": "Modifier l'email de l'utilisateur⋅ice", + "Previous email": "Email précédent", + "Notify the user of the change": "Notifier l'utilisateur du changement", + "Change user role": "Changer le role de l'utilisateur", + "Suspend the account?": "Suspendre le compte ?", + "Do you really want to suspend this account? All of the user's profiles will be deleted.": "Voulez-vous vraiment suspendre ce compte ? Tous les profils de cet⋅te utilisateur⋅ice seront supprimés.", + "Suspend the account": "Suspendre le compte", + "No user matches the filter": "Aucun⋅e utilisateur⋅ice ne correspond au filtre", + "new@email.com": "nouvel@email.com", + "Other users with the same email domain": "Autres utilisateur⋅ices avec le même domaine de courriel", + "Other users with the same IP address": "Autres utilisateur⋅ices avec la même adresse IP", + "IP Address": "Adresse IP", + "Last seen on": "Vu pour la dernière fois", + "No user matches the filters": "Aucun⋅e utilisateur⋅ice ne correspond aux filtres", + "Reset filters": "Réinitialiser les filtres" } diff --git a/js/src/main.ts b/js/src/main.ts index c3e513bd6..0e6695195 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -12,7 +12,9 @@ import { NotifierPlugin } from "./plugins/notifier"; import filters from "./filters"; import { i18n } from "./utils/i18n"; import apolloProvider from "./vue-apollo"; +import Breadcrumbs from "@/components/Utils/Breadcrumbs.vue"; import "./registerServiceWorker"; +import "./assets/tailwind.css"; Vue.config.productionTip = false; @@ -24,6 +26,7 @@ Vue.use(VueScrollTo); Vue.use(VTooltip); Vue.use(VueAnnouncer); Vue.use(VueSkipTo); +Vue.component("breadcrumbs-nav", Breadcrumbs); // Register the router hooks with their names Component.registerHooks([ diff --git a/js/src/mixins/relay.ts b/js/src/mixins/relay.ts deleted file mode 100644 index 0289c1392..000000000 --- a/js/src/mixins/relay.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { IActor } from "@/types/actor"; -import { ActorType } from "@/types/enums"; -import { Component, Vue, Ref } from "vue-property-decorator"; -import VueRouter from "vue-router"; -const { isNavigationFailure, NavigationFailureType } = VueRouter; - -@Component -export default class RelayMixin extends Vue { - @Ref("table") readonly table!: any; - - toggle(row: Record): void { - this.table.toggleDetails(row); - } - - protected async pushRouter( - routeName: string, - args: Record - ): Promise { - try { - await this.$router.push({ - name: routeName, - query: { ...this.$route.query, ...args }, - }); - } catch (e) { - if (isNavigationFailure(e, NavigationFailureType.redirected)) { - throw Error(e.toString()); - } - } - } - - static isInstance(actor: IActor): boolean { - return ( - actor.type === ActorType.APPLICATION && - (actor.preferredUsername === "relay" || - actor.preferredUsername === actor.domain) - ); - } -} diff --git a/js/src/router/settings.ts b/js/src/router/settings.ts index 29b3de00c..c71730ba0 100644 --- a/js/src/router/settings.ts +++ b/js/src/router/settings.ts @@ -11,9 +11,8 @@ export enum SettingsRouteName { ADMIN = "ADMIN", ADMIN_DASHBOARD = "ADMIN_DASHBOARD", ADMIN_SETTINGS = "ADMIN_SETTINGS", - RELAYS = "Relays", - RELAY_FOLLOWINGS = "Followings", - RELAY_FOLLOWERS = "Followers", + INSTANCES = "INSTANCES", + INSTANCE = "INSTANCE", USERS = "USERS", PROFILES = "PROFILES", ADMIN_PROFILE = "ADMIN_PROFILE", @@ -21,7 +20,7 @@ export enum SettingsRouteName { ADMIN_GROUPS = "ADMIN_GROUPS", ADMIN_GROUP_PROFILE = "ADMIN_GROUP_PROFILE", MODERATION = "MODERATION", - REPORTS = "Reports", + REPORTS = "REPORTS", REPORT = "Report", REPORT_LOGS = "Logs", CREATE_IDENTITY = "CreateIdentity", @@ -199,44 +198,35 @@ export const settingsRoutes: RouteConfig[] = [ meta: { requiredAuth: true, announcer: { skip: true } }, }, { - path: "admin/relays", - name: SettingsRouteName.RELAYS, - redirect: { name: SettingsRouteName.RELAY_FOLLOWINGS }, + path: "admin/instances", + name: SettingsRouteName.INSTANCES, component: (): Promise => - import(/* webpackChunkName: "Follows" */ "@/views/Admin/Follows.vue"), - meta: { requiredAuth: true, announcer: { skip: true } }, - children: [ - { - path: "followings", - name: SettingsRouteName.RELAY_FOLLOWINGS, - component: (): Promise => - import( - /* webpackChunkName: "Followings" */ "@/components/Admin/Followings.vue" - ), - meta: { - requiredAuth: true, - announcer: { - message: (): string => i18n.t("Followings") as string, - }, - }, + import( + /* webpackChunkName: "Instances" */ "@/views/Admin/Instances.vue" + ), + meta: { + requiredAuth: true, + announcer: { + message: (): string => i18n.t("Instances") as string, }, - { - path: "followers", - name: SettingsRouteName.RELAY_FOLLOWERS, - component: (): Promise => - import( - /* webpackChunkName: "Followers" */ "@/components/Admin/Followers.vue" - ), - meta: { - requiredAuth: true, - announcer: { - message: (): string => i18n.t("Followers") as string, - }, - }, - }, - ], + }, props: true, }, + { + path: "admin/instances/:domain", + name: SettingsRouteName.INSTANCE, + component: (): Promise => + import( + /* webpackChunkName: "Instance" */ "@/views/Admin/Instance.vue" + ), + props: true, + meta: { + requiredAuth: true, + announcer: { + message: (): string => i18n.t("Instance") as string, + }, + }, + }, { path: "/moderation", name: SettingsRouteName.MODERATION, @@ -244,7 +234,7 @@ export const settingsRoutes: RouteConfig[] = [ meta: { requiredAuth: true, announcer: { skip: true } }, }, { - path: "/moderation/reports/:filter?", + path: "/moderation/reports", name: SettingsRouteName.REPORTS, component: (): Promise => import( diff --git a/js/src/types/enums.ts b/js/src/types/enums.ts index 34049388b..2f05bd0d0 100644 --- a/js/src/types/enums.ts +++ b/js/src/types/enums.ts @@ -276,3 +276,15 @@ export enum EventMetadataCategories { BOOKING = "BOOKING", VIDEO_CONFERENCE = "VIDEO_CONFERENCE", } + +export enum InstanceFilterFollowStatus { + ALL = "ALL", + FOLLOWING = "FOLLOWING", + FOLLOWED = "FOLLOWED", +} + +export enum InstanceFollowStatus { + APPROVED = "APPROVED", + PENDING = "PENDING", + NONE = "NONE", +} diff --git a/js/src/types/instance.model.ts b/js/src/types/instance.model.ts new file mode 100644 index 000000000..5936f6ba6 --- /dev/null +++ b/js/src/types/instance.model.ts @@ -0,0 +1,14 @@ +import { InstanceFollowStatus } from "./enums"; + +export interface IInstance { + domain: string; + hasRelay: boolean; + followerStatus: InstanceFollowStatus; + followedStatus: InstanceFollowStatus; + personCount: number; + groupCount: number; + followersCount: number; + followingsCount: number; + reportsCount: number; + mediaSize: number; +} diff --git a/js/src/utils/datetime.ts b/js/src/utils/datetime.ts index e0b3210a3..4eef7cc7f 100644 --- a/js/src/utils/datetime.ts +++ b/js/src/utils/datetime.ts @@ -19,8 +19,8 @@ function localeShortWeekDayNames(): string[] { } // https://stackoverflow.com/a/18650828/10204399 -function formatBytes(bytes: number, decimals = 2): string { - if (bytes === 0) return "0 Bytes"; +function formatBytes(bytes: number, decimals = 2, zero = "0 Bytes"): string { + if (bytes === 0) return zero; const k = 1024; const dm = decimals < 0 ? 0 : decimals; diff --git a/js/src/variables.scss b/js/src/variables.scss index 46c03bcbb..07fb0e60e 100644 --- a/js/src/variables.scss +++ b/js/src/variables.scss @@ -139,3 +139,17 @@ $subtitle-sup-size: 15px; $breadcrumb-item-color: $primary; $checkbox-background-color: #fff; $title-color: $violet-3; + +:root { + --color-primary: 30 125 151; + --color-secondary: 255 213 153; + --color-violet-title: 66 64 86; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-primary: 30 125 151; + --color-secondary: 255 213 153; + --color-violet-title: 66 64 86; + } +} diff --git a/js/src/views/Account/children/EditIdentity.vue b/js/src/views/Account/children/EditIdentity.vue index 0bba7acde..d0e56dd43 100644 --- a/js/src/views/Account/children/EditIdentity.vue +++ b/js/src/views/Account/children/EditIdentity.vue @@ -1,28 +1,6 @@