feat(nodeinfo): extract and save NodeInfo information from instances to display it on instances list
We also try to detect the application actor if it's not given by NodeInfo metadata (FEP-2677) (guessing for Mobilizon, PeerTube & Mastodon). Closes #1392 Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -293,7 +293,7 @@ button.menubar__button {
|
||||
@apply px-3 dark:text-black;
|
||||
}
|
||||
.pagination-link-current {
|
||||
@apply bg-primary dark:bg-primary cursor-not-allowed pointer-events-none border-primary text-white dark:text-zinc-900;
|
||||
@apply bg-primary dark:bg-primary cursor-not-allowed pointer-events-none border-primary text-white;
|
||||
}
|
||||
.pagination-ellipsis {
|
||||
@apply text-center m-1 text-gray-300;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<li
|
||||
class="setting-menu-item"
|
||||
:class="{
|
||||
'cursor-pointer bg-mbz-yellow-alt-500 dark:bg-mbz-purple-500': isActive,
|
||||
'bg-mbz-yellow-alt-100 hover:bg-mbz-yellow-alt-200 dark:bg-mbz-purple-300 dark:hover:bg-mbz-purple-400 dark:text-white':
|
||||
'cursor-pointer bg-mbz-yellow-alt-500 dark:bg-mbz-purple-600': isActive,
|
||||
'bg-mbz-yellow-alt-100 hover:bg-mbz-yellow-alt-200 dark:bg-mbz-purple-500 dark:hover:bg-mbz-purple-600 dark:text-white':
|
||||
!isActive,
|
||||
}"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<li
|
||||
class="bg-mbz-yellow-alt-300 text-violet-2 dark:bg-mbz-purple-500 dark:text-zinc-100 text-xl"
|
||||
class="bg-mbz-yellow-alt-300 text-violet-2 dark:bg-mbz-purple-700 dark:text-zinc-100 text-xl"
|
||||
>
|
||||
<router-link
|
||||
class="cursor-pointer my-2 mx-0 py-2 px-3 font-medium block no-underline"
|
||||
|
||||
@@ -81,6 +81,10 @@ export const INSTANCE_FRAGMENT = gql`
|
||||
fragment InstanceFragment on Instance {
|
||||
domain
|
||||
hasRelay
|
||||
instanceName
|
||||
instanceDescription
|
||||
software
|
||||
softwareVersion
|
||||
relayAddress
|
||||
followerStatus
|
||||
followedStatus
|
||||
|
||||
@@ -1638,5 +1638,8 @@
|
||||
"Return to the event page": "Return to the event page",
|
||||
"Cancel participation": "Cancel participation",
|
||||
"Add a recipient": "Add a recipient",
|
||||
"Announcements for {eventTitle}": "Announcements for {eventTitle}"
|
||||
"Announcements for {eventTitle}": "Announcements for {eventTitle}",
|
||||
"Visit {instance_domain}": "Visit {instance_domain}",
|
||||
"Software details: {software_details}": "Software details: {software_details}",
|
||||
"Only instances with an application actor can be followed": "Only instances with an application actor can be followed"
|
||||
}
|
||||
@@ -1632,5 +1632,8 @@
|
||||
"Return to the event page": "Retourner à la page de l'événement",
|
||||
"Cancel participation": "Annuler la participation",
|
||||
"Add a recipient": "Ajouter un·e destinataire",
|
||||
"Announcements for {eventTitle}": "Annonces pour {eventTitle}"
|
||||
"Announcements for {eventTitle}": "Annonces pour {eventTitle}",
|
||||
"Visit {instance_domain}": "Visiter {instance_domain}",
|
||||
"Software details: {software_details}": "Détails du logiciel : {software_details}",
|
||||
"Only instances with an application actor can be followed": "Seules les instances avec un acteur application peuvent être suivies"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ import { InstanceFollowStatus } from "./enums";
|
||||
export interface IInstance {
|
||||
domain: string;
|
||||
hasRelay: boolean;
|
||||
instanceName: string | null;
|
||||
instanceDescription: string | null;
|
||||
software: string | null;
|
||||
softwareVersion: string | null;
|
||||
relayAddress: string | null;
|
||||
followerStatus: InstanceFollowStatus;
|
||||
followedStatus: InstanceFollowStatus;
|
||||
|
||||
@@ -2,138 +2,187 @@
|
||||
<div v-if="instance">
|
||||
<breadcrumbs-nav
|
||||
:links="[
|
||||
{ name: RouteName.ADMIN, text: $t('Admin') },
|
||||
{ name: RouteName.INSTANCES, text: $t('Instances') },
|
||||
{ name: RouteName.ADMIN, text: t('Admin') },
|
||||
{ name: RouteName.INSTANCES, text: t('Instances') },
|
||||
{ text: instance.domain },
|
||||
]"
|
||||
/>
|
||||
<h1 class="text-2xl">{{ instance.domain }}</h1>
|
||||
<div
|
||||
class="grid md:grid-cols-2 xl:grid-cols-4 gap-2 content-center text-center mt-2"
|
||||
<section
|
||||
class="flex flex-wrap md:flex-nowrap items-center justify-between gap-4"
|
||||
>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.PROFILES,
|
||||
query: { domain: instance.domain },
|
||||
}"
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold" v-if="instance.instanceName">
|
||||
{{ instance.instanceName }}
|
||||
</h2>
|
||||
<h2 class="text-4xl font-bold" v-else>{{ instance.domain }}</h2>
|
||||
<p
|
||||
v-if="instance.instanceDescription"
|
||||
class="text-slate-700 dark:text-slate-400 my-4"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.personCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Profiles") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_GROUPS,
|
||||
query: { domain: instance.domain },
|
||||
}"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.groupCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Groups") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.followingsCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Followings") }}</span>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.followersCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Followers") }}</span>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{ name: RouteName.REPORTS, query: { domain: instance.domain } }"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.reportsCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Reports") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 font-semibold block">{{
|
||||
formatBytes(instance.mediaSize)
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Uploaded media size") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 grid xl:grid-cols-2 gap-4">
|
||||
<div
|
||||
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md"
|
||||
v-if="instance.hasRelay"
|
||||
>
|
||||
<button
|
||||
@click="
|
||||
removeInstanceFollow({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followedStatus == InstanceFollowStatus.APPROVED"
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Stop following instance") }}
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
removeInstanceFollow({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-else-if="instance.followedStatus == InstanceFollowStatus.PENDING"
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Cancel follow request") }}
|
||||
</button>
|
||||
<button
|
||||
@click="followInstance"
|
||||
v-else
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Follow instance") }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="md:h-48 py-16 text-center opacity-50">
|
||||
{{ $t("Only Mobilizon instances can be followed") }}
|
||||
</div>
|
||||
<div
|
||||
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md flex flex-col gap-2"
|
||||
>
|
||||
<button
|
||||
@click="
|
||||
acceptInstance({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
|
||||
class="bg-green-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Accept follow") }}
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
rejectInstance({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followerStatus != InstanceFollowStatus.NONE"
|
||||
class="bg-red-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Reject follow") }}
|
||||
</button>
|
||||
<p v-if="instance.followerStatus == InstanceFollowStatus.NONE">
|
||||
{{ $t("This instance doesn't follow yours.") }}
|
||||
{{ instance.instanceDescription }}
|
||||
</p>
|
||||
<i18n-t
|
||||
v-if="instance.software && instance.softwareVersion"
|
||||
keypath="Software details: {software_details}"
|
||||
class="my-4"
|
||||
>
|
||||
<template #software_details>
|
||||
<span class="capitalize">
|
||||
{{ instance.software }} - {{ instance.softwareVersion }}
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
<o-button
|
||||
tag="a"
|
||||
:href="`https://${instance.domain}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
icon-right="open-in-new"
|
||||
class="mx-auto md:mx-0"
|
||||
>{{
|
||||
t("Visit {instance_domain}", { instance_domain: instance.domain })
|
||||
}}
|
||||
</o-button>
|
||||
</section>
|
||||
<section>
|
||||
<div
|
||||
class="grid md:grid-cols-2 xl:grid-cols-4 gap-2 content-center text-center mt-2"
|
||||
>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.PROFILES,
|
||||
query: { domain: instance.domain },
|
||||
}"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.personCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ t("Profiles") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_GROUPS,
|
||||
query: { domain: instance.domain },
|
||||
}"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.groupCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ t("Groups") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.followingsCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ t("Followings") }}</span>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.followersCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ t("Followers") }}</span>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.REPORTS,
|
||||
query: { domain: instance.domain },
|
||||
}"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.reportsCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ t("Reports") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 font-semibold block">{{
|
||||
formatBytes(instance.mediaSize)
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ t("Uploaded media size") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="mt-3 grid xl:grid-cols-2 gap-4">
|
||||
<div
|
||||
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md"
|
||||
v-if="
|
||||
instance.hasRelay &&
|
||||
!['mastodon', 'peertube'].includes(
|
||||
instance.software?.toLowerCase() ?? ''
|
||||
)
|
||||
"
|
||||
>
|
||||
<button
|
||||
@click="
|
||||
removeInstanceFollow({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followedStatus == InstanceFollowStatus.APPROVED"
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ t("Stop following instance") }}
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
removeInstanceFollow({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-else-if="instance.followedStatus == InstanceFollowStatus.PENDING"
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ t("Cancel follow request") }}
|
||||
</button>
|
||||
<button
|
||||
@click="followInstance"
|
||||
v-else
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ t("Follow instance") }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="md:h-48 py-16 text-center opacity-50">
|
||||
{{ t("Only instances with an application actor can be followed") }}
|
||||
</div>
|
||||
<div
|
||||
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md flex flex-col gap-2"
|
||||
>
|
||||
<button
|
||||
@click="
|
||||
acceptInstance({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
|
||||
class="bg-green-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ t("Accept follow") }}
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
rejectInstance({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followerStatus != InstanceFollowStatus.NONE"
|
||||
class="bg-red-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ t("Reject follow") }}
|
||||
</button>
|
||||
<p v-if="instance.followerStatus == InstanceFollowStatus.NONE">
|
||||
{{ t("This instance doesn't follow yours.") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
@@ -152,6 +201,7 @@ import { InstanceFollowStatus } from "@/types/enums";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject } from "vue";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ domain: string }>();
|
||||
|
||||
@@ -164,6 +214,8 @@ const instance = computed(() => instanceResult.value?.instance);
|
||||
|
||||
const notifier = inject<Notifier>("notifier");
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const { mutate: acceptInstance, onError: onAcceptInstanceError } = useMutation(
|
||||
ACCEPT_RELAY,
|
||||
() => ({
|
||||
|
||||
@@ -72,21 +72,65 @@
|
||||
name: RouteName.INSTANCE,
|
||||
params: { domain: instance.domain },
|
||||
}"
|
||||
class="flex items-center mb-2 rounded bg-mbz-yellow-alt-300 dark:bg-mbz-purple-400 p-4 flex-wrap justify-center gap-x-2 gap-y-3"
|
||||
class="flex items-center mb-2 rounded bg-mbz-yellow-alt-300 hover:bg-mbz-yellow-alt-200 dark:bg-mbz-purple-600 dark:hover:bg-mbz-purple-700 p-4 flex-wrap justify-center gap-x-2 gap-y-3"
|
||||
v-for="instance in instances.elements"
|
||||
:key="instance.domain"
|
||||
>
|
||||
<div class="grow overflow-hidden flex items-center gap-1">
|
||||
<img
|
||||
class="w-12"
|
||||
v-if="instance.hasRelay"
|
||||
v-if="instance.software === 'Mobilizon'"
|
||||
src="/img/logo.svg"
|
||||
alt=""
|
||||
/>
|
||||
<CloudQuestion v-else :size="36" />
|
||||
<mastodon-logo
|
||||
class="w-8 mx-2"
|
||||
alt=""
|
||||
v-else-if="instance.software?.toLowerCase() === 'mastodon'"
|
||||
/>
|
||||
<img
|
||||
class="w-8 mx-2"
|
||||
v-else-if="instance.software?.toLowerCase() === 'gancio'"
|
||||
src="/img/gancio.png"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
class="w-8 mx-2"
|
||||
v-else-if="instance.software?.toLowerCase() === 'wordpress'"
|
||||
src="/img/wordpress-logo.svg"
|
||||
alt=""
|
||||
/>
|
||||
<CloudQuestion class="mx-1.5" v-else :size="36" />
|
||||
|
||||
<div class="">
|
||||
<h3 class="text-lg truncate">{{ instance.domain }}</h3>
|
||||
<h3
|
||||
class="text-lg truncate font-bold text-slate-800 dark:text-slate-100"
|
||||
v-if="instance.instanceName"
|
||||
>
|
||||
{{ instance.instanceName }}
|
||||
</h3>
|
||||
<h3
|
||||
class="text-lg truncate font-bold text-slate-800 dark:text-slate-100"
|
||||
v-else
|
||||
>
|
||||
{{ instance.domain }}
|
||||
</h3>
|
||||
<p
|
||||
v-if="instance.instanceName"
|
||||
class="inline-flex gap-2 text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
<span class="capitalize" v-if="instance.software">{{
|
||||
instance.software
|
||||
}}</span>
|
||||
-
|
||||
<span>{{ instance.domain }}</span>
|
||||
</p>
|
||||
<p
|
||||
v-else-if="instance.software"
|
||||
class="capitalize text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
{{ instance.software }}
|
||||
</p>
|
||||
<span
|
||||
class="text-sm"
|
||||
v-if="instance.followedStatus === InstanceFollowStatus.APPROVED"
|
||||
@@ -186,6 +230,7 @@ import { useRouter } from "vue-router";
|
||||
import { useHead } from "@unhead/vue";
|
||||
import CloudQuestion from "../../../node_modules/vue-material-design-icons/CloudQuestion.vue";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import MastodonLogo from "@/components/Share/MastodonLogo.vue";
|
||||
|
||||
const INSTANCES_PAGE_LIMIT = 10;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user