feat: ノートの下書き(draft of note) (#15298)

* 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: 自分のアカウントでの投稿でしか下書きを利用できないように

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

* 🎨

* APIのエラーIDを追加

* enhance: スタイル調整

* remove unused code

* 🎨

* 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>
This commit is contained in:
taichan
2025-06-25 17:09:23 +09:00
committed by GitHub
parent 06d31c0b78
commit b752dc72e5
37 changed files with 2851 additions and 57 deletions

View File

@@ -1953,6 +1953,14 @@ declare namespace entities {
NotesCreateRequest,
NotesCreateResponse,
NotesDeleteRequest,
NotesDraftsCountResponse,
NotesDraftsCreateRequest,
NotesDraftsCreateResponse,
NotesDraftsDeleteRequest,
NotesDraftsListRequest,
NotesDraftsListResponse,
NotesDraftsUpdateRequest,
NotesDraftsUpdateResponse,
NotesFavoritesCreateRequest,
NotesFavoritesDeleteRequest,
NotesFeaturedRequest,
@@ -2118,6 +2126,7 @@ declare namespace entities {
Announcement,
App,
Note,
NoteDraft,
NoteReaction,
NoteFavorite,
Notification_2 as Notification,
@@ -2962,6 +2971,9 @@ declare namespace note {
}
export { note }
// @public (undocumented)
type NoteDraft = components['schemas']['NoteDraft'];
// @public (undocumented)
type NoteFavorite = components['schemas']['NoteFavorite'];
@@ -2995,6 +3007,30 @@ type NotesCreateResponse = operations['notes___create']['responses']['200']['con
// @public (undocumented)
type NotesDeleteRequest = operations['notes___delete']['requestBody']['content']['application/json'];
// @public (undocumented)
type NotesDraftsCountResponse = operations['notes___drafts___count']['responses']['200']['content']['application/json'];
// @public (undocumented)
type NotesDraftsCreateRequest = operations['notes___drafts___create']['requestBody']['content']['application/json'];
// @public (undocumented)
type NotesDraftsCreateResponse = operations['notes___drafts___create']['responses']['200']['content']['application/json'];
// @public (undocumented)
type NotesDraftsDeleteRequest = operations['notes___drafts___delete']['requestBody']['content']['application/json'];
// @public (undocumented)
type NotesDraftsListRequest = operations['notes___drafts___list']['requestBody']['content']['application/json'];
// @public (undocumented)
type NotesDraftsListResponse = operations['notes___drafts___list']['responses']['200']['content']['application/json'];
// @public (undocumented)
type NotesDraftsUpdateRequest = operations['notes___drafts___update']['requestBody']['content']['application/json'];
// @public (undocumented)
type NotesDraftsUpdateResponse = operations['notes___drafts___update']['responses']['200']['content']['application/json'];
// @public (undocumented)
type NotesFavoritesCreateRequest = operations['notes___favorites___create']['requestBody']['content']['application/json'];

View File

@@ -3593,6 +3593,61 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
request<E extends 'notes/drafts/count', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
request<E extends 'notes/drafts/create', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
request<E extends 'notes/drafts/delete', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
request<E extends 'notes/drafts/list', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
request<E extends 'notes/drafts/update', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*

View File

@@ -488,6 +488,14 @@ import type {
NotesCreateRequest,
NotesCreateResponse,
NotesDeleteRequest,
NotesDraftsCountResponse,
NotesDraftsCreateRequest,
NotesDraftsCreateResponse,
NotesDraftsDeleteRequest,
NotesDraftsListRequest,
NotesDraftsListResponse,
NotesDraftsUpdateRequest,
NotesDraftsUpdateResponse,
NotesFavoritesCreateRequest,
NotesFavoritesDeleteRequest,
NotesFeaturedRequest,
@@ -963,6 +971,11 @@ export type Endpoints = {
'notes/conversation': { req: NotesConversationRequest; res: NotesConversationResponse };
'notes/create': { req: NotesCreateRequest; res: NotesCreateResponse };
'notes/delete': { req: NotesDeleteRequest; res: EmptyResponse };
'notes/drafts/count': { req: EmptyRequest; res: NotesDraftsCountResponse };
'notes/drafts/create': { req: NotesDraftsCreateRequest; res: NotesDraftsCreateResponse };
'notes/drafts/delete': { req: NotesDraftsDeleteRequest; res: EmptyResponse };
'notes/drafts/list': { req: NotesDraftsListRequest; res: NotesDraftsListResponse };
'notes/drafts/update': { req: NotesDraftsUpdateRequest; res: NotesDraftsUpdateResponse };
'notes/favorites/create': { req: NotesFavoritesCreateRequest; res: EmptyResponse };
'notes/favorites/delete': { req: NotesFavoritesDeleteRequest; res: EmptyResponse };
'notes/featured': { req: NotesFeaturedRequest; res: NotesFeaturedResponse };

View File

@@ -491,6 +491,14 @@ export type NotesConversationResponse = operations['notes___conversation']['resp
export type NotesCreateRequest = operations['notes___create']['requestBody']['content']['application/json'];
export type NotesCreateResponse = operations['notes___create']['responses']['200']['content']['application/json'];
export type NotesDeleteRequest = operations['notes___delete']['requestBody']['content']['application/json'];
export type NotesDraftsCountResponse = operations['notes___drafts___count']['responses']['200']['content']['application/json'];
export type NotesDraftsCreateRequest = operations['notes___drafts___create']['requestBody']['content']['application/json'];
export type NotesDraftsCreateResponse = operations['notes___drafts___create']['responses']['200']['content']['application/json'];
export type NotesDraftsDeleteRequest = operations['notes___drafts___delete']['requestBody']['content']['application/json'];
export type NotesDraftsListRequest = operations['notes___drafts___list']['requestBody']['content']['application/json'];
export type NotesDraftsListResponse = operations['notes___drafts___list']['responses']['200']['content']['application/json'];
export type NotesDraftsUpdateRequest = operations['notes___drafts___update']['requestBody']['content']['application/json'];
export type NotesDraftsUpdateResponse = operations['notes___drafts___update']['responses']['200']['content']['application/json'];
export type NotesFavoritesCreateRequest = operations['notes___favorites___create']['requestBody']['content']['application/json'];
export type NotesFavoritesDeleteRequest = operations['notes___favorites___delete']['requestBody']['content']['application/json'];
export type NotesFeaturedRequest = operations['notes___featured']['requestBody']['content']['application/json'];

View File

@@ -14,6 +14,7 @@ export type Ad = components['schemas']['Ad'];
export type Announcement = components['schemas']['Announcement'];
export type App = components['schemas']['App'];
export type Note = components['schemas']['Note'];
export type NoteDraft = components['schemas']['NoteDraft'];
export type NoteReaction = components['schemas']['NoteReaction'];
export type NoteFavorite = components['schemas']['NoteFavorite'];
export type Notification = components['schemas']['Notification'];

View File

@@ -2948,6 +2948,51 @@ export type paths = {
*/
post: operations['notes___delete'];
};
'/notes/drafts/count': {
/**
* notes/drafts/count
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
post: operations['notes___drafts___count'];
};
'/notes/drafts/create': {
/**
* notes/drafts/create
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
post: operations['notes___drafts___create'];
};
'/notes/drafts/delete': {
/**
* notes/drafts/delete
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
post: operations['notes___drafts___delete'];
};
'/notes/drafts/list': {
/**
* notes/drafts/list
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
post: operations['notes___drafts___list'];
};
'/notes/drafts/update': {
/**
* notes/drafts/update
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
post: operations['notes___drafts___update'];
};
'/notes/favorites/create': {
/**
* notes/favorites/create
@@ -4315,6 +4360,61 @@ export type components = {
hasPoll?: boolean;
myReaction?: string | null;
};
NoteDraft: {
/**
* Format: id
* @example xxxxxxxxxx
*/
id: string;
/** Format: date-time */
createdAt: string;
text: string | null;
cw?: string | null;
/** Format: id */
userId: string;
user: components['schemas']['UserLite'];
/**
* Format: id
* @example xxxxxxxxxx
*/
replyId?: string | null;
/**
* Format: id
* @example xxxxxxxxxx
*/
renoteId?: string | null;
reply?: components['schemas']['Note'] | null;
renote?: components['schemas']['Note'] | null;
/** @enum {string} */
visibility: 'public' | 'home' | 'followers' | 'specified';
visibleUserIds?: string[];
fileIds?: string[];
files?: components['schemas']['DriveFile'][];
hashtag?: string;
poll?: {
/** Format: date-time */
expiresAt?: string | null;
expiredAfter?: number | null;
multiple: boolean;
choices: string[];
} | null;
/**
* Format: id
* @example xxxxxxxxxx
*/
channelId?: string | null;
channel?: {
id: string;
name: string;
color: string;
isSensitive: boolean;
allowRenoteToExternal: boolean;
userId: string | null;
} | null;
localOnly?: boolean;
/** @enum {string|null} */
reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null;
};
NoteReaction: {
/**
* Format: id
@@ -5106,6 +5206,7 @@ export type components = {
canImportUserLists: boolean;
/** @enum {string} */
chatAvailability: 'available' | 'readonly' | 'unavailable';
noteDraftLimit: number;
};
ReversiGameLite: {
/** Format: id */
@@ -28586,6 +28687,407 @@ export interface operations {
};
};
};
notes___drafts___count: {
responses: {
/** @description OK (with results) */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': number;
};
};
/** @description Client error */
400: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
notes___drafts___create: {
requestBody: {
content: {
'application/json': {
/**
* @default public
* @enum {string}
*/
visibility?: 'public' | 'home' | 'followers' | 'specified';
visibleUserIds?: string[];
cw?: string | null;
hashtag?: string | null;
/** @default false */
localOnly?: boolean;
/**
* @default null
* @enum {string|null}
*/
reactionAcceptance?: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote';
/** Format: misskey:id */
replyId?: string | null;
/** Format: misskey:id */
renoteId?: string | null;
/** Format: misskey:id */
channelId?: string | null;
text?: string | null;
fileIds?: string[];
poll?: {
choices: string[];
multiple?: boolean;
expiresAt?: number | null;
expiredAfter?: number | null;
} | null;
};
};
};
responses: {
/** @description OK (with results) */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': {
createdDraft: components['schemas']['NoteDraft'];
};
};
};
/** @description Client error */
400: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Too many requests */
429: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
notes___drafts___delete: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
draftId: string;
};
};
};
responses: {
/** @description OK (without any results) */
204: {
headers: {
[name: string]: unknown;
};
};
/** @description Client error */
400: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
notes___drafts___list: {
requestBody: {
content: {
'application/json': {
/** @default 30 */
limit?: number;
/** Format: misskey:id */
sinceId?: string;
/** Format: misskey:id */
untilId?: string;
};
};
};
responses: {
/** @description OK (with results) */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['NoteDraft'][];
};
};
/** @description Client error */
400: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
notes___drafts___update: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
draftId: string;
/**
* @default public
* @enum {string}
*/
visibility?: 'public' | 'home' | 'followers' | 'specified';
visibleUserIds?: string[];
cw?: string | null;
hashtag?: string | null;
/** @default false */
localOnly?: boolean;
/**
* @default null
* @enum {string|null}
*/
reactionAcceptance?: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote';
/** Format: misskey:id */
replyId?: string | null;
/** Format: misskey:id */
renoteId?: string | null;
/** Format: misskey:id */
channelId?: string | null;
text?: string | null;
fileIds?: string[];
poll?: {
choices: string[];
multiple?: boolean;
expiresAt?: number | null;
expiredAfter?: number | null;
} | null;
};
};
};
responses: {
/** @description OK (with results) */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': {
updatedDraft: components['schemas']['NoteDraft'];
};
};
};
/** @description Client error */
400: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Too many requests */
429: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
notes___favorites___create: {
requestBody: {
content: {