diff --git a/README.md b/README.md index 385c5428..8d2289ac 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ - 「秘密」という公開範囲を追加 - 宛先無しのダイレクト投稿を言い換えているだけです + - 既存の投稿を削除せずに後から秘密にすることもできます - パフォーマンス向上のためアクティブユーザー以外のチャート生成を無効化 - サードパーティー製クライアントが動かなくなるのを阻止するため API のエンドポイントは残していますが、叩いても `0` が並んだ配列しか返しません。 - モデレーターでない一般ユーザーにもカスタム絵文字の管理権を与えられるように diff --git a/locales/en-US.yml b/locales/en-US.yml index 4d05afb0..d2fc86e6 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -2176,3 +2176,5 @@ hideMyIcon: "Hide my icon" hideMyName: "Hide my name and ID" searchEngine: "Search engine used in search bar MFM" postSearch: "Post search on this server" +makePrivate: "Make private" +makePrivateConfirm: "This operation will send a deletion request to remote servers and change the visibility to private. Proceed?" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 3518224b..8b937ccb 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2015,3 +2015,5 @@ openServerInfo: "投稿内のサーバー名をクリックでサーバー情報 searchEngine: "検索の MFM で使用する検索エンジン" postSearch: "このサーバーの投稿検索" indexableDescription: MastodonやFirefishなどの検索機能に、あなたの投稿が表示されるのを許可します。 +makePrivate: "秘密にする" +makePrivateConfirm: "リモートサーバーに削除リクエストを送信し、投稿の公開範囲を「秘密」にして他の人から見られないようにします。実行しますか?" diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 58eff1ac..621b92fe 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -255,6 +255,7 @@ import * as ep___notes_globalTimeline from "./endpoints/notes/global-timeline.js import * as ep___notes_hybridTimeline from "./endpoints/notes/hybrid-timeline.js"; import * as ep___notes_localTimeline from "./endpoints/notes/local-timeline.js"; import * as ep___notes_recommendedTimeline from "./endpoints/notes/recommended-timeline.js"; +import * as ep___notes_makePrivate from "./endpoints/notes/make-private.js"; import * as ep___notes_mentions from "./endpoints/notes/mentions.js"; import * as ep___notes_polls_recommendation from "./endpoints/notes/polls/recommendation.js"; import * as ep___notes_polls_vote from "./endpoints/notes/polls/vote.js"; @@ -609,6 +610,7 @@ const eps = [ ["notes/hybrid-timeline", ep___notes_hybridTimeline], ["notes/local-timeline", ep___notes_localTimeline], ["notes/recommended-timeline", ep___notes_recommendedTimeline], + ["notes/make-private", ep___notes_makePrivate], ["notes/mentions", ep___notes_mentions], ["notes/polls/recommendation", ep___notes_polls_recommendation], ["notes/polls/vote", ep___notes_polls_vote], diff --git a/packages/backend/src/server/api/endpoints/notes/make-private.ts b/packages/backend/src/server/api/endpoints/notes/make-private.ts new file mode 100644 index 00000000..749b40b7 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/make-private.ts @@ -0,0 +1,60 @@ +import deleteNote from "@/services/note/delete.js"; +import { Notes } from "@/models/index.js"; +import define from "../../define.js"; +import { getNote } from "../../common/getters.js"; +import { ApiError } from "../../error.js"; +import { SECOND, HOUR } from "@/const.js"; + +export const meta = { + tags: ["notes"], + + requireCredential: true, + + kind: "write:notes", + + limit: { + duration: HOUR, + max: 300, + minInterval: SECOND, + }, + + errors: { + noSuchNote: { + message: "No such note.", + code: "NO_SUCH_NOTE", + id: "490be23f-8c1f-4796-819f-94cb4f9d1630", + }, + + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, + }, +} as const; + +export const paramDef = { + type: "object", + properties: { + noteId: { type: "string", format: "misskey:id" }, + }, + required: ["noteId"], +} as const; + +export default define(meta, paramDef, async (ps, user) => { + const note = await getNote(ps.noteId, user).catch((err) => { + if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") + throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + if (note.userId !== user.id) { + throw new ApiError(meta.errors.accessDenied); + } + + await deleteNote(user, note, false, false); + await Notes.update(note.id, { + visibility: "specified", + visibleUserIds: [], + }); +}); diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index b43f737e..a14af82d 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -27,19 +27,21 @@ export default async function ( user: { id: User["id"]; uri: User["uri"]; host: User["host"] }, note: Note, quiet = false, + deleteFromDb = true, ) { const deletedAt = new Date(); // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき if ( note.renoteId && - (await countSameRenotes(user.id, note.renoteId, note.id)) === 0 + (await countSameRenotes(user.id, note.renoteId, note.id)) === 0 && + deleteFromDb ) { Notes.decrement({ id: note.renoteId }, "renoteCount", 1); Notes.decrement({ id: note.renoteId }, "score", 1); } - if (note.replyId) { + if (note.replyId && deleteFromDb) { await Notes.decrement({ id: note.replyId }, "repliesCount", 1); } @@ -106,10 +108,12 @@ export default async function ( } } - await Notes.delete({ - id: note.id, - userId: user.id, - }); + if (deleteFromDb) { + await Notes.delete({ + id: note.id, + userId: user.id, + }); + } if (meilisearch) { await meilisearch.deleteNotes(note.id); diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index 0abcf865..a8812cda 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -10,6 +10,7 @@ import { url } from "@/config"; import { noteActions } from "@/store"; import { shareAvailable } from "@/scripts/share-available"; import { getUserMenu } from "@/scripts/get-user-menu"; +import { unisonReload } from "@/scripts/unison-reload"; export function getNoteMenu(props: { note: firefish.entities.Note; @@ -72,6 +73,21 @@ export function getNoteMenu(props: { }); } + function makePrivate(): void { + os.confirm({ + type: "warning", + text: i18n.ts.makePrivateConfirm, + }).then(async ({ canceled }) => { + if (canceled) return; + + await os.api("notes/make-private", { + noteId: appearNote.id, + }); + + unisonReload(); + }); + } + function toggleFavorite(favorite: boolean): void { os.apiWithDialog( favorite ? "notes/favorites/create" : "notes/favorites/delete", @@ -437,6 +453,18 @@ export function getNoteMenu(props: { action: edit, } : undefined, + isAppearAuthor && + !( + appearNote.visibility === "specified" && + appearNote.visibleUserIds.length === 0 + ) + ? { + icon: "ph-eye-slash ph-bold ph-lg", + text: i18n.ts.makePrivate, + danger: true, + action: makePrivate, + } + : undefined, isAppearAuthor ? { icon: "ph-eraser ph-bold ph-lg",