From b752dc72e531f6c63f09876a1c68a87a77c03b49 Mon Sep 17 00:00:00 2001
From: taichan <40626578+tai-cha@users.noreply.github.com>
Date: Wed, 25 Jun 2025 17:09:23 +0900
Subject: feat: ノートの下書き(draft of note) (#15298)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* WIp (backend)

* Remove unused

* 下書きbackend 続き

* fix(backedn): visibilityが下書きに反映されない

* Update packages/backend/src/postgres.ts

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>

* Fix : import order

* fix(backend) : createでcwが効かない

* FIX FOREGIN KEY

* wip: frontend（既存の下書きを挿入）

まだ：チャンネル表示、下書きの作成、削除

* WIP: ノート選択ダイアログ
投稿時に下書きを削除

* Promiseに変更

* 連合なし、チャンネルも表示

* Hashtagの値抜け漏れ

* hasthagを0文字でも作成可能に

* 下書きの保存機構

* chore(misskey-js): build types

* localOnly抜け漏れ

* チャンネル情報の書き換え

* enhance(frontend): ヘッダ部の表示改善

* fix(frontend): ファイル添付できない

* fix: no file

* fix(frontend): 投票が反映されない

* ハッシュタグの展開（コメントアウト外し忘れ）

* fix: visibleUserIdsが反映されない

* enhance: APIの型を整備

* refactor: 型が整備できたのでasを削除

* Add userhost

* fix

* enhance: paginationを使う

* fix

* fix: 自分のアカウントでの投稿でしか下書きを利用できないように

完全に塞ぐことはできないが一応

* :art:

* APIのエラーIDを追加

* enhance: スタイル調整

* remove unused code

* :art:

* fix: ロールポリシーの型

* ロールの編集画面

* ダイアログの挙動改善

* 下書き機能が利用できない場合は表示しないように

* refactor

* fix: ダブルクリックが効かない問題を修正

* add comments

* fix

* fix: 保存時のエラーの種別にかかわらずmodalを閉じないように

* fix()backend: NoteDraftのreply, renoteの型が間違ってたので修正 (migtrationはあってた)

* fix: 投稿フォームを空白にして通常リノートできるやつは下書きとしては弾くように

* fix(backend): テキストが0文字でも下書きは保存できるように

* Fix(backend): replyIdの型定義がミスっているのを修正

* chore(misskey-js): update types

* Add CHANGELOG

* lint

* 常にサーバー下書きに保存し、上限を超えた場合のみ尋ねるように

* NoteDraftServiceにcreate, updateの処理を移譲

* Fix typeerror

* remove tooltip

* Remove Mkbutton:short and use iconOnly

* 不要なコメントの削除

* Remove Short Completely

* wip

* escキーまわりの挙動を改善

* 下書き選択時に下書き可能数と現在の量が分かるように

* cleanUp

* wip

* wi

* wip

* Update MkPostForm.vue

---------

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 .../frontend/src/components/MkNoteDraftsDialog.vue | 218 +++++++++++++++++++++
 1 file changed, 218 insertions(+)
 create mode 100644 packages/frontend/src/components/MkNoteDraftsDialog.vue

(limited to 'packages/frontend/src/components/MkNoteDraftsDialog.vue')

diff --git a/packages/frontend/src/components/MkNoteDraftsDialog.vue b/packages/frontend/src/components/MkNoteDraftsDialog.vue
new file mode 100644
index 0000000000..b4aff8d16f
--- /dev/null
+++ b/packages/frontend/src/components/MkNoteDraftsDialog.vue
@@ -0,0 +1,218 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkModalWindow
+	ref="dialogEl"
+	:width="600"
+	:height="650"
+	:withOkButton="false"
+	@click="cancel()"
+	@close="cancel()"
+	@closed="emit('closed')"
+	@esc="cancel()"
+>
+	<template #header>
+		{{ i18n.ts.drafts }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
+	</template>
+	<div :class="$style.drafts" class="_gaps">
+		<MkPagination ref="pagingEl" :pagination="paging">
+			<template #empty>
+				<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
+			</template>
+
+			<template #default="{ items }">
+				<div class="_spacer _gaps_s">
+					<div
+						v-for="draft in (items as unknown as Misskey.entities.NoteDraft[])"
+						:key="draft.id"
+						v-panel
+						:class="[$style.draft]"
+					>
+						<div :class="$style.draftBody" class="_gaps_s">
+							<div :class="$style.draftInfo">
+								<div :class="$style.draftMeta">
+									<div v-if="draft.reply" class="_nowrap">
+										<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
+											<template #user>
+												<Mfm v-if="draft.reply.user.name != null" :text="draft.reply.user.name" :plain="true" :nowrap="true"/>
+												<MkAcct v-else :user="draft.reply.user"/>
+											</template>
+										</I18n>
+									</div>
+									<div v-if="draft.renote && draft.text != null" class="_nowrap">
+										<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
+											<template #user>
+												<Mfm v-if="draft.renote.user.name != null" :text="draft.renote.user.name" :plain="true" :nowrap="true"/>
+												<MkAcct v-else :user="draft.renote.user"/>
+											</template>
+										</I18n>
+									</div>
+									<div v-if="draft.channel" class="_nowrap">
+										<i class="ti ti-device-tv"></i> {{ i18n.tsx._drafts.postTo({ channel: draft.channel.name }) }}
+									</div>
+								</div>
+							</div>
+							<div :class="$style.draftContent">
+								<Mfm :text="getNoteSummary(draft, { showRenote: false, showReply: false })" :plain="true" :author="draft.user"/>
+							</div>
+							<div :class="$style.draftFooter">
+								<div :class="$style.draftVisibility">
+									<span :title="i18n.ts._visibility[draft.visibility]">
+										<i v-if="draft.visibility === 'public'" class="ti ti-world"></i>
+										<i v-else-if="draft.visibility === 'home'" class="ti ti-home"></i>
+										<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
+										<i v-else-if="draft.visibility === 'specified'" class="ti ti-mail"></i>
+									</span>
+									<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
+								</div>
+								<MkTime :time="draft.createdAt" :class="$style.draftCreatedAt" mode="detail" colored/>
+							</div>
+						</div>
+						<div :class="$style.draftActions" class="_buttons">
+							<MkButton
+								:class="$style.itemButton"
+								small
+								@click="restoreDraft(draft)"
+							>
+								<i class="ti ti-corner-up-left"></i>
+								{{ i18n.ts._drafts.restore }}
+							</MkButton>
+							<MkButton
+								v-tooltip="i18n.ts._drafts.delete"
+								danger
+								small
+								:iconOnly="true"
+								:class="$style.itemButton"
+								@click="deleteDraft(draft)"
+							>
+								<i class="ti ti-trash"></i>
+							</MkButton>
+						</div>
+					</div>
+				</div>
+			</template>
+		</MkPagination>
+	</div>
+</MkModalWindow>
+</template>
+
+<script lang="ts" setup>
+import { ref, shallowRef, useTemplateRef } from 'vue';
+import * as Misskey from 'misskey-js';
+import type { PagingCtx } from '@/composables/use-pagination.js';
+import MkButton from '@/components/MkButton.vue';
+import MkPagination from '@/components/MkPagination.vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
+import { getNoteSummary } from '@/utility/get-note-summary.js';
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+import { $i } from '@/i.js';
+import { misskeyApi } from '@/utility/misskey-api';
+
+const emit = defineEmits<{
+	(ev: 'restore', draft: Misskey.entities.NoteDraft): void;
+	(ev: 'cancel'): void;
+	(ev: 'closed'): void;
+}>();
+
+const paging = {
+	endpoint: 'notes/drafts/list',
+	limit: 10,
+} satisfies PagingCtx;
+
+const pagingComponent = useTemplateRef('pagingEl');
+
+const currentDraftsCount = ref(0);
+misskeyApi('notes/drafts/count').then((count) => {
+	currentDraftsCount.value = count;
+});
+
+const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+
+function cancel() {
+	emit('cancel');
+	dialogEl.value?.close();
+}
+
+function restoreDraft(draft: Misskey.entities.NoteDraft) {
+	emit('restore', draft);
+	dialogEl.value?.close();
+}
+
+async function deleteDraft(draft: Misskey.entities.NoteDraft) {
+	const { canceled } = await os.confirm({
+		type: 'warning',
+		text: i18n.ts._drafts.deleteAreYouSure,
+	});
+
+	if (canceled) return;
+
+	os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => {
+		pagingComponent.value?.paginator.reload();
+	});
+}
+</script>
+
+<style lang="scss" module>
+.drafts {
+	overflow-x: hidden;
+	overflow-x: clip;
+	overflow-y: auto;
+}
+
+.draft {
+	padding: 16px;
+	gap: 16px;
+	border-radius: 10px;
+}
+
+.draftBody {
+	width: 100%;
+	min-width: 0;
+}
+
+.draftInfo {
+	display: flex;
+	width: 100%;
+	font-size: 0.85em;
+	opacity: 0.7;
+}
+
+.draftMeta {
+	flex-grow: 1;
+	min-width: 0;
+}
+
+.draftContent {
+	display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+	line-clamp: 2;
+  overflow: hidden;
+	font-size: 0.9em;
+}
+
+.draftFooter {
+	display: flex;
+	align-items: center;
+	gap: 8px;
+}
+
+.draftVisibility {
+	flex-shrink: 0;
+}
+
+.draftCreatedAt {
+	font-size: 85%;
+	opacity: 0.7;
+}
+
+.draftActions {
+	margin-top: 16px;
+	padding-top: 16px;
+	border-top: solid 1px var(--MI_THEME-divider);
+}
+</style>
-- 
cgit v1.2.3-freya

