From 5445b023e5cedb7228710637c895c63328e3db74 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Tue, 14 Jan 2025 20:08:54 +0900
Subject: [PATCH] =?UTF-8?q?enhance:=20=E9=80=A3=E5=90=88=E3=83=A2=E3=83=BC?=
 =?UTF-8?q?=E3=83=89=E3=81=AB=E3=81=82=E3=82=8F=E3=81=9B=E3=81=A6=E3=83=95?=
 =?UTF-8?q?=E3=83=AD=E3=83=B3=E3=83=88=E3=82=A8=E3=83=B3=E3=83=89=E3=82=92?=
 =?UTF-8?q?=E5=A4=89=E5=8C=96=E3=81=95=E3=81=9B=E3=82=8B=E3=82=88=E3=81=86?=
 =?UTF-8?q?=E3=81=AB=20(#15112)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* enhance(backend): metaにfederation modeに関する情報を公開

* enhance(frontend): 登録画面の注意書きを追加

* enhance(frontend): aboutページ・サーバー情報

* enhance(frontend): サーバー統計

* enhance(frontend): みつけるページ

* enhance(frontend): 検索

* enhance(frontend): ユーザー選択

* enhance(frontend): 設定画面

* enhance(frontend): ウィジェット

* enhance(frontend): リモートで開くオプション

* Update Changelog

* enhance(frontend): ステータスバー

* i18n

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 CHANGELOG.md                                  |  1 +
 locales/index.d.ts                            |  8 ++++
 locales/ja-JP.yml                             |  2 +
 .../src/core/entities/MetaEntityService.ts    |  1 +
 .../backend/src/models/json-schema/meta.ts    |  5 ++
 .../src/components/MkInstanceStats.vue        | 18 ++++---
 .../src/components/MkSignupDialog.rules.vue   |  6 ++-
 .../src/components/MkUserSelectDialog.vue     | 11 +++--
 .../src/components/MkVisitorDashboard.vue     |  6 ++-
 .../frontend/src/components/MkWidgets.vue     | 19 ++++++--
 packages/frontend/src/pages/about.vue         | 47 ++++++++++++-------
 packages/frontend/src/pages/explore.users.vue |  3 +-
 packages/frontend/src/pages/search.note.vue   | 22 +++++----
 packages/frontend/src/pages/search.user.vue   |  5 +-
 .../frontend/src/pages/settings/general.vue   |  3 +-
 .../src/pages/settings/mute-block.vue         |  4 +-
 .../frontend/src/pages/settings/privacy.vue   |  9 ++--
 .../pages/settings/statusbar.statusbar.vue    |  3 +-
 packages/frontend/src/scripts/please-login.ts | 12 ++++-
 packages/frontend/src/ui/_common_/common.ts   | 18 ++++---
 .../frontend/src/ui/_common_/statusbars.vue   |  3 +-
 packages/frontend/src/widgets/index.ts        | 10 +++-
 packages/misskey-js/src/autogen/types.ts      |  2 +
 23 files changed, 150 insertions(+), 68 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index af5d333927..287d390453 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
 - Enhance: PC画面でチャンネルが複数列で表示されるように  
   (Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13)
 - Enhance: 照会に失敗した場合、その理由を表示するように
+- Enhance: 連合がホワイトリスト化・無効化されているサーバー向けのデザイン修正
 - Enhance: AiScriptのセーブデータを明示的に削除する関数`Mk:remove`を追加
 - Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に
 - Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 7c3ef5d93c..453d40feea 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5230,6 +5230,14 @@ export interface Locale extends ILocale {
      * 注意事項を理解した上でオンにします。
      */
     "acknowledgeNotesAndEnable": string;
+    /**
+     * このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。
+     */
+    "federationSpecified": string;
+    /**
+     * このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。
+     */
+    "federationDisabled": string;
     "_accountSettings": {
         /**
          * コンテンツの表示にログインを必須にする
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 57a88062c1..a3cb9d052a 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1303,6 +1303,8 @@ lockdown: "ロックダウン"
 pleaseSelectAccount: "アカウントを選択してください"
 availableRoles: "利用可能なロール"
 acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。"
+federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
+federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
 
 _accountSettings:
   requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index 409dca3426..ec0b5360f4 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -132,6 +132,7 @@ export class MetaEntityService {
 			enableUrlPreview: instance.urlPreviewEnabled,
 			noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
 			maxFileSize: this.config.maxFileSize,
+			federation: this.meta.federation,
 		};
 
 		return packed;
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index e3fd63464a..e7ae2ee8e5 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -261,6 +261,11 @@ export const packedMetaLiteSchema = {
 			type: 'number',
 			optional: false, nullable: false,
 		},
+		federation: {
+			type: 'string',
+			enum: ['all', 'specified', 'none'],
+			optional: false, nullable: false,
+		},
 	},
 } as const;
 
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 8ccbf61e48..d8066857fe 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.chart">
 			<div class="selects">
 				<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
-					<optgroup :label="i18n.ts.federation">
+					<optgroup v-if="shouldShowFederation" :label="i18n.ts.federation">
 						<option value="federation">{{ i18n.ts._charts.federation }}</option>
 						<option value="ap-request">{{ i18n.ts._charts.apRequest }}</option>
 					</optgroup>
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<optgroup :label="i18n.ts.notes">
 						<option value="notes">{{ i18n.ts._charts.notesIncDec }}</option>
 						<option value="local-notes">{{ i18n.ts._charts.localNotesIncDec }}</option>
-						<option value="remote-notes">{{ i18n.ts._charts.remoteNotesIncDec }}</option>
+						<option v-if="shouldShowFederation" value="remote-notes">{{ i18n.ts._charts.remoteNotesIncDec }}</option>
 						<option value="notes-total">{{ i18n.ts._charts.notesTotal }}</option>
 					</optgroup>
 					<optgroup :label="i18n.ts.drive">
@@ -46,9 +46,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
 			<option value="active-users">Active users</option>
 			<option value="notes">Notes</option>
-			<option value="ap-requests-inbox-received">AP Requests: inboxReceived</option>
-			<option value="ap-requests-deliver-succeeded">AP Requests: deliverSucceeded</option>
-			<option value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
+			<option v-if="shouldShowFederation" value="ap-requests-inbox-received">AP Requests: inboxReceived</option>
+			<option v-if="shouldShowFederation" value="ap-requests-deliver-succeeded">AP Requests: deliverSucceeded</option>
+			<option v-if="shouldShowFederation" value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
 		</MkSelect>
 		<div class="_panel" :class="$style.heatmap">
 			<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
@@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</MkFoldableSection>
 
-	<MkFoldableSection class="item">
+	<MkFoldableSection v-if="shouldShowFederation" class="item">
 		<template #header>Federation</template>
 		<div :class="$style.federation">
 			<div class="pies">
@@ -84,13 +84,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref, shallowRef } from 'vue';
+import { onMounted, ref, computed, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
 import MkSelect from '@/components/MkSelect.vue';
 import MkChart from '@/components/MkChart.vue';
 import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
+import { $i } from '@/account.js';
 import * as os from '@/os.js';
 import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import MkHeatmap, { type HeatmapSource } from '@/components/MkHeatmap.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
@@ -100,6 +102,8 @@ import { initChart } from '@/scripts/init-chart.js';
 
 initChart();
 
+const shouldShowFederation = computed(() => instance.federation !== 'none' || $i?.isModerator);
+
 const chartLimit = 500;
 const chartSpan = ref<'hour' | 'day'>('hour');
 const chartSrc = ref('active-users');
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue
index e2a06dd91f..999e843325 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.vue
+++ b/packages/frontend/src/components/MkSignupDialog.rules.vue
@@ -10,8 +10,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 	<MkSpacer :marginMin="20" :marginMax="28">
 		<div class="_gaps_m">
-			<div v-if="instance.disableRegistration">
-				<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
+			<div v-if="instance.disableRegistration || instance.federation !== 'all'" class="_gaps_s">
+				<MkInfo v-if="instance.disableRegistration" warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
+				<MkInfo v-if="instance.federation === 'specified'" warn>{{ i18n.ts.federationSpecified }}</MkInfo>
+				<MkInfo v-else-if="instance.federation === 'none'" warn>{{ i18n.ts.federationDisabled }}</MkInfo>
 			</div>
 
 			<div style="text-align: center;">
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index 764bf74f21..63af652cbc 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header>{{ i18n.ts.selectUser }}</template>
 	<div>
 		<div :class="$style.form">
-			<MkInput v-if="localOnly" v-model="username" :autofocus="true" @update:modelValue="search">
+			<MkInput v-if="computedLocalOnly" v-model="username" :autofocus="true" @update:modelValue="search">
 				<template #label>{{ i18n.ts.username }}</template>
 				<template #prefix>@</template>
 			</MkInput>
@@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref, shallowRef } from 'vue';
+import { onMounted, ref, computed, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkInput from '@/components/MkInput.vue';
 import FormSplit from '@/components/form/split.vue';
@@ -70,6 +70,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
+import { instance } from '@/instance.js';
 import { host as currentHost, hostname } from '@@/js/config.js';
 
 const emit = defineEmits<{
@@ -86,6 +87,8 @@ const props = withDefaults(defineProps<{
 	localOnly: false,
 });
 
+const computedLocalOnly = computed(() => props.localOnly || instance.federation === 'none');
+
 const username = ref('');
 const host = ref('');
 const users = ref<Misskey.entities.UserLite[]>([]);
@@ -100,7 +103,7 @@ function search() {
 	}
 	misskeyApi('users/search-by-username-and-host', {
 		username: username.value,
-		host: props.localOnly ? '.' : host.value,
+		host: computedLocalOnly.value ? '.' : host.value,
 		limit: 10,
 		detail: false,
 	}).then(_users => {
@@ -142,7 +145,7 @@ onMounted(() => {
 	}).then(foundUsers => {
 		let _users = foundUsers;
 		_users = _users.filter((u) => {
-			if (props.localOnly) {
+			if (computedLocalOnly.value) {
 				return u.host == null;
 			} else {
 				return true;
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index 97c765d81c..9554d6fff0 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -18,8 +18,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<!-- eslint-disable-next-line vue/no-v-html -->
 				<div v-html="instance.description || i18n.ts.headlineMisskey"></div>
 			</div>
-			<div v-if="instance.disableRegistration" :class="$style.mainWarn">
-				<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
+			<div v-if="instance.disableRegistration || instance.federation !== 'all'" :class="$style.mainWarn" class="_gaps_s">
+				<MkInfo v-if="instance.disableRegistration" warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
+				<MkInfo v-if="instance.federation === 'specified'" warn>{{ i18n.ts.federationSpecified }}</MkInfo>
+				<MkInfo v-else-if="instance.federation === 'none'" warn>{{ i18n.ts.federationDisabled }}</MkInfo>
 			</div>
 			<div class="_gaps_s" :class="$style.mainActions">
 				<MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton>
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index ba619f6063..44f6921a85 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<header :class="$style.editHeader">
 			<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
 				<template #label>{{ i18n.ts.selectWidget }}</template>
-				<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
+				<option v-for="widget in _widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
 			</MkSelect>
 			<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
 			<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 		</Sortable>
 	</template>
-	<component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
+	<component :is="`widget-${widget.name}`" v-for="widget in _widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
 </div>
 </template>
 
@@ -50,13 +50,14 @@ export type DefaultStoredWidget = {
 </script>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, ref } from 'vue';
+import { defineAsyncComponent, ref, computed } from 'vue';
 import { v4 as uuid } from 'uuid';
 import MkSelect from '@/components/MkSelect.vue';
 import MkButton from '@/components/MkButton.vue';
-import { widgets as widgetDefs } from '@/widgets/index.js';
+import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
 import { isLink } from '@@/js/is-link.js';
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@@ -66,6 +67,16 @@ const props = defineProps<{
 	edit: boolean;
 }>();
 
+const _widgetDefs = computed(() => {
+	if (instance.federation === 'none') {
+		return widgetDefs.filter(x => !federationWidgets.includes(x));
+	} else {
+		return widgetDefs;
+	}
+});
+
+const _widgets = computed(() => props.widgets.filter(x => _widgetDefs.value.includes(x.name)));
+
 const emit = defineEmits<{
 	(ev: 'updateWidgets', widgets: Widget[]): void;
 	(ev: 'addWidget', widget: Widget): void;
diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue
index 8dfeb6d2a7..ef0fd39ffe 100644
--- a/packages/frontend/src/pages/about.vue
+++ b/packages/frontend/src/pages/about.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
 			<XEmojis/>
 		</MkSpacer>
-		<MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20">
+		<MkSpacer v-else-if="instance.federation !== 'none' && tab === 'federation'" :contentMax="1000" :marginMin="20">
 			<XFederation/>
 		</MkSpacer>
 		<MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20">
@@ -25,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, defineAsyncComponent, ref, watch } from 'vue';
+import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -51,22 +52,34 @@ watch(tab, () => {
 
 const headerActions = computed(() => []);
 
-const headerTabs = computed(() => [{
-	key: 'overview',
-	title: i18n.ts.overview,
-}, {
-	key: 'emojis',
-	title: i18n.ts.customEmojis,
-	icon: 'ti ti-icons',
-}, {
-	key: 'federation',
-	title: i18n.ts.federation,
-	icon: 'ti ti-whirl',
-}, {
-	key: 'charts',
-	title: i18n.ts.charts,
-	icon: 'ti ti-chart-line',
-}]);
+const headerTabs = computed(() => {
+	const items = [];
+
+	items.push({
+		key: 'overview',
+		title: i18n.ts.overview,
+	}, {
+		key: 'emojis',
+		title: i18n.ts.customEmojis,
+		icon: 'ti ti-icons',
+	});
+
+	if (instance.federation !== 'none') {
+		items.push({
+			key: 'federation',
+			title: i18n.ts.federation,
+			icon: 'ti ti-whirl',
+		});
+	}
+
+	items.push({
+		key: 'charts',
+		title: i18n.ts.charts,
+		icon: 'ti ti-chart-line',
+	});
+
+	return items;
+});
 
 definePageMetadata(() => ({
 	title: i18n.ts.instanceInfo,
diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue
index c9acfec04f..56ae08b322 100644
--- a/packages/frontend/src/pages/explore.users.vue
+++ b/packages/frontend/src/pages/explore.users.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkSpacer :contentMax="1200">
-	<MkTab v-model="origin" style="margin-bottom: var(--MI-margin);">
+	<MkTab v-if="instance.federation !== 'none'" v-model="origin" style="margin-bottom: var(--MI-margin);">
 		<option value="local">{{ i18n.ts.local }}</option>
 		<option value="remote">{{ i18n.ts.remote }}</option>
 	</MkTab>
@@ -69,6 +69,7 @@ import MkUserList from '@/components/MkUserList.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkTab from '@/components/MkTab.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
+import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index 105c947d25..4cb149a58b 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -13,15 +13,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<template #header>{{ i18n.ts.options }}</template>
 
 			<div class="_gaps_m">
-				<MkRadios v-model="hostSelect">
-					<template #label>{{ i18n.ts.host }}</template>
-					<option value="all" default>{{ i18n.ts.all }}</option>
-					<option value="local">{{ i18n.ts.local }}</option>
-					<option v-if="noteSearchableScope === 'global'" value="specified">{{ i18n.ts.specifyHost }}</option>
-				</MkRadios>
-				<MkInput v-if="noteSearchableScope === 'global'" v-model="hostInput" :disabled="hostSelect !== 'specified'" :large="true" type="search">
-					<template #prefix><i class="ti ti-server"></i></template>
-				</MkInput>
+				<template v-if="instance.federation !== 'none'">
+					<MkRadios v-model="hostSelect">
+						<template #label>{{ i18n.ts.host }}</template>
+						<option value="all" default>{{ i18n.ts.all }}</option>
+						<option value="local">{{ i18n.ts.local }}</option>
+						<option v-if="noteSearchableScope === 'global'" value="specified">{{ i18n.ts.specifyHost }}</option>
+					</MkRadios>
+					<MkInput v-if="noteSearchableScope === 'global'" v-model="hostInput" :disabled="hostSelect !== 'specified'" :large="true" type="search">
+						<template #prefix><i class="ti ti-server"></i></template>
+					</MkInput>
+				</template>
 
 				<MkFolder :defaultOpen="true">
 					<template #label>{{ i18n.ts.specifyUser }}</template>
@@ -102,7 +104,7 @@ setHostSelectWithInput(hostInput.value, undefined);
 watch(hostInput, setHostSelectWithInput);
 
 const searchHost = computed(() => {
-	if (hostSelect.value === 'local') return '.';
+	if (hostSelect.value === 'local' || instance.federation === 'none') return '.';
 	if (hostSelect.value === 'specified') return hostInput.value;
 	return null;
 });
diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue
index 724fbfdfbd..e8bc4cd6d3 100644
--- a/packages/frontend/src/pages/search.user.vue
+++ b/packages/frontend/src/pages/search.user.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search">
 			<template #prefix><i class="ti ti-search"></i></template>
 		</MkInput>
-		<MkRadios v-model="searchOrigin" @update:modelValue="search()">
+		<MkRadios v-if="instance.federation !== 'none'" v-model="searchOrigin" @update:modelValue="search()">
 			<option value="combined">{{ i18n.ts.all }}</option>
 			<option value="local">{{ i18n.ts.local }}</option>
 			<option value="remote">{{ i18n.ts.remote }}</option>
@@ -33,6 +33,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
 import * as os from '@/os.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
@@ -113,7 +114,7 @@ async function search() {
 		limit: 10,
 		params: {
 			query: query,
-			origin: searchOrigin.value,
+			origin: instance.federation === 'none' ? 'local' : searchOrigin.value,
 		},
 	};
 
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index 1bfdfd0e76..1ee7909aa8 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -64,7 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch>
 			</div>
 
-			<MkSelect v-model="instanceTicker">
+			<MkSelect v-if="instance.federation !== 'none'" v-model="instanceTicker">
 				<template #label>{{ i18n.ts.instanceTicker }}</template>
 				<option value="none">{{ i18n.ts._instanceTicker.none }}</option>
 				<option value="remote">{{ i18n.ts._instanceTicker.remote }}</option>
@@ -263,6 +263,7 @@ import MkLink from '@/components/MkLink.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import { defaultStore } from '@/store.js';
 import * as os from '@/os.js';
+import { instance } from '@/instance.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { reloadAsk } from '@/scripts/reload-ask.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index 19dd2468f8..d32d4842bd 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</MkFolder>
 
-	<MkFolder>
+	<MkFolder v-if="instance.federation !== 'none'">
 		<template #icon><i class="ti ti-planet-off"></i></template>
 		<template #label>{{ i18n.ts.instanceMute }}</template>
 
@@ -142,7 +142,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { infoImageUrl } from '@/instance.js';
+import { instance, infoImageUrl } from '@/instance.js';
 import { signinRequired } from '@/account.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFolder from '@/components/MkFolder.vue';
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index 40d9be0f60..54a5aeb6c1 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #caption>
 					<div>{{ i18n.ts._accountSettings.requireSigninToViewContentsDescription1 }}</div>
 					<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription2 }}</div>
-					<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div>
+					<div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div>
 				</template>
 			</MkSwitch>
 
@@ -89,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 				<template #caption>
 					<div>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</div>
-					<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
+					<div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
 				</template>
 			</FormSlot>
 
@@ -125,7 +125,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 				<template #caption>
 					<div>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</div>
-					<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
+					<div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
 				</template>
 			</FormSlot>
 		</div>
@@ -167,6 +167,7 @@ import MkFolder from '@/components/MkFolder.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
 import { signinRequired } from '@/account.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import FormSlot from '@/components/form/slot.vue';
@@ -219,7 +220,7 @@ watch([makeNotesFollowersOnlyBefore, makeNotesHiddenBefore], () => {
 });
 
 async function update_requireSigninToViewContents(value: boolean) {
-	if (value) {
+	if (value === true && instance.federation !== 'none') {
 		const { canceled } = await os.confirm({
 			type: 'warning',
 			text: i18n.ts.acknowledgeNotesAndEnable,
diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
index 67943524ef..140b6beb14 100644
--- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkSelect v-model="statusbar.type" placeholder="Please select">
 		<template #label>{{ i18n.ts.type }}</template>
 		<option value="rss">RSS</option>
-		<option value="federation">Federation</option>
+		<option v-if="instance.federation !== 'none'" value="federation">Federation</option>
 		<option value="userList">User list timeline</option>
 	</MkSelect>
 
@@ -96,6 +96,7 @@ import MkButton from '@/components/MkButton.vue';
 import MkRange from '@/components/MkRange.vue';
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
 import { deepClone } from '@/scripts/clone.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts
index 43dcf11936..a8a330eb6d 100644
--- a/packages/frontend/src/scripts/please-login.ts
+++ b/packages/frontend/src/scripts/please-login.ts
@@ -5,6 +5,7 @@
 
 import { defineAsyncComponent } from 'vue';
 import { $i } from '@/account.js';
+import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import { popup } from '@/os.js';
 
@@ -51,10 +52,17 @@ export function pleaseLogin(opts: {
 } = {}) {
 	if ($i) return;
 
+	let _openOnRemote: OpenOnRemoteOptions | undefined = undefined;
+
+	// 連合できる場合と、(連合ができなくても)共有する場合は外部連携オプションを設定
+	if (opts.openOnRemote != null && (instance.federation !== 'none' || opts.openOnRemote.type === 'share')) {
+		_openOnRemote = opts.openOnRemote;
+	}
+
 	const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
 		autoSet: true,
-		message: opts.message ?? (opts.openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired),
-		openOnRemote: opts.openOnRemote,
+		message: opts.message ?? (_openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired),
+		openOnRemote: _openOnRemote,
 	}, {
 		cancelled: () => {
 			if (opts.path) {
diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts
index b31254196a..8e5ba8927a 100644
--- a/packages/frontend/src/ui/_common_/common.ts
+++ b/packages/frontend/src/ui/_common_/common.ts
@@ -56,12 +56,18 @@ export function openInstanceMenu(ev: MouseEvent) {
 		text: i18n.ts.customEmojis,
 		icon: 'ti ti-icons',
 		to: '/about#emojis',
-	}, {
-		type: 'link',
-		text: i18n.ts.federation,
-		icon: 'ti ti-whirl',
-		to: '/about#federation',
-	}, {
+	});
+
+	if (instance.federation !== 'none') {
+		menuItems.push({
+			type: 'link',
+			text: i18n.ts.federation,
+			icon: 'ti ti-whirl',
+			to: '/about#federation',
+		});
+	}
+
+	menuItems.push({
 		type: 'link',
 		text: i18n.ts.charts,
 		icon: 'ti ti-chart-line',
diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue
index 5f9a938017..ed881bef22 100644
--- a/packages/frontend/src/ui/_common_/statusbars.vue
+++ b/packages/frontend/src/ui/_common_/statusbars.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	>
 		<span :class="$style.name">{{ x.name }}</span>
 		<XRss v-if="x.type === 'rss'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :url="x.props.url" :shuffle="x.props.shuffle"/>
-		<XFederation v-else-if="x.type === 'federation'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :colored="x.props.colored"/>
+		<XFederation v-else-if="x.type === 'federation' && instance.federation !== 'none'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :colored="x.props.colored"/>
 		<XUserList v-else-if="x.type === 'userList'" :class="$style.body" :refreshIntervalSec="x.props.refreshIntervalSec" :marqueeDuration="x.props.marqueeDuration" :marqueeReverse="x.props.marqueeReverse" :display="x.props.display" :userListId="x.props.userListId"/>
 	</div>
 </div>
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { defineAsyncComponent } from 'vue';
+import { instance } from '@/instance.js';
 import { defaultStore } from '@/store.js';
 const XRss = defineAsyncComponent(() => import('./statusbar-rss.vue'));
 const XFederation = defineAsyncComponent(() => import('./statusbar-federation.vue'));
diff --git a/packages/frontend/src/widgets/index.ts b/packages/frontend/src/widgets/index.ts
index e269fcf9eb..c5be25e7df 100644
--- a/packages/frontend/src/widgets/index.ts
+++ b/packages/frontend/src/widgets/index.ts
@@ -36,6 +36,12 @@ export default function(app: App) {
 	app.component('WidgetBirthdayFollowings', defineAsyncComponent(() => import('./WidgetBirthdayFollowings.vue')));
 }
 
+// 連合関連のウィジェット(連合無効時に隠す)
+export const federationWidgets = [
+	'federation',
+	'instanceCloud',
+];
+
 export const widgets = [
 	'profile',
 	'instanceInfo',
@@ -51,8 +57,6 @@ export const widgets = [
 	'photos',
 	'digitalClock',
 	'unixClock',
-	'federation',
-	'instanceCloud',
 	'postForm',
 	'slideshow',
 	'serverMetric',
@@ -65,4 +69,6 @@ export const widgets = [
 	'userList',
 	'clicker',
 	'birthdayFollowings',
+
+	...federationWidgets,
 ];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index e6a9df3f5a..ada685604d 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5047,6 +5047,8 @@ export type components = {
        */
       noteSearchableScope: 'local' | 'global';
       maxFileSize: number;
+      /** @enum {string} */
+      federation: 'all' | 'specified' | 'none';
     };
     MetaDetailedOnly: {
       features?: {
-- 
GitLab