Various admin improvements Closes #973 See merge request framasoft/mobilizon!1152chapril
commit
e6b95a43d1
@ -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 ...
|
@ -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"
|
||||
}
|
@ -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
|
@ -0,0 +1,2 @@
|
||||
elixir 1.13
|
||||
erlang 24.2
|
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
After Width: | Height: | Size: 920 B |
@ -0,0 +1,5 @@
|
||||
@tailwind base;
|
||||
|
||||
@tailwind components;
|
||||
|
||||
@tailwind utilities;
|
@ -1,262 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-table
|
||||
v-show="relayFollowers.elements.length > 0"
|
||||
:data="relayFollowers.elements"
|
||||
:loading="$apollo.queries.relayFollowers.loading"
|
||||
ref="table"
|
||||
:checked-rows.sync="checkedRows"
|
||||
detailed
|
||||
: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="FOLLOWERS_PER_PAGE"
|
||||
@page-change="onFollowersPageChange"
|
||||
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.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>
|
||||
|
||||
<b-table-column
|
||||
field="approved"
|
||||
:label="$t('Status')"
|
||||
width="100"
|
||||
sortable
|
||||
centered
|
||||
v-slot="props"
|
||||
>
|
||||
<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-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
|
||||
>
|
||||
|
||||
<template #detail="props">
|
||||
<article>
|
||||
<div class="content">
|
||||
<strong>{{ props.row.actor.name }}</strong>
|
||||
<small v-if="props.row.actor.preferredUsername !== 'relay'"
|
||||
>@{{ props.row.actor.preferredUsername }}</small
|
||||
>
|
||||
<p v-html="props.row.actor.summary" />
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<template slot="bottom-left" v-if="checkedRows.length > 0">
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
@click="acceptRelays"
|
||||
type="is-success"
|
||||
v-if="checkedRowsHaveAtLeastOneToApprove"
|
||||
>
|
||||
{{
|
||||
$tc(
|
||||
"No instance to approve|Approve instance|Approve {number} instances",
|
||||
checkedRows.length,
|
||||
{ number: checkedRows.length }
|
||||
)
|
||||
}}
|
||||
</b-button>
|
||||
<b-button @click="rejectRelays" type="is-danger">
|
||||
{{
|
||||
$tc(
|
||||
"No instance to reject|Reject instance|Reject {number} instances",
|
||||
checkedRows.length,
|
||||
{ number: checkedRows.length }
|
||||
)
|
||||
}}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
</b-table>
|
||||
<b-message type="is-danger" v-if="relayFollowers.elements.length === 0">{{
|
||||
$t("No instance follows your instance yet.")
|
||||
}}</b-message>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins } from "vue-property-decorator";
|
||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
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,
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Followers extends Mixins(RelayMixin) {
|
||||
RelayMixin = RelayMixin;
|
||||
|
||||
formatDistanceToNow = formatDistanceToNow;
|
||||
|
||||
relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
|
||||
checkedRows: IFollower[] = [];
|
||||
|
||||
FOLLOWERS_PER_PAGE = FOLLOWERS_PER_PAGE;
|
||||
|
||||
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}`);
|
||||
});
|
||||
}
|
||||
|
||||
rejectRelays(): void {
|
||||
this.checkedRows.forEach((row: IFollower) => {
|
||||
this.rejectRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
|
||||
});
|
||||
}
|
||||
|
||||
async acceptRelay(address: string): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: ACCEPT_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowers.refetch();
|
||||
this.checkedRows = [];
|
||||
} catch (e: any) {
|
||||
if (e.message) {
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async rejectRelay(address: string): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REJECT_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowers.refetch();
|
||||
this.checkedRows = [];
|
||||
} catch (e: any) {
|
||||
if (e.message) {
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: any) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,311 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<form @submit="followRelay">
|
||||
<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')"
|
||||
/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<b-button type="is-primary" native-type="submit">{{
|
||||
$t("Add an instance")
|
||||
}}</b-button>
|
||||
</p>
|
||||
</b-field>
|
||||
</b-field>
|
||||
</form>
|
||||
<b-table
|
||||
v-show="relayFollowings.elements.length > 0"
|
||||
:data="relayFollowings.elements"
|
||||
:loading="$apollo.queries.relayFollowings.loading"
|
||||
ref="table"
|
||||
:checked-rows.sync="checkedRows"
|
||||
:is-row-checkable="(row) => row.id !== 3"
|
||||
detailed
|
||||
: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="FOLLOWINGS_PER_PAGE"
|
||||
@page-change="onFollowingsPageChange"
|
||||
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.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>
|
||||
|
||||
<b-table-column
|
||||
field="approved"
|
||||
:label="$t('Status')"
|
||||
width="100"
|
||||
sortable
|
||||
centered
|
||||
v-slot="props"
|
||||
>
|
||||
<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-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
|
||||
>
|
||||
|
||||
<template #detail="props">
|
||||
<article>
|
||||
<div class="content">
|
||||
<strong>{{ props.row.targetActor.name }}</strong>
|
||||
<small v-if="props.row.actor.preferredUsername !== 'relay'"
|
||||
>@{{ props.row.targetActor.preferredUsername }}</small
|
||||
>
|
||||
<p v-html="props.row.targetActor.summary" />
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<template slot="bottom-left" v-if="checkedRows.length > 0">
|
||||
<b-button @click="removeRelays" type="is-danger">
|
||||
{{
|
||||
$tc(
|
||||
"No instance to remove|Remove instance|Remove {number} instances",
|
||||
checkedRows.length,
|
||||
{ number: checkedRows.length }
|
||||
)
|
||||
}}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
<b-message type="is-danger" v-if="relayFollowings.total === 0">{{
|
||||
$t("You don't follow any instances yet.")
|
||||
}}</b-message>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins } from "vue-property-decorator";
|
||||
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 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,
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Followings extends Mixins(RelayMixin) {
|
||||
newRelayAddress = "";
|
||||
|
||||
RelayMixin = 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: any) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async followRelay(e: Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
try {
|
||||
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,
|
||||
});
|
||||
},
|
||||
});
|
||||
this.newRelayAddress = "";
|
||||
} catch (err: any) {
|
||||
if (err.message) {
|
||||
Snackbar.open({
|
||||
message: err.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeRelays(): void {
|
||||
this.checkedRows.forEach((row: IFollower) => {
|
||||
this.removeRelay(row);
|
||||
});
|
||||
}
|
||||
|
||||
async removeRelay(follower: IFollower): Promise<void> {
|
||||
const address = `${follower.targetActor.preferredUsername}@${follower.targetActor.domain}`;
|
||||
try {
|
||||
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 = [];
|
||||
} catch (e: any) {
|
||||
if (e.message) {
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<nav class="flex mb-3" :aria-label="$t('Breadcrumbs')">
|
||||
<ol class="inline-flex items-center space-x-1 md:space-x-3 flex-wrap">
|
||||
<li
|
||||
class="inline-flex items-center"
|
||||
v-for="(element, index) in links"
|
||||
:key="index"
|
||||
:aria-current="index > 0 ? 'page' : undefined"
|
||||
>
|
||||
<router-link
|
||||
v-if="index === 0"
|
||||
:to="element"
|
||||
class="inline-flex items-center text-gray-800 hover:text-gray-900"
|
||||
>
|
||||
{{ element.text }}
|
||||
</router-link>
|
||||
<div class="flex items-center" v-else-if="index === links.length - 1">
|
||||
<svg
|
||||
class="w-6 h-6 text-gray-400 rtl:rotate-180"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
<span
|
||||
class="ltr:ml-1 rtl:mr-1 font-medium text-gray-600 md:ltr:ml-2 md:rtl:mr-2"
|
||||
>{{ element.text }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center" v-else>
|
||||
<svg
|
||||
class="w-6 h-6 text-gray-400 rtl:rotate-180"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
<router-link
|
||||
:to="element"
|
||||
class="ltr:ml-1 rtl:mr-1 font-medium text-gray-800 hover:text-gray-900 md:ltr:ml-2 md:rtl:mr-2"
|
||||
>{{ element.text }}</router-link
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
<slot></slot>
|
||||
</ol>
|
||||
</nav>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { Location } from "vue-router";
|
||||
|
||||
type LinkElement = Location & { text: string };
|
||||
|
||||
@Component
|
||||
export default class Breadcrumbs extends Vue {
|
||||
@Prop({ type: Array, required: true }) links!: LinkElement[];
|
||||
}
|
||||
</script>
|