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

@@ -0,0 +1,157 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { noteVisibilities, noteReactionAcceptances } from '@/types.js';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiChannel } from './Channel.js';
import { MiNote } from './Note.js';
import type { MiDriveFile } from './DriveFile.js';
@Entity('note_draft')
export class MiNoteDraft {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The ID of reply target.',
})
public replyId: MiNote['id'] | null;
@ManyToOne(type => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
public reply: MiNote | null;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The ID of renote target.',
})
public renoteId: MiNote['id'] | null;
@ManyToOne(type => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
public renote: MiNote | null;
// TODO: varcharにしたい(Note.tsと同じ)
@Column('text', {
nullable: true,
})
public text: string | null;
@Column('varchar', {
length: 512, nullable: true,
})
public cw: string | null;
@Index()
@Column({
...id(),
comment: 'The ID of author.',
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;
@Column('boolean', {
default: false,
})
public localOnly: boolean;
@Column('varchar', {
length: 64, nullable: true,
})
public reactionAcceptance: typeof noteReactionAcceptances[number];
/**
* public ... 公開
* home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す
* followers ... フォロワーのみ
* specified ... visibleUserIds で指定したユーザーのみ
*/
@Column('enum', { enum: noteVisibilities })
public visibility: typeof noteVisibilities[number];
@Index('IDX_NOTE_DRAFT_FILE_IDS', { synchronize: false })
@Column({
...id(),
array: true, default: '{}',
})
public fileIds: MiDriveFile['id'][];
@Index('IDX_NOTE_DRAFT_VISIBLE_USER_IDS', { synchronize: false })
@Column({
...id(),
array: true, default: '{}',
})
public visibleUserIds: MiUser['id'][];
@Column('varchar', {
length: 128, nullable: true,
})
public hashtag: string | null;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The ID of source channel.',
})
public channelId: MiChannel['id'] | null;
@ManyToOne(type => MiChannel, {
onDelete: 'CASCADE',
})
@JoinColumn()
public channel: MiChannel | null;
// 以下、Pollについて追加
@Column('boolean', {
default: false,
})
public hasPoll: boolean;
@Column('varchar', {
length: 256, array: true, default: '{}',
})
public pollChoices: string[];
@Column('boolean')
public pollMultiple: boolean;
@Column('timestamp with time zone', {
nullable: true,
})
public pollExpiresAt: Date | null;
@Column('bigint', {
nullable: true,
})
public pollExpiredAfter: number | null;
// ここまで追加
constructor(data: Partial<MiNoteDraft>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}