feat: ability to make existing posts private

This commit is contained in:
naskya 2023-10-15 14:46:58 +09:00
parent 764e516a3c
commit 5e0b1f1114
Signed by: naskya
GPG key ID: 164DFF24E2D40139
7 changed files with 105 additions and 6 deletions

View file

@ -21,6 +21,7 @@
- 「秘密」という公開範囲を追加
- 宛先無しのダイレクト投稿を言い換えているだけです
- 既存の投稿を削除せずに後から秘密にすることもできます
- パフォーマンス向上のためアクティブユーザー以外のチャート生成を無効化
- サードパーティー製クライアントが動かなくなるのを阻止するため API のエンドポイントは残していますが、叩いても `0` が並んだ配列しか返しません。
- モデレーターでない一般ユーザーにもカスタム絵文字の管理権を与えられるように

View file

@ -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?"

View file

@ -2015,3 +2015,5 @@ openServerInfo: "投稿内のサーバー名をクリックでサーバー情報
searchEngine: "検索の MFM で使用する検索エンジン"
postSearch: "このサーバーの投稿検索"
indexableDescription: MastodonやFirefishなどの検索機能に、あなたの投稿が表示されるのを許可します。
makePrivate: "秘密にする"
makePrivateConfirm: "リモートサーバーに削除リクエストを送信し、投稿の公開範囲を「秘密」にして他の人から見られないようにします。実行しますか?"

View file

@ -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],

View file

@ -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: [],
});
});

View file

@ -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);

View file

@ -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",