From aa72db76c4ce5c90ea502aca0ca0cc5d5b18829b Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Fri, 16 Feb 2024 12:24:35 +0000
Subject: [PATCH] rework boost visibility #388

* only show allowed visibilities
* "local only" is a switch

notice that the backend will limit the visibility, too
---
 packages/frontend/src/components/MkNote.vue   | 60 ++------------
 .../src/components/MkNoteDetailed.vue         | 59 ++------------
 .../frontend/src/components/MkNoteSub.vue     | 47 ++---------
 packages/frontend/src/components/SkNote.vue   | 60 ++------------
 .../src/components/SkNoteDetailed.vue         | 59 ++------------
 .../frontend/src/components/SkNoteSub.vue     | 47 ++---------
 .../frontend/src/pages/settings/general.vue   |  1 -
 packages/frontend/src/scripts/boost-quote.ts  | 81 +++++++++++++++++++
 packages/frontend/src/store.ts                |  2 +-
 9 files changed, 118 insertions(+), 298 deletions(-)
 create mode 100644 packages/frontend/src/scripts/boost-quote.ts

diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index ff9bf3c395..158fdc2c36 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -216,6 +216,7 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import { shouldCollapsed } from '@/scripts/collapsed.js';
 import { useRouter } from '@/router/supplier.js';
+import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
@@ -407,58 +408,15 @@ if (!props.mock) {
 	}
 }
 
