From 40bb6069ec04bc0461ac407da7d03c6910c23d6d Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Fri, 22 Mar 2024 08:54:34 +0900
Subject: [PATCH 1/3] =?UTF-8?q?fix(frontend):=20URL=E3=83=97=E3=83=AC?=
 =?UTF-8?q?=E3=83=93=E3=83=A5=E3=83=BC=E3=81=AEto/href=E3=81=8C=E3=81=AA?=
 =?UTF-8?q?=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20Fix=20?=
 =?UTF-8?q?of=20https://github.com/misskey-dev/misskey/pull/13579/files/9a?=
 =?UTF-8?q?e577871b10f6231acc3451188cd69ede9443ed#diff-cfa02e203bdbd03dbf3?=
 =?UTF-8?q?12a889f009ca7f9ebd8376334ebd74c4961b716b22d93?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/MkUrlPreview.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index b3dc492616..6954f1f6ff 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -110,6 +110,7 @@ const MOBILE_THRESHOLD = 500;
 const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
 
 const self = props.url.startsWith(local);
+const attr = self ? 'to' : 'href';
 const target = self ? null : '_blank';
 const fetching = ref(true);
 const title = ref<string | null>(null);

From c9c6424205f1d05813735a8f9c0e53a1ccc35e0e 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: Fri, 22 Mar 2024 15:03:21 +0900
Subject: [PATCH 2/3] =?UTF-8?q?enhance(frontend):=20TOTP=E3=81=AE=E5=85=A5?=
 =?UTF-8?q?=E5=8A=9B=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD=E3=82=B0=E3=82=92?=
 =?UTF-8?q?=E6=94=B9=E8=89=AF=20(#13607)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* enhance(frontend): TOTPの入力ダイアログを改良

* Update Changelog
---
 CHANGELOG.md                                  |  3 ++-
 locales/index.d.ts                            |  8 ++++++
 locales/ja-JP.yml                             |  2 ++
 packages/frontend/src/components/MkInput.vue  |  2 ++
 .../src/components/MkPasswordDialog.vue       | 26 +++++++++++--------
 packages/frontend/src/components/MkSignin.vue | 11 ++++----
 .../src/pages/settings/2fa.qrdialog.vue       |  2 +-
 packages/frontend/src/pages/settings/2fa.vue  |  6 ++++-
 8 files changed, 41 insertions(+), 19 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 188e146cdd..d90b1425c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,7 +16,8 @@
 - Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように
 - Enhance: 設定>プラグインのページからプラグインの簡易的なログやエラーを見られるように
   - 実装の都合により、プラグインは1つエラーを起こした時に即時停止するようになりました
-- Enhance: ページのデザインを変更	
+- Enhance: ページのデザインを変更
+- Enhance: 2要素認証(ワンタイムパスワード)の入力欄を改善
 - Fix: 一部のページ内リンクが正しく動作しない問題を修正
 - Fix: 周年の実績が閏年を考慮しない問題を修正
 - Fix: ローカルURLのプレビューポップアップが左上に表示される
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 70586d7a87..e1250946f3 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -4920,6 +4920,14 @@ export interface Locale extends ILocale {
      * 使用しない場合は空欄にしてください
      */
     "notUsePleaseLeaveBlank": string;
+    /**
+     * ワンタイムパスワードを使う
+     */
+    "useTotp": string;
+    /**
+     * バックアップコードを使う
+     */
+    "useBackupCode": string;
     "_bubbleGame": {
         /**
          * 遊び方
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index cada6d855f..1c4df27d92 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1226,6 +1226,8 @@ loading: "読み込み中"
 surrender: "やめる"
 gameRetry: "リトライ"
 notUsePleaseLeaveBlank: "使用しない場合は空欄にしてください"
+useTotp: "ワンタイムパスワードを使う"
+useBackupCode: "バックアップコードを使う"
 
 _bubbleGame:
   howToPlay: "遊び方"
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index d3cddad15b..88ef4635e6 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			:autocomplete="autocomplete"
 			:autocapitalize="autocapitalize"
 			:spellcheck="spellcheck"
+			:inputmode="inputmode"
 			:step="step"
 			:list="id"
 			:min="min"
@@ -63,6 +64,7 @@ const props = defineProps<{
 	mfmAutocomplete?: boolean | SuggestionType[],
 	autocapitalize?: string;
 	spellcheck?: boolean;
+	inputmode?: 'none' | 'text' | 'search' | 'email' | 'url' | 'numeric' | 'tel' | 'decimal';
 	step?: any;
 	datalist?: string[];
 	min?: number;
diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue
index c49526d8e2..e749725fea 100644
--- a/packages/frontend/src/components/MkPasswordDialog.vue
+++ b/packages/frontend/src/components/MkPasswordDialog.vue
@@ -19,18 +19,21 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div style="margin-top: 16px;">{{ i18n.ts.authenticationRequiredToContinue }}</div>
 		</div>
 
-		<div class="_gaps">
-			<MkInput ref="passwordInput" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true">
-				<template #prefix><i class="ti ti-password"></i></template>
-			</MkInput>
+		<form @submit.prevent="done">
+			<div class="_gaps">
+				<MkInput ref="passwordInput" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" required :withPasswordToggle="true">
+					<template #prefix><i class="ti ti-password"></i></template>
+				</MkInput>
 
-			<MkInput v-if="$i.twoFactorEnabled" v-model="token" type="text" pattern="^([0-9]{6}|[A-Z0-9]{32})$" autocomplete="one-time-code" :spellcheck="false">
-				<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
-				<template #prefix><i class="ti ti-123"></i></template>
-			</MkInput>
+				<MkInput v-if="$i.twoFactorEnabled" v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
+					<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
+					<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
+					<template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
+				</MkInput>
 
-			<MkButton :disabled="(password ?? '') == '' || ($i.twoFactorEnabled && (token ?? '') == '')" primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-lock-open"></i> {{ i18n.ts.continue }}</MkButton>
-		</div>
+				<MkButton :disabled="(password ?? '') == '' || ($i.twoFactorEnabled && (token ?? '') == '')" type="submit" primary rounded style="margin: 0 auto;"><i class="ti ti-lock-open"></i> {{ i18n.ts.continue }}</MkButton>
+			</div>
+		</form>
 	</MkSpacer>
 </MkModalWindow>
 </template>
@@ -54,6 +57,7 @@ const emit = defineEmits<{
 const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 const passwordInput = shallowRef<InstanceType<typeof MkInput>>();
 const password = ref('');
+const isBackupCode = ref(false);
 const token = ref<string | null>(null);
 
 function onClose() {
@@ -61,7 +65,7 @@ function onClose() {
 	if (dialog.value) dialog.value.close();
 }
 
-function done(res) {
+function done() {
 	emit('done', { password: password.value, token: token.value });
 	if (dialog.value) dialog.value.close();
 }
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 852af01b5a..970aff825d 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -31,15 +31,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-if="user && user.securityKeys" class="or-hr">
 				<p class="or-msg">{{ i18n.ts.or }}</p>
 			</div>
-			<div class="twofa-group totp-group">
-				<p style="margin-bottom:0;">{{ i18n.ts['2fa'] }}</p>
+			<div class="twofa-group totp-group _gaps">
 				<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
 					<template #label>{{ i18n.ts.password }}</template>
 					<template #prefix><i class="ti ti-lock"></i></template>
 				</MkInput>
-				<MkInput v-model="token" type="text" pattern="^([0-9]{6}|[A-Z0-9]{32})$" autocomplete="one-time-code" :spellcheck="false" required>
-					<template #label>{{ i18n.ts.token }}</template>
-					<template #prefix><i class="ti ti-123"></i></template>
+				<MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
+					<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
+					<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
+					<template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
 				</MkInput>
 				<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
 			</div>
@@ -70,6 +70,7 @@ const password = ref('');
 const token = ref('');
 const host = ref(toUnicode(configHost));
 const totpLogin = ref(false);
+const isBackupCode = ref(false);
 const queryingKey = ref(false);
 const credentialRequest = ref<CredentialRequestOptions | null>(null);
 
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
index 2608560cc4..2ef664b9a3 100644
--- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue
+++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
@@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkSpacer :marginMin="20" :marginMax="28">
 						<div class="_gaps">
 							<div>{{ i18n.ts._2fa.step3Title }}</div>
-							<MkInput v-model="token" autocomplete="one-time-code"></MkInput>
+							<MkInput v-model="token" autocomplete="one-time-code" inputmode="numeric"></MkInput>
 							<div>{{ i18n.ts._2fa.step3 }}</div>
 						</div>
 						<div class="_buttonsCenter" style="margin-top: 16px;">
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index d8c5f848fe..975f23cdd1 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -80,7 +80,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import FormSection from '@/components/form/section.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import * as os from '@/os.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired, updateAccount } from '@/account.js';
 import { i18n } from '@/i18n.js';
 
 const $i = signinRequired();
@@ -116,6 +116,10 @@ async function unregisterTOTP(): Promise<void> {
 	os.apiWithDialog('i/2fa/unregister', {
 		password: auth.result.password,
 		token: auth.result.token,
+	}).then(res => {
+		updateAccount({
+			twoFactorEnabled: false,
+		});
 	}).catch(error => {
 		os.alert({
 			type: 'error',

From 6bd78770de06bd3694127da17ccd051f05057329 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: Fri, 22 Mar 2024 18:21:14 +0900
Subject: [PATCH 3/3] =?UTF-8?q?enhance(frontend):=20=E3=83=AA=E3=82=A2?=
 =?UTF-8?q?=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E5=8F=97=E3=81=91=E5=85=A5?=
 =?UTF-8?q?=E3=82=8C=E3=81=8C=E3=81=84=E3=81=84=E3=81=AD=E3=81=AE=E3=81=BF?=
 =?UTF-8?q?=E3=81=AE=E5=A0=B4=E5=90=88=E3=81=AF=E3=83=9C=E3=82=BF=E3=83=B3?=
 =?UTF-8?q?=E3=83=9B=E3=83=90=E3=83=BC=E3=81=A7=E3=83=84=E3=83=BC=E3=83=AB?=
 =?UTF-8?q?=E3=83=81=E3=83=83=E3=83=97=E3=81=8C=E5=87=BA=E3=82=8B=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB=20(#13613)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/MkNote.vue   | 25 ++++++++++++++++++-
 .../src/components/MkNoteDetailed.vue         | 25 ++++++++++++++++++-
 2 files changed, 48 insertions(+), 2 deletions(-)

diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 50741f2cb7..4add3aa6dd 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -167,6 +167,7 @@ import MkNoteSub from '@/components/MkNoteSub.vue';
 import MkNoteHeader from '@/components/MkNoteHeader.vue';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
 import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
+import MkReactionsViewerDetails from '@/components/MkReactionsViewer.details.vue';
 import MkMediaList from '@/components/MkMediaList.vue';
 import MkCwButton from '@/components/MkCwButton.vue';
 import MkPoll from '@/components/MkPoll.vue';
@@ -180,7 +181,7 @@ import { userPage } from '@/filters/user.js';
 import number from '@/filters/number.js';
 import * as os from '@/os.js';
 import * as sound from '@/scripts/sound.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
 import { defaultStore, noteViewInterruptors } from '@/store.js';
 import { reactionPicker } from '@/scripts/reaction-picker.js';
 import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
@@ -340,6 +341,28 @@ if (!props.mock) {
 			targetElement: renoteButton.value,
 		}, {}, 'closed');
 	});
+
+	if (appearNote.value.reactionAcceptance === 'likeOnly') {
+		useTooltip(reactButton, async (showing) => {
+			const reactions = await misskeyApiGet('notes/reactions', {
+				noteId: appearNote.value.id,
+				limit: 10,
+				_cacheKey_: appearNote.value.reactionCount,
+			});
+
+			const users = reactions.map(x => x.user);
+
+			if (users.length < 1) return;
+
+			os.popup(MkReactionsViewerDetails, {
+				showing,
+				reaction: '❤️',
+				users,
+				count: appearNote.value.reactionCount,
+				targetElement: reactButton.value!,
+			}, {}, 'closed');
+		});
+	}
 }
 
 function renote(viaKeyboard = false) {
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 1b7dcda409..ebccd60986 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -201,6 +201,7 @@ import * as Misskey from 'misskey-js';
 import MkNoteSub from '@/components/MkNoteSub.vue';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
 import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
+import MkReactionsViewerDetails from '@/components/MkReactionsViewer.details.vue';
 import MkMediaList from '@/components/MkMediaList.vue';
 import MkCwButton from '@/components/MkCwButton.vue';
 import MkPoll from '@/components/MkPoll.vue';
@@ -213,7 +214,7 @@ import { userPage } from '@/filters/user.js';
 import { notePage } from '@/filters/note.js';
 import number from '@/filters/number.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
 import * as sound from '@/scripts/sound.js';
 import { defaultStore, noteViewInterruptors } from '@/store.js';
 import { reactionPicker } from '@/scripts/reaction-picker.js';
@@ -348,6 +349,28 @@ useTooltip(renoteButton, async (showing) => {
 	}, {}, 'closed');
 });
 
+if (appearNote.value.reactionAcceptance === 'likeOnly') {
+	useTooltip(reactButton, async (showing) => {
+		const reactions = await misskeyApiGet('notes/reactions', {
+			noteId: appearNote.value.id,
+			limit: 10,
+			_cacheKey_: appearNote.value.reactionCount,
+		});
+
+		const users = reactions.map(x => x.user);
+
+		if (users.length < 1) return;
+
+		os.popup(MkReactionsViewerDetails, {
+			showing,
+			reaction: '❤️',
+			users,
+			count: appearNote.value.reactionCount,
+			targetElement: reactButton.value!,
+		}, {}, 'closed');
+	});
+}
+
 function renote(viaKeyboard = false) {
 	pleaseLogin();
 	showMovedDialog();