(add) シェアボタン中継: おすすめサーバー機能

close #82
This commit is contained in:
kakkokari-gtyih 2023-12-25 01:12:21 +09:00
parent e4c8e16cf4
commit f6235f2c3c
5 changed files with 156 additions and 14 deletions

View File

@ -9,34 +9,59 @@
<div class="w-12 h-12 mx-auto bg-accent-600/20 text-accent-600 rounded-full p-3"> <div class="w-12 h-12 mx-auto bg-accent-600/20 text-accent-600 rounded-full p-3">
<component :is="branding?.icon ?? ShareIco" class="w-6 h-6" /> <component :is="branding?.icon ?? ShareIco" class="w-6 h-6" />
</div> </div>
<h1 class="font-bold text-center text-lg md:text-xl">{{ branding?.heading ?? $t('_share.chooseServer') }}</h1> <h1 class="font-bold text-center text-lg sm:text-xl">{{ branding?.heading ?? $t('_share.chooseServer') }}</h1>
<div>
<div class="text-xs sm:text-sm mb-1 opacity-70">
{{ $t('_share.recommendedByWebsite') }}
<NuxtLink :to="localePath('/docs/for-users/features/share-form/#hub-share-disclaimer')" target="_blank"><HelpIco class="ml-1" /></NuxtLink>
</div>
<div v-if="manualInstanceData" class="rounded-lg border border-gray-300 dark:border-gray-600 group">
<GNuxtLink
:to="joinURL(`https://${manualInstanceData.url}/`, path)"
class="group-first:rounded-t-lg group-last:rounded-b-lg py-2 px-4 sm:p-4 w-full flex items-center hover:bg-gray-100 active:bg-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-600"
>
<div
class="h-6 w-6 sm:h-9 sm:w-9 flex-shrink-0 overflow-hidden rounded bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 mr-3"
>
<img v-if="manualInstanceData.icon && manualInstanceData.meta?.iconUrl" :src="manualInstanceData.meta?.iconUrl" class="w-full h-full" />
</div>
<div class="min-w-0 mr-3">
<h2 class="text-sm sm:text-base font-bold truncate">{{ manualInstanceData.name }}</h2>
<p class="text-xs truncate">{{ manualInstanceData.url }}</p>
</div>
<ArrowRightIco class="block ml-auto flex-shrink-0 h-4 w-4" />
</GNuxtLink>
</div>
</div>
<ul class="rounded-lg border divide-y border-gray-300 dark:border-gray-600 divide-gray-300 dark:divide-gray-600"> <ul class="rounded-lg border divide-y border-gray-300 dark:border-gray-600 divide-gray-300 dark:divide-gray-600">
<li v-for="instance in displayInstances" :key="instance.url" class="group"> <li v-for="instance in displayInstances" :key="instance.url" class="group">
<GNuxtLink <GNuxtLink
:to="joinURL(`https://${instance.url}/`, path)" :to="joinURL(`https://${instance.url}/`, path)"
class="group-first:rounded-t-lg group-last:rounded-b-lg p-4 w-full flex items-center hover:bg-gray-100 active:bg-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-600" class="group-first:rounded-t-lg group-last:rounded-b-lg py-2 px-4 sm:p-4 w-full flex items-center hover:bg-gray-100 active:bg-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-600"
> >
<div <div
class="h-9 w-9 flex-shrink-0 overflow-hidden rounded bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 mr-3" class="h-6 w-6 sm:h-9 sm:w-9 flex-shrink-0 overflow-hidden rounded bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 mr-3"
:class="instance.isUserDefined && 'transition-[border-color] hover:border-red-600 dark:hover:border-red-600'" :class="instance.isUserDefined && 'transition-[border-color] hover:border-red-600 dark:hover:border-red-600'"
> >
<template v-if="instance.isUserDefined"> <template v-if="instance.isUserDefined">
<button class="relative w-full h-full group/delete" @click.stop.prevent="deleteInstance(instance.url)"> <button class="relative w-full h-full group/delete" @click.stop.prevent="deleteInstance(instance.url)">
<img v-if="instance.icon && instance.meta?.iconUrl" :src="getInstanceImage(instance)" class="w-full h-full" /> <img v-if="instance.icon && instance.meta?.iconUrl" :src="getInstanceImage(instance)" class="w-full h-full" />
<div v-else-if="['forked', 'notDetermined'].includes(getPlaceholderImage(instance))" class="w-full h-full bg-accent-600/20 p-2 text-accent-600"> <div v-else-if="['forked', 'notDetermined'].includes(getPlaceholderImage(instance))" class="w-full h-full bg-accent-600/20 p-1 sm:p-2 text-accent-600">
<ForkedIco v-if="getPlaceholderImage(instance) === 'forked'" class="bi h-5 w-5 stroke-1 stroke-current" /> <ForkedIco v-if="getPlaceholderImage(instance) === 'forked'" class="block h-4 w-4 sm:h-5 sm:w-5 stroke-1 stroke-current" />
<QuestionIco v-else class="h-5 w-5" /> <QuestionIco v-else class="block h-4 w-4 sm:h-5 sm:w-5" />
</div> </div>
<img v-else :src="getPlaceholderImage(instance)" class="w-full h-full" /> <img v-else :src="getPlaceholderImage(instance)" class="w-full h-full" />
<div class="pointer-events-none absolute top-0 left-0 w-full h-full bg-white dark:bg-gray-800 transition-opacity opacity-0 group-hover/delete:opacity-100 p-2 text-red-600"> <div class="pointer-events-none absolute top-0 left-0 w-full h-full bg-white dark:bg-gray-800 transition-opacity opacity-0 group-hover/delete:opacity-100 p-1 sm:p-2 text-red-600">
<DeleteIco class="h-5 w-5" /> <DeleteIco class="block h-4 w-4 sm:h-5 sm:w-5" />
</div> </div>
</button> </button>
</template> </template>
<img v-else-if="instance.icon && instance.meta?.iconUrl" :src="instance.meta?.iconUrl" class="w-full h-full" /> <img v-else-if="instance.icon && instance.meta?.iconUrl" :src="instance.meta?.iconUrl" class="w-full h-full" />
</div> </div>
<div class="min-w-0 mr-3"> <div class="min-w-0 mr-3">
<h2 class="font-bold truncate">{{ instance.name }}</h2> <h2 class="text-sm sm:text-base font-bold truncate">{{ instance.name }}</h2>
<p class="text-xs truncate">{{ instance.url }}</p> <p class="text-xs truncate">{{ instance.url }}</p>
</div> </div>
<ArrowRightIco class="block ml-auto flex-shrink-0 h-4 w-4" /> <ArrowRightIco class="block ml-auto flex-shrink-0 h-4 w-4" />
@ -45,9 +70,9 @@
<li class="group"> <li class="group">
<details class="group-first:rounded-t-lg group-last:rounded-b-lg group/details"> <details class="group-first:rounded-t-lg group-last:rounded-b-lg group/details">
<summary class="p-4 w-full flex items-center hover:bg-gray-100 dark:hover:bg-gray-700 group-first:rounded-t-lg group-last:rounded-b-lg group-last:group-open/details:rounded-b-none group-open/details:border-b cursor-pointer border-gray-300 dark:border-gray-600"> <summary class="p-4 w-full flex items-center hover:bg-gray-100 dark:hover:bg-gray-700 group-first:rounded-t-lg group-last:rounded-b-lg group-last:group-open/details:rounded-b-none group-open/details:border-b cursor-pointer border-gray-300 dark:border-gray-600">
<div class="h-9 w-9 mr-2"> <div class="h-6 w-6 sm:h-9 sm:w-9 mr-2">
<div class="w-full h-full rounded bg-accent-600/20 p-2 text-accent-600"> <div class="w-full h-full rounded bg-accent-600/20 p-1 sm:p-2 text-accent-600">
<PlusIco class="h-5 w-5 stroke-1 stroke-current" /> <PlusIco class="block h-4 w-4 sm:h-5 sm:w-5 stroke-1 stroke-current" />
</div> </div>
</div> </div>
<h2 class="font-bold">{{ $t('_share.addServer') }}</h2> <h2 class="font-bold">{{ $t('_share.addServer') }}</h2>
@ -82,6 +107,7 @@ import ArrowRightIco from 'bi/chevron-right.svg';
import PlusIco from 'bi/plus-lg.svg'; import PlusIco from 'bi/plus-lg.svg';
import DeleteIco from 'bi/trash.svg'; import DeleteIco from 'bi/trash.svg';
import QuestionIco from 'bi/question-lg.svg'; import QuestionIco from 'bi/question-lg.svg';
import HelpIco from 'bi/question-circle.svg';
import ForkedIco from '@/assets/svg/repo-forked.svg'; import ForkedIco from '@/assets/svg/repo-forked.svg';
import { isLocalPath, resolveObjPath } from '@/assets/js/misc'; import { isLocalPath, resolveObjPath } from '@/assets/js/misc';
import { parseURL, joinURL } from 'ufo'; import { parseURL, joinURL } from 'ufo';
@ -91,6 +117,7 @@ import type { InstanceInfo, InstanceItem } from '@/types/instances-info';
import type { FunctionalComponent } from 'vue'; import type { FunctionalComponent } from 'vue';
const { t } = useI18n(); const { t } = useI18n();
const localePath = useGLocalePath();
const props = defineProps<{ const props = defineProps<{
path: string; path: string;
@ -98,6 +125,7 @@ const props = defineProps<{
heading?: string; heading?: string;
icon?: FunctionalComponent | string; icon?: FunctionalComponent | string;
}; };
manualInstance?: string;
}>(); }>();
type ExtendedInstanceItem = InstanceItem & { type ExtendedInstanceItem = InstanceItem & {
@ -106,6 +134,7 @@ type ExtendedInstanceItem = InstanceItem & {
const loading = ref(true); const loading = ref(true);
const iFetching = ref(false); const iFetching = ref(false);
const manualInstanceData = ref<ExtendedInstanceItem>();
const featuredInstances = ref<ExtendedInstanceItem[]>([]); const featuredInstances = ref<ExtendedInstanceItem[]>([]);
const userDefinedInstances = ref<ExtendedInstanceItem[]>([]); const userDefinedInstances = ref<ExtendedInstanceItem[]>([]);
const displayInstances = computed(() => [ const displayInstances = computed(() => [
@ -203,6 +232,43 @@ onMounted(async () => {
const lsJ = JSON.parse(ls) as ExtendedInstanceItem[]; const lsJ = JSON.parse(ls) as ExtendedInstanceItem[];
userDefinedInstances.value = [...lsJ]; userDefinedInstances.value = [...lsJ];
} }
if (props.manualInstance) {
const realHost = parseURL(props.manualInstance.startsWith('https://') ? props.manualInstance : 'https://' + props.manualInstance);
if (!realHost.host) {
alert(t('_servers._system.fetchError'));
return;
}
const miApi = new misskeyApi.APIClient({
origin: `https://${realHost.host}`,
});
try {
const res = await miApi.request('meta', { detail: true });
manualInstanceData.value = {
background: !(!res.backgroundImageUrl),
banner: !(!res.bannerUrl),
description: res.description,
icon: !(!res.iconUrl),
isAlive: true,
langs: res.langs,
meta: res,
name: res.name ?? '',
nodeinfo: null,
npd15: 0,
stats: {},
url: realHost.host ?? '',
value: 0,
isUserDefined: true,
};
} catch (err) {
console.error(err);
}
}
} }
loading.value = false; loading.value = false;

View File

@ -51,3 +51,42 @@ URLにクエリパラメータとして共有内容をはじめとするいく
| 名前 | 説明 | | 名前 | 説明 |
| ---- | ---- | | ---- | ---- |
| `fileIds` | 添付するファイルのID(カンマ区切り) | | `fileIds` | 添付するファイルのID(カンマ区切り) |
## Misskey Hubの共有フォーム中継サービスについて
<a name="hub-share-disclaimer" id="hub-share-disclaimer"></a>
新Misskey Hubでは、Misskeyのシェアボタンの設置にかかる煩雑な手間を減らすために、共有フォームの中継サービスを提供しています。
こちらのサービスは、無料でどなたでもお使いいただけます。
今までの共有フォームのリンクの各サーバーのドメイン部分を `misskey-hub.net` に変更するだけで、様々なMisskeyサーバーへの共有リンクへと進化させることができます
:::tip
[共有ボタンジェネレーター](/tools/share-link-generator/) も併せてお使いください。
:::
:::warning
共有フォーム中継サービス以下、「本サービス」というはWebサイト管理者の便宜のためにMisskey Development Division以下、「当方」というが無償・無保証で提供する機能です。本サービスを利用したこと、または何らかの原因によりこれをご利用できなかったことにより生じたいかなる損害について、当方は一切の責任を負いません。
:::
### 基本のパラメータ
基本的に上記で紹介されているパラメーターをそのままお使いいただけますが、ユーザーIDやファイルIDなど、 **各サーバーに依存するパラメーターは使用できません。** それらが指定されていた場合、Misskey Hub上で削除されます。
### 独自機能
#### おすすめサーバー機能
URLパラメータ `manualInstance` にMisskeyサーバーのドメインを入力することで、「シェア元Webサイトからのおすすめ」として、別枠でそのサーバーへのリンクを設置することができます。ご自身のサーバーに誘導する際などにお使いいただけます。
:::warning
「おすすめサーバー機能」はWebサイト管理者の便宜のために設置してある機能であり、当方が「シェア元Webサイトからのおすすめ」欄にあるサーバーをおすすめしているものではございません。
「シェア元Webサイトからのおすすめ」から遷移したサーバーを利用・登録したことに起因するいかなる損害・不利益について、当方では責任を負いかねます。
:::

View File

@ -268,6 +268,7 @@ _share:
addServer: "サーバーを追加" addServer: "サーバーを追加"
domain: "サーバーのドメイン" domain: "サーバーのドメイン"
compatibleWith: "Misskeyと、一部のMisskeyフォークに対応しています。" compatibleWith: "Misskeyと、一部のMisskeyフォークに対応しています。"
recommendedByWebsite: "シェア元Webサイトからのおすすめ"
_noteVisibility: _noteVisibility:
public: "パブリック" public: "パブリック"
@ -284,6 +285,8 @@ _shareLinkGenerator:
url: "URL" url: "URL"
urlCaption: "任意。本文の後ろに挿入されます。" urlCaption: "任意。本文の後ろに挿入されます。"
settings: "詳細設定" settings: "詳細設定"
manualInstance: "おすすめサーバー ドメイン"
manualInstanceDescription: "ここに指定したサーバー(ひとつ)をシェアボタン中継ページのトップに表示できます。ご自身のサーバーに誘導する際などにお使いいただけます。"
visibility: "公開範囲" visibility: "公開範囲"
recipents: "ダイレクトを受け取る人のacct改行区切り" recipents: "ダイレクトを受け取る人のacct改行区切り"
resultLink: "リンク生成結果" resultLink: "リンク生成結果"

View File

@ -1,6 +1,14 @@
<template> <template>
<MkAnimBg
v-if="showAnimBg"
class="fixed z-0 top-0 left-0 w-screen h-screen transition-opacity duration-[2s]"
:class="isCanvasLoaded ? 'opacity-100' : 'opacity-0'"
@load="isCanvasLoaded = true"
></MkAnimBg>
<GMisskeyGateway <GMisskeyGateway
:path="`/share?${stringifyQuery(query)}`" class="relative"
:path="`/share?${stringifyQuery(filteredQuery)}`"
:manualInstance="manualInstance"
></GMisskeyGateway> ></GMisskeyGateway>
</template> </template>
@ -18,6 +26,25 @@ useHead({
}); });
const { meta, query } = useRoute(); const { meta, query } = useRoute();
const manualInstance = (Array.isArray(query.manualInstance) ? query.manualInstance[0] : query.manualInstance) ?? undefined;
const filteredQuery = computed(() => ({
...query,
replyId: undefined,
renoteId: undefined,
visibleUserIds: undefined,
fileIds: undefined,
manualInstance: undefined,
}));
const isCanvasLoaded = ref(false);
const showAnimBg = ref(false);
if (process.client && window.innerWidth >= 768) {
showAnimBg.value = true;
}
const { t } = useI18n(); const { t } = useI18n();
meta.title = t('_share.title'); meta.title = t('_share.title');

View File

@ -44,6 +44,11 @@
<header class="-mt-6 -mx-6 px-6 py-3 font-bold text-lg border-b"> <header class="-mt-6 -mx-6 px-6 py-3 font-bold text-lg border-b">
{{ $t('_shareLinkGenerator.settings') }} {{ $t('_shareLinkGenerator.settings') }}
</header> </header>
<div>
<label for="shareLinkGeneratorManualInstance">{{ $t('_shareLinkGenerator.manualInstance') }}</label>
<input class="form-control" id="shareLinkGeneratorManualInstance" v-model="manualInstance" />
<div class="form-text">{{ $t('_shareLinkGenerator.manualInstanceDescription') }}</div>
</div>
<div> <div>
<label for="shareLinkGeneratorVisibility">{{ $t('_shareLinkGenerator.visibility') }}</label> <label for="shareLinkGeneratorVisibility">{{ $t('_shareLinkGenerator.visibility') }}</label>
<select class="form-select" id="shareLinkGeneratorVisibility" v-model="visibility"> <select class="form-select" id="shareLinkGeneratorVisibility" v-model="visibility">
@ -107,6 +112,7 @@ const mfmUrl = ref<string>('');
const visibility = ref<MisskeyEntities.Note['visibility']>('public'); const visibility = ref<MisskeyEntities.Note['visibility']>('public');
const localOnly = ref<boolean>(false); const localOnly = ref<boolean>(false);
const recipents = ref<string>(''); const recipents = ref<string>('');
const manualInstance = ref<string>('');
const runtimeConfig = useRuntimeConfig(); const runtimeConfig = useRuntimeConfig();
const mfmPreviewString = computed(() => { const mfmPreviewString = computed(() => {
@ -126,10 +132,11 @@ const mfmPreviewString = computed(() => {
const generatedUrl = computed(() => { const generatedUrl = computed(() => {
const query: QueryObject = { const query: QueryObject = {
text: mfmText.value, text: mfmText.value,
url: (mfmUrl.value != '') ? mfmUrl.value : undefined, url: mfmUrl.value ? mfmUrl.value : undefined,
visibility: visibility.value, visibility: visibility.value,
localOnly: localOnly.value ? '1' : '0', localOnly: localOnly.value ? '1' : '0',
visibleAccts: (visibility.value === 'specified') ? recipents.value.split('\n').join(',') : undefined, visibleAccts: (visibility.value === 'specified') ? recipents.value.split('\n').join(',') : undefined,
manualInstance: manualInstance.value ? manualInstance.value : undefined,
}; };
const baseUrl = runtimeConfig.public.baseUrl; const baseUrl = runtimeConfig.public.baseUrl;