-type Visibility = 'public' | 'home' | 'followers' | 'specified';
-
-// defaultStore.state.visibilityがstringなためstringも受け付けている
-function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility {
-	if (a === 'specified' || b === 'specified') return 'specified';
-	if (a === 'followers' || b === 'followers') return 'followers';
-	if (a === 'home' || b === 'home') return 'home';
-	// if (a === 'public' || b === 'public')
-	return 'public';
-}
-
 function boostVisibility() {
 	if (!defaultStore.state.showVisibilitySelectorOnBoost) {
 		renote(defaultStore.state.visibilityOnBoost);
 	} else {
-		os.popupMenu([
-			{
-				type: 'button',
-				icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
-				text: i18n.ts._visibility['public'],
-				action: () => {
-					renote('public');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-house ph-bold ph-lg',
-				text: i18n.ts._visibility['home'],
-				action: () => {
-					renote('home');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-lock ph-bold ph-lg',
-				text: i18n.ts._visibility['followers'],
-				action: () => {
-					renote('followers');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-planet ph-bold ph-lg',
-				text: i18n.ts._timelines.local,
-				action: () => {
-					renote('local');
-				},
-			}], renoteButton.value);
+		os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
 	}
 }
 
-function renote(visibility: Visibility | 'local') {
+function renote(visibility: Visibility, localOnly: boolean = false) {
 	pleaseLogin();
 	showMovedDialog();
 
@@ -489,18 +447,10 @@ function renote(visibility: Visibility | 'local') {
 			os.popup(MkRippleEffect, { x, y }, {}, 'end');
 		}
 
-		const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
-		const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
-
-		let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
-		if (appearNote.value.channel?.isSensitive) {
-			noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home');
-		}
-
 		if (!props.mock) {
 			misskeyApi('notes/create', {
-				localOnly: visibility === 'local' ? true : localOnlySetting,
-				visibility: noteVisibility,
+				localOnly: localOnly,
+				visibility: visibility,
 				renoteId: appearNote.value.id,
 			}).then(() => {
 				os.toast(i18n.ts.renoted);
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index a2e3a747d9..28c1f5d6ff 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -257,6 +257,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkPagination, { type Paging } from '@/components/MkPagination.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import MkButton from '@/components/MkButton.vue';
+import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
 
 const props = defineProps<{
 	note: Misskey.entities.Note;
@@ -429,57 +430,15 @@ useTooltip(quoteButton, async (showing) => {
 	}, {}, 'closed');
 });
 
-type Visibility = 'public' | 'home' | 'followers' | 'specified';
-
-function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility {
-	if (a === 'specified' || b === 'specified') return 'specified';
-	if (a === 'followers' || b === 'followers') return 'followers';
-	if (a === 'home' || b === 'home') return 'home';
-	// if (a === 'public' || b === 'public')
-	return 'public';
-}
-
 function boostVisibility() {
 	if (!defaultStore.state.showVisibilitySelectorOnBoost) {
 		renote(defaultStore.state.visibilityOnBoost);
 	} else {
-		os.popupMenu([
-			{
-				type: 'button',
-				icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
-				text: i18n.ts._visibility['public'],
-				action: () => {
-					renote('public');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-house ph-bold ph-lg',
-				text: i18n.ts._visibility['home'],
-				action: () => {
-					renote('home');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-lock ph-bold ph-lg',
-				text: i18n.ts._visibility['followers'],
-				action: () => {
-					renote('followers');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-planet ph-bold ph-lg',
-				text: i18n.ts._timelines.local,
-				action: () => {
-					renote('local');
-				},
-			}], renoteButton.value);
+		os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
 	}
 }
 
-function renote(visibility: Visibility | 'local') {
+function renote(visibility: Visibility, localOnly: boolean = false) {
 	pleaseLogin();
 	showMovedDialog();
 
@@ -508,17 +467,9 @@ function renote(visibility: Visibility | 'local') {
 			os.popup(MkRippleEffect, { x, y }, {}, 'end');
 		}
 
-		const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
-		const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
-
-		let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
-		if (appearNote.value.channel?.isSensitive) {
-			noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home');
-		}
-
 		misskeyApi('notes/create', {
-			localOnly: visibility === 'local' ? true : localOnlySetting,
-			visibility: noteVisibility,
+			localOnly: localOnly,
+			visibility: visibility,
 			renoteId: appearNote.value.id,
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index bb30fe53cf..6bd99b845c 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -105,6 +105,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 import { getNoteMenu } from '@/scripts/get-note-menu.js';
 import { useNoteCapture } from '@/scripts/use-note-capture.js';
+import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
 
 const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
 
@@ -276,43 +277,11 @@ function boostVisibility() {
 	if (!defaultStore.state.showVisibilitySelectorOnBoost) {
 		renote(defaultStore.state.visibilityOnBoost);
 	} else {
-		os.popupMenu([
-			{
-				type: 'button',
-				icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
-				text: i18n.ts._visibility['public'],
-				action: () => {
-					renote('public');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-house ph-bold ph-lg',
-				text: i18n.ts._visibility['home'],
-				action: () => {
-					renote('home');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-lock ph-bold ph-lg',
-				text: i18n.ts._visibility['followers'],
-				action: () => {
-					renote('followers');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-planet ph-bold ph-lg',
-				text: i18n.ts._timelines.local,
-				action: () => {
-					renote('local');
-				},
-			}], renoteButton.value);
+		os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
 	}
 }
 
-function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'local') {
+function renote(visibility: Visibility, localOnly: boolean = false) {
 	pleaseLogin();
 	showMovedDialog();
 
@@ -326,8 +295,8 @@ function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'loc
 		}
 
 		misskeyApi('notes/create', {
-			renoteId: props.note.id,
-			channelId: props.note.channelId,
+			renoteId: appearNote.value.id,
+			channelId: appearNote.value.channelId,
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
@@ -342,9 +311,9 @@ function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'loc
 		}
 
 		misskeyApi('notes/create', {
-			renoteId: props.note.id,
-			localOnly: visibility === 'local' ? true : false,
-			visibility: visibility === 'local' || visibility === 'specified' ? props.note.visibility : visibility,
+			renoteId: appearNote.value.id,
+			localOnly: localOnly,
+			visibility: visibility,
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue
index dc74c4928d..09decad1a2 100644
--- a/packages/frontend/src/components/SkNote.vue
+++ b/packages/frontend/src/components/SkNote.vue
@@ -217,6 +217,7 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import { shouldCollapsed } from '@/scripts/collapsed.js';
 import { useRouter } from '@/router/supplier.js';
+import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
@@ -408,58 +409,15 @@ if (!props.mock) {
 	}
 }
 
-type Visibility = 'public' | 'home' | 'followers' | 'specified';
-
-// defaultStore.state.visibilityがstringなためstringも受け付けている
-function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility {
-	if (a === 'specified' || b === 'specified') return 'specified';
-	if (a === 'followers' || b === 'followers') return 'followers';
-	if (a === 'home' || b === 'home') return 'home';
-	// if (a === 'public' || b === 'public')
-	return 'public';
-}
-
 function boostVisibility() {
 	if (!defaultStore.state.showVisibilitySelectorOnBoost) {
 		renote(defaultStore.state.visibilityOnBoost);
 	} else {
-		os.popupMenu([
-			{
-				type: 'button',
-				icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
-				text: i18n.ts._visibility['public'],
-				action: () => {
-					renote('public');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-house ph-bold ph-lg',
-				text: i18n.ts._visibility['home'],
-				action: () => {
-					renote('home');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-lock ph-bold ph-lg',
-				text: i18n.ts._visibility['followers'],
-				action: () => {
-					renote('followers');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-planet ph-bold ph-lg',
-				text: i18n.ts._timelines.local,
-				action: () => {
-					renote('local');
-				},
-			}], renoteButton.value);
+		os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
 	}
 }
 
-function renote(visibility: Visibility | 'local') {
+function renote(visibility: Visibility, localOnly: boolean = false) {
 	pleaseLogin();
 	showMovedDialog();
 
@@ -490,18 +448,10 @@ function renote(visibility: Visibility | 'local') {
 			os.popup(MkRippleEffect, { x, y }, {}, 'end');
 		}
 
-		const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
-		const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
-
-		let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
-		if (appearNote.value.channel?.isSensitive) {
-			noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home');
-		}
-
 		if (!props.mock) {
 			misskeyApi('notes/create', {
-				localOnly: visibility === 'local' ? true : localOnlySetting,
-				visibility: noteVisibility,
+				localOnly: localOnly,
+				visibility: visibility,
 				renoteId: appearNote.value.id,
 			}).then(() => {
 				os.toast(i18n.ts.renoted);
diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue
index ef17a2e8a6..80d6ac369e 100644
--- a/packages/frontend/src/components/SkNoteDetailed.vue
+++ b/packages/frontend/src/components/SkNoteDetailed.vue
@@ -265,6 +265,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkPagination, { type Paging } from '@/components/MkPagination.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import MkButton from '@/components/MkButton.vue';
+import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
 
 const props = defineProps<{
 	note: Misskey.entities.Note;
@@ -438,57 +439,15 @@ useTooltip(quoteButton, async (showing) => {
 	}, {}, 'closed');
 });
 
-type Visibility = 'public' | 'home' | 'followers' | 'specified';
-
-function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility {
-	if (a === 'specified' || b === 'specified') return 'specified';
-	if (a === 'followers' || b === 'followers') return 'followers';
-	if (a === 'home' || b === 'home') return 'home';
-	// if (a === 'public' || b === 'public')
-	return 'public';
-}
-
 function boostVisibility() {
 	if (!defaultStore.state.showVisibilitySelectorOnBoost) {
 		renote(defaultStore.state.visibilityOnBoost);
 	} else {
-		os.popupMenu([
-			{
-				type: 'button',
-				icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
-				text: i18n.ts._visibility['public'],
-				action: () => {
-					renote('public');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-house ph-bold ph-lg',
-				text: i18n.ts._visibility['home'],
-				action: () => {
-					renote('home');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-lock ph-bold ph-lg',
-				text: i18n.ts._visibility['followers'],
-				action: () => {
-					renote('followers');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-planet ph-bold ph-lg',
-				text: i18n.ts._timelines.local,
-				action: () => {
-					renote('local');
-				},
-			}], renoteButton.value);
+		os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
 	}
 }
 
-function renote(visibility: Visibility | 'local') {
+function renote(visibility: Visibility, localOnly: boolean = false) {
 	pleaseLogin();
 	showMovedDialog();
 
@@ -517,17 +476,9 @@ function renote(visibility: Visibility | 'local') {
 			os.popup(MkRippleEffect, { x, y }, {}, 'end');
 		}
 
-		const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
-		const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
-
-		let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
-		if (appearNote.value.channel?.isSensitive) {
-			noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home');
-		}
-
 		misskeyApi('notes/create', {
-			localOnly: visibility === 'local' ? true : localOnlySetting,
-			visibility: noteVisibility,
+			localOnly: localOnly,
+			visibility: visibility,
 			renoteId: appearNote.value.id,
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue
index 1641f8a5a4..1486742f42 100644
--- a/packages/frontend/src/components/SkNoteSub.vue
+++ b/packages/frontend/src/components/SkNoteSub.vue
@@ -113,6 +113,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 import { getNoteMenu } from '@/scripts/get-note-menu.js';
 import { useNoteCapture } from '@/scripts/use-note-capture.js';
+import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
 
 const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
 const hideLine = computed(() => { return props.detail ? true : false; });
@@ -290,43 +291,11 @@ function boostVisibility() {
 	if (!defaultStore.state.showVisibilitySelectorOnBoost) {
 		renote(defaultStore.state.visibilityOnBoost);
 	} else {
-		os.popupMenu([
-			{
-				type: 'button',
-				icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
-				text: i18n.ts._visibility['public'],
-				action: () => {
-					renote('public');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-house ph-bold ph-lg',
-				text: i18n.ts._visibility['home'],
-				action: () => {
-					renote('home');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-lock ph-bold ph-lg',
-				text: i18n.ts._visibility['followers'],
-				action: () => {
-					renote('followers');
-				},
-			},
-			{
-				type: 'button',
-				icon: 'ph-planet ph-bold ph-lg',
-				text: i18n.ts._timelines.local,
-				action: () => {
-					renote('local');
-				},
-			}], renoteButton.value);
+		os.popupMenu(boostMenuItems(appearNote, renote), renoteButton.value);
 	}
 }
 
-function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'local') {
+function renote(visibility: Visibility, localOnly: boolean = false) {
 	pleaseLogin();
 	showMovedDialog();
 
@@ -340,8 +309,8 @@ function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'loc
 		}
 
 		misskeyApi('notes/create', {
-			renoteId: props.note.id,
-			channelId: props.note.channelId,
+			renoteId: appearNote.value.id,
+			channelId: appearNote.value.channelId,
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
@@ -356,9 +325,9 @@ function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'loc
 		}
 
 		misskeyApi('notes/create', {
-			renoteId: props.note.id,
-			localOnly: visibility === 'local' ? true : false,
-			visibility: visibility === 'local' || visibility === 'specified' ? props.note.visibility : visibility,
+			renoteId: appearNote.value.id,
+			localOnly: localOnly,
+			visibility: visibility,
 		}).then(() => {
 			os.toast(i18n.ts.renoted);
 			renoted.value = true;
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index 3b2946e2b7..a27902c467 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -212,7 +212,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<option value="public">{{ i18n.ts._visibility['public'] }}</option>
 						<option value="home">{{ i18n.ts._visibility['home'] }}</option>
 						<option value="followers">{{ i18n.ts._visibility['followers'] }}</option>
-						<option value="local">{{ i18n.ts._timelines.local }}</option>
 					</MkSelect>
 				</div>
 			</MkFolder>
diff --git a/packages/frontend/src/scripts/boost-quote.ts b/packages/frontend/src/scripts/boost-quote.ts
new file mode 100644
index 0000000000..4e025f5d4f
--- /dev/null
+++ b/packages/frontend/src/scripts/boost-quote.ts
@@ -0,0 +1,81 @@
+/*
+ * SPDX-FileCopyrightText: dakkar and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+import { ref, Ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import { i18n } from '@/i18n.js';
+import { defaultStore } from '@/store.js';
+import { MenuItem } from '@/types/menu.js';
+
+/*
+	this script should eventually contain all Sharkey-specific bits of
+	boosting and quoting that we would otherwise have to replicate in
+	`{M,S}kNote{,Detailed,Sub}.vue`
+ */
+
+export type Visibility = 'public' | 'home' | 'followers' | 'specified';
+
+export function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility {
+	if (a === 'specified' || b === 'specified') return 'specified';
+	if (a === 'followers' || b === 'followers') return 'followers';
+	if (a === 'home' || b === 'home') return 'home';
+	// if (a === 'public' || b === 'public')
+	return 'public';
+}
+
+export function visibilityIsAtLeast(a: Visibility | string, b: Visibility | string): boolean {
+	return smallerVisibility(a, b) === b;
+}
+
+export function boostMenuItems(appearNote: Ref<Misskey.entities.Note>, renote: (v: Visibility, l: boolean) => void): MenuItem[] {
+	const localOnly = ref(defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly);
+	const effectiveVisibility = (
+		appearNote.value.channel?.isSensitive
+			? smallerVisibility(appearNote.value.visibility, 'home')
+			: appearNote.value.visibility
+	);
+
+	const menuItems: MenuItem[] = [];
+	if (visibilityIsAtLeast(effectiveVisibility, 'public')) {
+		menuItems.push({
+			type: 'button',
+			icon: 'ph-globe-hemisphere-west ph-bold ph-lg',
+			text: i18n.ts._visibility['public'],
+			action: () => {
+				renote('public', localOnly.value);
+			},
+		} as MenuItem);
+	}
+	if (visibilityIsAtLeast(effectiveVisibility, 'home')) {
+		menuItems.push({
+			type: 'button',
+			icon: 'ph-house ph-bold ph-lg',
+			text: i18n.ts._visibility['home'],
+			action: () => {
+				renote('home', localOnly.value);
+			},
+		} as MenuItem);
+	}
+	if (visibilityIsAtLeast(effectiveVisibility, 'followers')) {
+		menuItems.push({
+			type: 'button',
+			icon: 'ph-lock ph-bold ph-lg',
+			text: i18n.ts._visibility['followers'],
+			action: () => {
+				renote('followers', localOnly.value);
+			},
+		} as MenuItem);
+	}
+
+	return [
+		...menuItems,
+		{
+			type: 'switch',
+			icon: 'ph-planet ph-bold ph-lg',
+			text: i18n.ts._timelines.local,
+			ref: localOnly,
+		} as MenuItem,
+	];
+}
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index d695caa95f..53d259cf39 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -159,7 +159,7 @@ export const defaultStore = markRaw(new Storage('base', {
 	},
 	visibilityOnBoost: {
 		where: 'account',
-		default: 'public' as 'public' | 'home' | 'followers' | 'local',
+		default: 'public' as 'public' | 'home' | 'followers',
 	},
 
 	menu: {