From fe0bb21b37833e33f44b8e6ef697ac3259518b0c Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 16 Jan 2023 11:21:04 +0900
Subject: [PATCH] enhance(client): make possible to hide ads

Resolve #9590
Resolve #8996
---
 locales/ja-JP.yml                             |  2 ++
 packages/backend/src/core/RoleService.ts      |  3 +++
 .../frontend/src/components/global/MkAd.vue   | 12 ++++++++++-
 .../frontend/src/pages/admin/roles.editor.vue | 21 +++++++++++++++++++
 packages/frontend/src/pages/admin/roles.vue   |  9 ++++++++
 packages/frontend/src/store.ts                |  6 +++++-
 6 files changed, 51 insertions(+), 2 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 174c28f4a1..f0e53cb814 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -982,6 +982,7 @@ _role:
     userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
     rateLimitFactor: "レートリミット"
     descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。"
+    canHideAds: "広告の非表示"
   _condition:
     isLocal: "ローカルユーザー"
     isRemote: "リモートユーザー"
@@ -1032,6 +1033,7 @@ _accountDelete:
 _ad:
   back: "戻る"
   reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"
+  hide: "表示しない"
 
 _forgotPassword:
   enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 0ba5ca9a03..c0f5eae3d7 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -19,6 +19,7 @@ export type RolePolicies = {
 	canPublicNote: boolean;
 	canInvite: boolean;
 	canManageCustomEmojis: boolean;
+	canHideAds: boolean;
 	driveCapacityMb: number;
 	pinLimit: number;
 	antennaLimit: number;
@@ -37,6 +38,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
 	canPublicNote: true,
 	canInvite: false,
 	canManageCustomEmojis: false,
+	canHideAds: false,
 	driveCapacityMb: 100,
 	pinLimit: 5,
 	antennaLimit: 5,
@@ -223,6 +225,7 @@ export class RoleService implements OnApplicationShutdown {
 			canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
 			canInvite: calc('canInvite', vs => vs.some(v => v === true)),
 			canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
+			canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
 			driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
 			pinLimit: calc('pinLimit', vs => Math.max(...vs)),
 			antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index a1ac682a45..a518484a5d 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -1,5 +1,5 @@
 <template>
-<div v-if="chosen" :class="$style.root">
+<div v-if="chosen && !shouldHide" :class="$style.root">
 	<div v-if="!showMenu" :class="[$style.main, $style['form_' + chosen.place]]">
 		<a :href="chosen.url" target="_blank" :class="$style.link">
 			<img :src="chosen.imageUrl" :class="$style.img">
@@ -11,6 +11,7 @@
 			<div>Ads by {{ host }}</div>
 			<!--<MkButton class="button" primary>{{ $ts._ad.like }}</MkButton>-->
 			<MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ $ts._ad.reduceFrequencyOfThisAd }}</MkButton>
+			<MkButton v-if="$i && $i.policies.canHideAds" :class="$style.menuButton" @click="hide">{{ $ts._ad.hide }}</MkButton>
 			<button class="_textButton" @click="toggleMenu">{{ $ts._ad.back }}</button>
 		</div>
 	</div>
@@ -25,6 +26,7 @@ import { host } from '@/config';
 import MkButton from '@/components/MkButton.vue';
 import { defaultStore } from '@/store';
 import * as os from '@/os';
+import { $i } from '@/account';
 
 type Ad = (typeof instance)['ads'][number];
 
@@ -81,6 +83,7 @@ const choseAd = (): Ad | null => {
 };
 
 const chosen = ref(choseAd());
+let shouldHide = $ref(chosen.value && $i && $i.policies.canHideAds && defaultStore.state.hiddenAds.includes(chosen.value.id));
 
 function reduceFrequency(): void {
 	if (chosen.value == null) return;
@@ -90,6 +93,13 @@ function reduceFrequency(): void {
 	chosen.value = choseAd();
 	showMenu.value = false;
 }
+
+function hide() {
+	if (chosen.value == null) return;
+	defaultStore.push('hiddenAds', chosen.value.id);
+	os.success();
+	shouldHide = true;
+}
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index e5f4675c19..ae5ef39bae 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -335,6 +335,26 @@
 					</MkRange>
 				</div>
 			</MkFolder>
+
+			<MkFolder v-if="matchQuery([i18n.ts._role._options.canHideAds, 'canHideAds'])">
+				<template #label>{{ i18n.ts._role._options.canHideAds }}</template>
+				<template #suffix>
+					<span v-if="policies.canHideAds.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
+					<span v-else>{{ policies.canHideAds.value ? i18n.ts.yes : i18n.ts.no }}</span>
+					<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(policies.canHideAds)"></i></span>
+				</template>
+				<div class="_gaps">
+					<MkSwitch v-model="policies.canHideAds.useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
+					</MkSwitch>
+					<MkSwitch v-model="policies.canHideAds.value" :disabled="policies.canHideAds.useDefault" :readonly="readonly">
+						<template #label>{{ i18n.ts.enable }}</template>
+					</MkSwitch>
+					<MkRange v-model="policies.canHideAds.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
+				</div>
+			</MkFolder>
 		</div>
 	</FormSlot>
 
@@ -376,6 +396,7 @@ const ROLE_POLICIES = [
 	'canPublicNote',
 	'canInvite',
 	'canManageCustomEmojis',
+	'canHideAds',
 	'driveCapacityMb',
 	'pinLimit',
 	'antennaLimit',
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index f074069df0..6e0c038982 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -121,6 +121,14 @@
 							</MkInput>
 						</MkFolder>
 
+						<MkFolder>
+							<template #label>{{ i18n.ts._role._options.canHideAds }}</template>
+							<template #suffix>{{ policies.canHideAds ? i18n.ts.yes : i18n.ts.no }}</template>
+							<MkSwitch v-model="policies.canHideAds">
+								<template #label>{{ i18n.ts.enable }}</template>
+							</MkSwitch>
+						</MkFolder>
+
 						<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
 					</div>
 				</MkFolder>
@@ -156,6 +164,7 @@ const ROLE_POLICIES = [
 	'canPublicNote',
 	'canInvite',
 	'canManageCustomEmojis',
+	'canHideAds',
 	'driveCapacityMb',
 	'pinLimit',
 	'antennaLimit',
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index b2bf8db646..b29d9355ba 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -86,6 +86,10 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'account',
 		default: [] as string[],
 	},
+	hiddenAds: {
+		where: 'account',
+		default: [] as string[],
+	},
 
 	menu: {
 		where: 'deviceAccount',
@@ -293,10 +297,10 @@ interface Watcher {
 /**
  * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
  */
+import { miLocalStorage } from './local-storage';
 import lightTheme from '@/themes/l-light.json5';
 import darkTheme from '@/themes/d-green-lime.json5';
 import { Note, UserDetailed } from 'misskey-js/built/entities';
-import { miLocalStorage } from './local-storage';
 
 export class ColdDeviceStorage {
 	public static default = {
-- 
GitLab