From 2c6f3a998986fa54bcd00dba046abac5a565f866 Mon Sep 17 00:00:00 2001 From: Kaity A Date: Sun, 7 May 2023 20:27:25 +1000 Subject: [PATCH 01/10] Note editing --- packages/backend/src/server/api/endpoints.ts | 2 + .../src/server/api/endpoints/notes/edit.ts | 643 ++++++++++++++++++ packages/calckey-js/src/api.types.ts | 43 +- packages/calckey-js/src/entities.ts | 2 + packages/client/src/components/MkPostForm.vue | 12 +- .../src/components/MkPostFormDialog.vue | 1 + .../client/src/components/MkRenoteButton.vue | 7 +- .../src/components/MkSubNoteContent.vue | 18 +- 8 files changed, 699 insertions(+), 29 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/notes/edit.ts diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index e6f8f7ee6..f629a505c 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -242,6 +242,7 @@ import * as ep___notes_clips from "./endpoints/notes/clips.js"; import * as ep___notes_conversation from "./endpoints/notes/conversation.js"; import * as ep___notes_create from "./endpoints/notes/create.js"; import * as ep___notes_delete from "./endpoints/notes/delete.js"; +import * as ep___notes_edit from "./endpoints/notes/edit.js"; import * as ep___notes_favorites_create from "./endpoints/notes/favorites/create.js"; import * as ep___notes_favorites_delete from "./endpoints/notes/favorites/delete.js"; import * as ep___notes_featured from "./endpoints/notes/featured.js"; @@ -590,6 +591,7 @@ const eps = [ ["notes/conversation", ep___notes_conversation], ["notes/create", ep___notes_create], ["notes/delete", ep___notes_delete], + ["notes/edit", ep___notes_edit], ["notes/favorites/create", ep___notes_favorites_create], ["notes/favorites/delete", ep___notes_favorites_delete], ["notes/featured", ep___notes_featured], diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts new file mode 100644 index 000000000..7cddb7d21 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -0,0 +1,643 @@ +import { In } from "typeorm"; +import create, { index } from "@/services/note/create.js"; +import type { IRemoteUser, User } from "@/models/entities/user.js"; +import { + Users, + DriveFiles, + Notes, + Channels, + Blockings, + UserProfiles, + Polls, + NoteEdits, +} from "@/models/index.js"; +import type { DriveFile } from "@/models/entities/drive-file.js"; +import type { IMentionedRemoteUsers, Note } from "@/models/entities/note.js"; +import type { Channel } from "@/models/entities/channel.js"; +import { MAX_NOTE_TEXT_LENGTH } from "@/const.js"; +import { noteVisibilities } from "../../../../types.js"; +import { ApiError } from "../../error.js"; +import define from "../../define.js"; +import { HOUR } from "@/const.js"; +import { getNote } from "../../common/getters.js"; +import { Poll } from "@/models/entities/poll.js"; +import * as mfm from "mfm-js"; +import { concat } from "@/prelude/array.js"; +import { extractHashtags } from "@/misc/extract-hashtags.js"; +import { extractCustomEmojisFromMfm } from "@/misc/extract-custom-emojis-from-mfm.js"; +import { extractMentionedUsers } from "@/services/note/create.js"; +import { genId } from "@/misc/gen-id.js"; +import { publishNoteStream } from "@/services/stream.js"; +import DeliverManager from "@/remote/activitypub/deliver-manager.js"; +import { renderActivity } from "@/remote/activitypub/renderer/index.js"; +import renderNote from "@/remote/activitypub/renderer/note.js"; +import renderUpdate from "@/remote/activitypub/renderer/update.js"; +import { deliverToRelays } from "@/services/relay.js"; + +export const meta = { + tags: ["notes"], + + requireCredential: true, + + limit: { + duration: HOUR, + max: 300, + }, + + kind: "write:notes", + + res: { + type: "object", + optional: false, + nullable: false, + properties: { + createdNote: { + type: "object", + optional: false, + nullable: false, + ref: "Note", + }, + }, + }, + + errors: { + noSuchRenoteTarget: { + message: "No such renote target.", + code: "NO_SUCH_RENOTE_TARGET", + id: "b5c90186-4ab0-49c8-9bba-a1f76c282ba4", + }, + + cannotReRenote: { + message: "You can not Renote a pure Renote.", + code: "CANNOT_RENOTE_TO_A_PURE_RENOTE", + id: "fd4cc33e-2a37-48dd-99cc-9b806eb2031a", + }, + + noSuchReplyTarget: { + message: "No such reply target.", + code: "NO_SUCH_REPLY_TARGET", + id: "749ee0f6-d3da-459a-bf02-282e2da4292c", + }, + + cannotReplyToPureRenote: { + message: "You can not reply to a pure Renote.", + code: "CANNOT_REPLY_TO_A_PURE_RENOTE", + id: "3ac74a84-8fd5-4bb0-870f-01804f82ce15", + }, + + cannotCreateAlreadyExpiredPoll: { + message: "Poll is already expired.", + code: "CANNOT_CREATE_ALREADY_EXPIRED_POLL", + id: "04da457d-b083-4055-9082-955525eda5a5", + }, + + noSuchChannel: { + message: "No such channel.", + code: "NO_SUCH_CHANNEL", + id: "b1653923-5453-4edc-b786-7c4f39bb0bbb", + }, + + youHaveBeenBlocked: { + message: "You have been blocked by this user.", + code: "YOU_HAVE_BEEN_BLOCKED", + id: "b390d7e1-8a5e-46ed-b625-06271cafd3d3", + }, + + accountLocked: { + message: "You migrated. Your account is now locked.", + code: "ACCOUNT_LOCKED", + id: "d390d7e1-8a5e-46ed-b625-06271cafd3d3", + }, + + needsEditId: { + message: "You need to specify `editId`.", + code: "NEEDS_EDIT_ID", + id: "d697edc8-8c73-4de8-bded-35fd198b79e5", + }, + + noSuchNote: { + message: "No such note.", + code: "NO_SUCH_NOTE", + id: "eef6c173-3010-4a23-8674-7c4fcaeba719", + }, + + youAreNotTheAuthor: { + message: "You are not the author of this note.", + code: "YOU_ARE_NOT_THE_AUTHOR", + id: "c6e61685-411d-43d0-b90a-a448d2539001", + }, + + cannotPrivateRenote: { + message: "You can not perform a private renote.", + code: "CANNOT_PRIVATE_RENOTE", + id: "19a50f1c-84fa-4e33-81d3-17834ccc0ad8", + }, + + notLocalUser: { + message: "You are not a local user.", + code: "NOT_LOCAL_USER", + id: "b907f407-2aa0-4283-800b-a2c56290b822", + }, + }, +} as const; + +export const paramDef = { + type: "object", + properties: { + editId: { type: "string", format: "misskey:id" }, + visibility: { type: "string", enum: noteVisibilities, default: "public" }, + visibleUserIds: { + type: "array", + uniqueItems: true, + items: { + type: "string", + format: "misskey:id", + }, + }, + text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true }, + cw: { type: "string", nullable: true, maxLength: 250 }, + localOnly: { type: "boolean", default: false }, + noExtractMentions: { type: "boolean", default: false }, + noExtractHashtags: { type: "boolean", default: false }, + noExtractEmojis: { type: "boolean", default: false }, + fileIds: { + type: "array", + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: "string", format: "misskey:id" }, + }, + mediaIds: { + deprecated: true, + description: + "Use `fileIds` instead. If both are specified, this property is discarded.", + type: "array", + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: "string", format: "misskey:id" }, + }, + replyId: { type: "string", format: "misskey:id", nullable: true }, + renoteId: { type: "string", format: "misskey:id", nullable: true }, + channelId: { type: "string", format: "misskey:id", nullable: true }, + poll: { + type: "object", + nullable: true, + properties: { + choices: { + type: "array", + uniqueItems: true, + minItems: 2, + maxItems: 10, + items: { type: "string", minLength: 1, maxLength: 50 }, + }, + multiple: { type: "boolean", default: false }, + expiresAt: { type: "integer", nullable: true }, + expiredAfter: { type: "integer", nullable: true, minimum: 1 }, + }, + required: ["choices"], + }, + }, + anyOf: [ + { + // (re)note with text, files and poll are optional + properties: { + text: { + type: "string", + minLength: 1, + maxLength: MAX_NOTE_TEXT_LENGTH, + nullable: false, + }, + }, + required: ["text"], + }, + { + // (re)note with files, text and poll are optional + required: ["fileIds"], + }, + { + // (re)note with files, text and poll are optional + required: ["mediaIds"], + }, + { + // (re)note with poll, text and files are optional + properties: { + poll: { type: "object", nullable: false }, + }, + required: ["poll"], + }, + { + // pure renote + required: ["renoteId"], + }, + ], +} as const; + +export default define(meta, paramDef, async (ps, user) => { + if (user.movedToUri != null) throw new ApiError(meta.errors.accountLocked); + + if (!Users.isLocalUser(user)) { + throw new ApiError(meta.errors.notLocalUser); + } + + if (!ps.editId) { + throw new ApiError(meta.errors.needsEditId); + } + + let publishing = false; + let note = await Notes.findOneBy({ + id: ps.editId, + }); + + if (note == null) { + throw new ApiError(meta.errors.noSuchNote); + } + + if (note.userId !== user.id) { + throw new ApiError(meta.errors.youAreNotTheAuthor); + } + + let renote: Note | null = null; + if (ps.renoteId != null) { + // Fetch renote to note + renote = await getNote(ps.renoteId, user).catch((e) => { + if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") + throw new ApiError(meta.errors.noSuchRenoteTarget); + throw e; + }); + + if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) { + throw new ApiError(meta.errors.cannotReRenote); + } + + // Check blocking + if (renote.userId !== user.id) { + const block = await Blockings.findOneBy({ + blockerId: renote.userId, + blockeeId: user.id, + }); + if (block) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } + } + + let reply: Note | null = null; + if (ps.replyId != null) { + // Fetch reply + reply = await getNote(ps.replyId, user).catch((e) => { + if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") + throw new ApiError(meta.errors.noSuchReplyTarget); + throw e; + }); + + if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) { + throw new ApiError(meta.errors.cannotReplyToPureRenote); + } + + // Check blocking + if (reply.userId !== user.id) { + const block = await Blockings.findOneBy({ + blockerId: reply.userId, + blockeeId: user.id, + }); + if (block) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } + } + + let channel: Channel | null = null; + if (ps.channelId != null) { + channel = await Channels.findOneBy({ id: ps.channelId }); + + if (channel == null) { + throw new ApiError(meta.errors.noSuchChannel); + } + } + + // enforce silent clients on server + if (user.isSilenced && ps.visibility === "public" && ps.channelId == null) { + ps.visibility = "home"; + } + + // Reject if the target of the renote is a public range other than "Home or Entire". + if ( + renote && + renote.visibility !== "public" && + renote.visibility !== "home" && + renote.userId !== user.id + ) { + throw new ApiError(meta.errors.cannotPrivateRenote); + } + + // If the target of the renote is not public, make it home. + if (renote && renote.visibility !== "public" && ps.visibility === "public") { + ps.visibility = "home"; + } + + // If the reply target is not public, make it home. + if (reply && reply.visibility !== "public" && ps.visibility === "public") { + ps.visibility = "home"; + } + + // Renote local only if you Renote local only. + if (renote?.localOnly && ps.channelId == null) { + ps.localOnly = true; + } + + // If you reply to local only, make it local only. + if (reply?.localOnly && ps.channelId == null) { + ps.localOnly = true; + } + + if (ps.text) { + ps.text = ps.text.trim(); + } else { + ps.text = null; + } + + let tags = []; + let emojis = []; + let mentionedUsers = []; + + const tokens = ps.text ? mfm.parse(ps.text) : []; + const cwTokens = ps.cw ? mfm.parse(ps.cw) : []; + const choiceTokens = ps.poll?.choices + ? concat(ps.poll.choices.map((choice) => mfm.parse(choice))) + : []; + + const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens); + + tags = extractHashtags(combinedTokens); + + emojis = extractCustomEmojisFromMfm(combinedTokens); + + mentionedUsers = await extractMentionedUsers(user, combinedTokens); + + tags = [...new Set(tags)] + .sort() + .filter((tag) => Array.from(tag || "").length <= 128) + .splice(0, 32); + + emojis = [...new Set(emojis)].sort(); + + if ( + reply && + user.id !== reply.userId && + !mentionedUsers.some((u) => u.id === reply?.userId) + ) { + mentionedUsers.push(await Users.findOneByOrFail({ id: reply.userId })); + } + + let visibleUsers: User[] = []; + if (ps.visibleUserIds) { + visibleUsers = await Users.findBy({ + id: In(ps.visibleUserIds), + }); + } + + if (ps.visibility === "specified") { + if (visibleUsers == null) throw new Error("invalid param"); + + for (const u of visibleUsers) { + if (!mentionedUsers.some((x) => x.id === u.id)) { + mentionedUsers.push(u); + } + } + + if (reply && !visibleUsers.some((x) => x.id === reply?.userId)) { + visibleUsers.push(await Users.findOneByOrFail({ id: reply.userId })); + } + } + + let files: DriveFile[] = []; + const fileIds = + ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; + if (fileIds != null) { + files = await DriveFiles.createQueryBuilder("file") + .where("file.userId = :userId AND file.id IN (:...fileIds)", { + userId: user.id, + fileIds, + }) + .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') + .setParameters({ fileIds }) + .getMany(); + } + + if (ps.poll) { + let expires = ps.poll.expiresAt; + if (typeof expires === "number") { + if (expires < Date.now()) { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); + } + } else if (typeof ps.poll.expiredAfter === "number") { + expires = Date.now() + ps.poll.expiredAfter; + } + + let poll = await Polls.findOneBy({ noteId: note.id }); + const pp = ps.poll; + if (!poll && pp) { + poll = new Poll({ + noteId: note.id, + choices: pp.choices, + expiresAt: expires ? new Date(expires) : null, + multiple: pp.multiple, + votes: new Array(pp.choices.length).fill(0), + noteVisibility: ps.visibility, + userId: user.id, + userHost: user.host, + }); + await Polls.insert(poll); + publishing = true; + } else if (poll && !pp) { + await Polls.remove(poll); + publishing = true; + } else if (poll && pp) { + const pollUpdate: Partial = {}; + if (poll.expiresAt !== expires) { + pollUpdate.expiresAt = expires ? new Date(expires) : null; + } + if (poll.multiple !== pp.multiple) { + pollUpdate.multiple = pp.multiple; + } + if (poll.noteVisibility !== ps.visibility) { + pollUpdate.noteVisibility = ps.visibility; + } + // We can't do an unordered equal check because the order of choices + // is important and if it changes, we need to reset the votes. + if (JSON.stringify(poll.choices) !== JSON.stringify(pp.choices)) { + pollUpdate.choices = pp.choices; + pollUpdate.votes = new Array(pp.choices.length).fill(0); + } + if (notEmpty(pollUpdate)) { + await Polls.update(note.id, pollUpdate); + } + publishing = true; + } + } + + const mentionedUserLookup: Record = {}; + mentionedUsers.forEach((u) => { + mentionedUserLookup[u.id] = u; + }); + + const mentionedUserIds = [...new Set(mentionedUsers.map((u) => u.id))].sort(); + + const remoteUsers = mentionedUserIds + .map((id) => mentionedUserLookup[id]) + .filter((u) => u.host != null); + + const remoteUserIds = remoteUsers.map((user) => user.id); + const remoteProfiles = await UserProfiles.findBy({ + userId: In(remoteUserIds), + }); + const mentionedRemoteUsers = remoteUsers.map((user) => { + const profile = remoteProfiles.find( + (profile) => profile.userId === user.id, + ); + return { + username: user.username, + host: user.host ?? null, + uri: user.uri, + url: profile ? profile.url : undefined, + } as IMentionedRemoteUsers[0]; + }); + + const update: Partial = {}; + if (ps.text !== note.text) { + update.text = ps.text; + } + if (ps.cw !== note.cw) { + update.cw = ps.cw; + } + if (ps.visibility !== note.visibility) { + update.visibility = ps.visibility; + } + if (ps.localOnly !== note.localOnly) { + update.localOnly = ps.localOnly; + } + if (ps.visibleUserIds !== note.visibleUserIds) { + update.visibleUserIds = ps.visibleUserIds; + } + if (!unorderedEqual(mentionedUserIds, note.mentions)) { + update.mentions = mentionedUserIds; + update.mentionedRemoteUsers = JSON.stringify(mentionedRemoteUsers); + } + if (ps.channelId !== note.channelId) { + update.channelId = ps.channelId; + } + if (ps.replyId !== note.replyId) { + update.replyId = ps.replyId; + } + if (ps.renoteId !== note.renoteId) { + update.renoteId = ps.renoteId; + } + if (note.hasPoll !== !!ps.poll) { + update.hasPoll = !!ps.poll; + } + if (!unorderedEqual(emojis, note.emojis)) { + update.emojis = emojis; + } + if (!unorderedEqual(tags, note.tags)) { + update.tags = tags; + } + if (!unorderedEqual(ps.fileIds || [], note.fileIds)) { + update.fileIds = fileIds || undefined; + + if (fileIds) { + // Get attachedFileTypes for each file with fileId from fileIds + const attachedFiles = fileIds.map((fileId) => + files.find((file) => file.id === fileId), + ); + update.attachedFileTypes = attachedFiles.map( + (file) => file?.type || "application/octet-stream", + ); + } else { + update.attachedFileTypes = undefined; + } + } + + if (notEmpty(update)) { + update.updatedAt = new Date(); + await Notes.update(note.id, update); + + // Add NoteEdit history + await NoteEdits.insert({ + id: genId(), + noteId: note.id, + text: ps.text || undefined, + cw: ps.cw, + fileIds: ps.fileIds, + updatedAt: new Date(), + }); + + publishing = true; + } + + note = await Notes.findOneBy({ id: note.id }); + if (!note) { + throw new ApiError(meta.errors.noSuchNote); + } + + if (publishing) { + index(note); + + // Publish update event for the updated note details + publishNoteStream(note.id, "updated", { + updatedAt: update.updatedAt, + }); + + (async () => { + const noteActivity = await renderNote(note, false); + noteActivity.updated = note.updatedAt.toISOString(); + const updateActivity = renderUpdate(noteActivity, user); + updateActivity.to = noteActivity.to; + updateActivity.cc = noteActivity.cc; + const activity = renderActivity(updateActivity); + const dm = new DeliverManager(user, activity); + + // Delivery to remote mentioned users + for (const u of mentionedUsers.filter((u) => Users.isRemoteUser(u))) { + dm.addDirectRecipe(u as IRemoteUser); + } + + // Post is a reply and remote user is the contributor of the original post + if (note.reply && note.reply.userHost !== null) { + const u = await Users.findOneBy({ id: note.reply.userId }); + if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); + } + + // Post is a renote and remote user is the contributor of the original post + if (note.renote && note.renote.userHost !== null) { + const u = await Users.findOneBy({ id: note.renote.userId }); + if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); + } + + // Deliver to followers for non-direct posts. + if (["public", "home", "followers"].includes(note.visibility)) { + dm.addFollowersRecipe(); + } + + // Deliver to relays for public posts. + if (["public"].includes(note.visibility)) { + deliverToRelays(user, activity); + } + + // GO! + dm.execute(); + })(); + } + + return { + createdNote: await Notes.pack(note, user), + }; +}); + +function unorderedEqual(a: T[], b: T[]) { + return a.length === b.length && a.every((v) => b.includes(v)); +} + +function notEmpty(partial: Partial) { + return Object.keys(partial).length > 0; +} diff --git a/packages/calckey-js/src/api.types.ts b/packages/calckey-js/src/api.types.ts index 478b86721..d5a909b74 100644 --- a/packages/calckey-js/src/api.types.ts +++ b/packages/calckey-js/src/api.types.ts @@ -43,6 +43,26 @@ type NoParams = Record; type ShowUserReq = { username: string; host?: string } | { userId: User["id"] }; +type NoteSubmitReq = { + editId?: null | Note["id"]; + visibility?: "public" | "home" | "followers" | "specified"; + visibleUserIds?: User["id"][]; + text?: null | string; + cw?: null | string; + viaMobile?: boolean; + localOnly?: boolean; + fileIds?: DriveFile["id"][]; + replyId?: null | Note["id"]; + renoteId?: null | Note["id"]; + channelId?: null | Channel["id"]; + poll?: null | { + choices: string[]; + multiple?: boolean; + expiresAt?: null | number; + expiredAfter?: null | number; + }; +}; + export type Endpoints = { // admin "admin/abuse-user-reports": { req: TODO; res: TODO }; @@ -790,27 +810,14 @@ export type Endpoints = { "notes/clips": { req: TODO; res: TODO }; "notes/conversation": { req: TODO; res: TODO }; "notes/create": { - req: { - visibility?: "public" | "home" | "followers" | "specified"; - visibleUserIds?: User["id"][]; - text?: null | string; - cw?: null | string; - viaMobile?: boolean; - localOnly?: boolean; - fileIds?: DriveFile["id"][]; - replyId?: null | Note["id"]; - renoteId?: null | Note["id"]; - channelId?: null | Channel["id"]; - poll?: null | { - choices: string[]; - multiple?: boolean; - expiresAt?: null | number; - expiredAfter?: null | number; - }; - }; + req: NoteSubmitReq; res: { createdNote: Note }; }; "notes/delete": { req: { noteId: Note["id"] }; res: null }; + "notes/edit": { + req: NoteSubmitReq; + res: { createdNote: Note }; + }; "notes/favorites/create": { req: { noteId: Note["id"] }; res: null }; "notes/favorites/delete": { req: { noteId: Note["id"] }; res: null }; "notes/featured": { req: TODO; res: Note[] }; diff --git a/packages/calckey-js/src/entities.ts b/packages/calckey-js/src/entities.ts index bf881df2f..debb4fae4 100644 --- a/packages/calckey-js/src/entities.ts +++ b/packages/calckey-js/src/entities.ts @@ -144,6 +144,7 @@ export type Note = { visibility: "public" | "home" | "followers" | "specified"; visibleUserIds?: User["id"][]; localOnly?: boolean; + channel?: Channel["id"]; myReaction?: string; reactions: Record; renoteCount: number; @@ -163,6 +164,7 @@ export type Note = { }[]; uri?: string; url?: string; + updatedAt?: DateString; isHidden?: boolean; }; diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue index 60d5dee2f..5948a22aa 100644 --- a/packages/client/src/components/MkPostForm.vue +++ b/packages/client/src/components/MkPostForm.vue @@ -274,6 +274,7 @@ const props = withDefaults( instant?: boolean; fixed?: boolean; autofocus?: boolean; + editId?: misskey.entities.Note["id"]; }>(), { initialVisibleUsers: () => [], @@ -334,6 +335,10 @@ const typing = throttle(3000, () => { }); const draftKey = $computed((): string => { + if (props.editId) { + return `edit:${props.editId}`; + } + let key = props.channel ? `channel:${props.channel.id}` : ""; if (props.renote) { @@ -368,7 +373,9 @@ const placeholder = $computed((): string => { }); const submitText = $computed((): string => { - return props.renote + return props.editId + ? i18n.ts.edit + : props.renote ? i18n.ts.quote : props.reply ? i18n.ts.reply @@ -809,6 +816,7 @@ async function post() { const processedText = preprocess(text); let postData = { + editId: props.editId ? props.editId : undefined, text: processedText === "" ? undefined : processedText, fileIds: files.length > 0 ? files.map((f) => f.id) : undefined, replyId: props.reply ? props.reply.id : undefined, @@ -854,7 +862,7 @@ async function post() { } posting = true; - os.api("notes/create", postData, token) + os.api(postData.editId ? "notes/edit" : "notes/create", postData, token) .then(() => { clear(); nextTick(() => { diff --git a/packages/client/src/components/MkPostFormDialog.vue b/packages/client/src/components/MkPostFormDialog.vue index c96a94c0c..b95bf35fa 100644 --- a/packages/client/src/components/MkPostFormDialog.vue +++ b/packages/client/src/components/MkPostFormDialog.vue @@ -39,6 +39,7 @@ const props = defineProps<{ instant?: boolean; fixed?: boolean; autofocus?: boolean; + editId?: misskey.entities.Note["id"]; }>(); const emit = defineEmits<{ diff --git a/packages/client/src/components/MkRenoteButton.vue b/packages/client/src/components/MkRenoteButton.vue index b547e3159..e0f45ecfe 100644 --- a/packages/client/src/components/MkRenoteButton.vue +++ b/packages/client/src/components/MkRenoteButton.vue @@ -45,7 +45,10 @@ const canRenote = computed( props.note.userId === $i.id ); -const getCw = () => (addCw.value ? cwInput.value : props.note.cw ?? undefined); +const getCw = () => + addCw.value && cwInput.value !== "" + ? cwInput.value + : props.note.cw ?? undefined; useTooltip(buttonRef, async (showing) => { const renotes = await os.api("notes/renotes", { @@ -85,7 +88,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => { if ( props.note.visibility === "public" || - props.note.visibility === "hidden" + props.note.visibil ity === "hidden" ) { buttonActions.push({ text: i18n.ts.renote, diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue index 41f6f9fda..85c77f522 100644 --- a/packages/client/src/components/MkSubNoteContent.vue +++ b/packages/client/src/components/MkSubNoteContent.vue @@ -5,7 +5,7 @@ cwHighlight, }" > -

+

+ .text { margin-right: 8px; padding-inline-start: 0.25em; + font-weight: 900; } } .cwHighlight.hasCw { - outline: 1px dotted var(--cwFg); + outline: 1px dotted var(--fg); border-radius: 5px; > .wrmlmaau { padding-inline-start: 0.25em; } > .cw { - background-color: var(--cwFg); - color: var(--cwBg); + background-color: var(--fg); + color: var(--bg); border-top-left-radius: 5px; border-top-right-radius: 5px; - > .reply-icon { - color: var(--cwBg); + > .reply-icon, + > .cw-icon { + padding-inline-start: 0.25em; + color: var(--bg); } } } From fa9a28d230c4adb95aa409591d1bc7eeceda8e11 Mon Sep 17 00:00:00 2001 From: Kaity A Date: Sun, 7 May 2023 20:30:20 +1000 Subject: [PATCH 02/10] Revert accidental commit --- .../client/src/components/MkSubNoteContent.vue | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue index 85c77f522..41f6f9fda 100644 --- a/packages/client/src/components/MkSubNoteContent.vue +++ b/packages/client/src/components/MkSubNoteContent.vue @@ -5,7 +5,7 @@ cwHighlight, }" > -

+

- .text { margin-right: 8px; padding-inline-start: 0.25em; - font-weight: 900; } } .cwHighlight.hasCw { - outline: 1px dotted var(--fg); + outline: 1px dotted var(--cwFg); border-radius: 5px; > .wrmlmaau { padding-inline-start: 0.25em; } > .cw { - background-color: var(--fg); - color: var(--bg); + background-color: var(--cwFg); + color: var(--cwBg); border-top-left-radius: 5px; border-top-right-radius: 5px; - > .reply-icon, - > .cw-icon { - padding-inline-start: 0.25em; - color: var(--bg); + > .reply-icon { + color: var(--cwBg); } } } From 80897d0a3c1503f609dc6f92f0b3b19ef88f52fe Mon Sep 17 00:00:00 2001 From: Kaity A Date: Sun, 7 May 2023 20:48:55 +1000 Subject: [PATCH 03/10] Add in edit buttons --- packages/client/src/scripts/get-note-menu.ts | 98 +++++++++++++++----- 1 file changed, 75 insertions(+), 23 deletions(-) diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index 03c35e132..09281e9e6 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -1,6 +1,5 @@ import { defineAsyncComponent, Ref, inject } from "vue"; import * as misskey from "calckey-js"; -import { pleaseLogin } from "./please-login"; import { $i } from "@/account"; import { i18n } from "@/i18n"; import { instance } from "@/instance"; @@ -12,7 +11,7 @@ import { shareAvailable } from "@/scripts/share-available"; export function getNoteMenu(props: { note: misskey.entities.Note; - menuButton: Ref; + menuButton: Ref; translation: Ref; translating: Ref; isDeleted: Ref; @@ -61,6 +60,39 @@ export function getNoteMenu(props: { }); } + function edit(): void { + os.confirm({ + type: "info", + text: "This feature is experimental, please be careful and report bugs if you find them to @supakaity@blahaj.zone.", + }).then(({ canceled }) => { + if (canceled) return; + + os.post({ + initialNote: appearNote, + renote: appearNote.renote, + reply: appearNote.reply, + channel: appearNote.channel, + editId: appearNote.id, + }); + }); + } + + function duplicate(): void { + os.confirm({ + type: "info", + text: "This feature is experimental, please be careful and report bugs if you find them to @supakaity@blahaj.zone.", + }).then(({ canceled }) => { + if (canceled) return; + + os.post({ + initialNote: appearNote, + renote: appearNote.renote, + reply: appearNote.reply, + channel: appearNote.channel, + }); + }); + } + function toggleFavorite(favorite: boolean): void { os.apiWithDialog( favorite ? "notes/favorites/create" : "notes/favorites/delete", @@ -251,6 +283,9 @@ export function getNoteMenu(props: { noteId: appearNote.id, }); + const isAppearAuthor = appearNote.userId === $i.id; + const isModerator = $i.isAdmin || $i.isModerator; + menu = [ ...(props.currentClipPage?.value.userId === $i.id ? [ @@ -320,7 +355,7 @@ export function getNoteMenu(props: { text: i18n.ts.clip, action: () => clip(), }, - appearNote.userId !== $i.id + !isAppearAuthor ? statePromise.then((state) => state.isWatching ? { @@ -348,7 +383,7 @@ export function getNoteMenu(props: { action: () => toggleThreadMute(true), }, ), - appearNote.userId === $i.id + isAppearAuthor ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? { icon: "ph-push-pin ph-bold ph-lg", @@ -371,7 +406,7 @@ export function getNoteMenu(props: { }] : [] ),*/ - ...(appearNote.userId !== $i.id + ...(!isAppearAuthor ? [ null, { @@ -397,24 +432,41 @@ export function getNoteMenu(props: { }, ] : []), - ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin - ? [ - null, - appearNote.userId === $i.id - ? { - icon: "ph-eraser ph-bold ph-lg", - text: i18n.ts.deleteAndEdit, - action: delEdit, - } - : undefined, - { - icon: "ph-trash ph-bold ph-lg", - text: i18n.ts.delete, - danger: true, - action: del, - }, - ] - : []), + + null, + + isAppearAuthor + ? { + icon: "ph-pencil-line ph-bold ph-lg", + text: i18n.ts.edit, + textStyle: "color: var(--accent)", + action: edit, + } + : undefined, + + { + icon: "ph-copy ph-bold ph-lg", + text: i18n.ts.duplicate, + textStyle: "color: var(--accent)", + action: duplicate, + }, + + isAppearAuthor || isModerator + ? { + icon: "ph-trash ph-bold ph-lg", + text: i18n.ts.delete, + danger: true, + action: del, + } + : undefined, + + isAppearAuthor + ? { + icon: "ph-eraser ph-bold ph-lg", + text: i18n.ts.deleteAndEdit, + action: delEdit, + } + : undefined, ].filter((x) => x !== undefined); } else { menu = [ From ba4e322dbf5cfbd4509b0a8f93b09ad8c8a1ada3 Mon Sep 17 00:00:00 2001 From: Kaity A Date: Sun, 7 May 2023 22:07:40 +1000 Subject: [PATCH 04/10] Fix up PR issues --- .../client/src/components/MkRenoteButton.vue | 2 +- packages/client/src/scripts/get-note-menu.ts | 36 ++++++------------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/packages/client/src/components/MkRenoteButton.vue b/packages/client/src/components/MkRenoteButton.vue index e0f45ecfe..476e46727 100644 --- a/packages/client/src/components/MkRenoteButton.vue +++ b/packages/client/src/components/MkRenoteButton.vue @@ -88,7 +88,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => { if ( props.note.visibility === "public" || - props.note.visibil ity === "hidden" + props.note.visibility === "hidden" ) { buttonActions.push({ text: i18n.ts.renote, diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index 09281e9e6..d36eff609 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -61,35 +61,21 @@ export function getNoteMenu(props: { } function edit(): void { - os.confirm({ - type: "info", - text: "This feature is experimental, please be careful and report bugs if you find them to @supakaity@blahaj.zone.", - }).then(({ canceled }) => { - if (canceled) return; - - os.post({ - initialNote: appearNote, - renote: appearNote.renote, - reply: appearNote.reply, - channel: appearNote.channel, - editId: appearNote.id, - }); + os.post({ + initialNote: appearNote, + renote: appearNote.renote, + reply: appearNote.reply, + channel: appearNote.channel, + editId: appearNote.id, }); } function duplicate(): void { - os.confirm({ - type: "info", - text: "This feature is experimental, please be careful and report bugs if you find them to @supakaity@blahaj.zone.", - }).then(({ canceled }) => { - if (canceled) return; - - os.post({ - initialNote: appearNote, - renote: appearNote.renote, - reply: appearNote.reply, - channel: appearNote.channel, - }); + os.post({ + initialNote: appearNote, + renote: appearNote.renote, + reply: appearNote.reply, + channel: appearNote.channel, }); } From 804dbe69856a26ad3dfcf8d7c6768ffc5f7063c3 Mon Sep 17 00:00:00 2001 From: Kaity A Date: Sat, 13 May 2023 23:41:36 +1000 Subject: [PATCH 05/10] add experimental feature gate --- locales/en-US.yml | 7 ++ .../1683980686995-ExperimentalFeatures.js | 16 ++++ packages/backend/src/models/entities/meta.ts | 5 ++ .../src/server/api/endpoints/admin/meta.ts | 11 +++ .../server/api/endpoints/admin/update-meta.ts | 7 ++ packages/calckey-js/src/entities.ts | 1 + .../client/src/pages/admin/experiments.vue | 83 +++++++++++++++++++ packages/client/src/pages/admin/index.vue | 6 ++ packages/client/src/router.ts | 5 ++ packages/client/src/scripts/get-note-menu.ts | 16 ++-- 10 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 packages/backend/migration/1683980686995-ExperimentalFeatures.js create mode 100644 packages/client/src/pages/admin/experiments.vue diff --git a/locales/en-US.yml b/locales/en-US.yml index 9cb58e6ef..e7c25de71 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -2008,3 +2008,10 @@ _deck: list: "List" mentions: "Mentions" direct: "Direct messages" +_experiments: + title: "Experiments" + alpha: "Alpha" + beta: "Beta" + release: "Release" + enableExperimentalPostEditing: "Enable experimental post editing" + experimentalPostEditingCaption: "Enables the option for users to edit their posts in post options" diff --git a/packages/backend/migration/1683980686995-ExperimentalFeatures.js b/packages/backend/migration/1683980686995-ExperimentalFeatures.js new file mode 100644 index 000000000..a289a9ecd --- /dev/null +++ b/packages/backend/migration/1683980686995-ExperimentalFeatures.js @@ -0,0 +1,16 @@ +export class ExperimentalFeatures1683980686995 { + name = "ExperimentalFeatures1683980686995"; + + async up(queryRunner) { + await queryRunner.query(` + ALTER TABLE "meta" + ADD "experimentalFeatures" jsonb NOT NULL DEFAULT '{}' + `); + } + + async down(queryRunner) { + await queryRunner.query(` + ALTER TABLE "meta" DROP COLUMN "experimentalFeatures" + `); + } +} diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts index 84f9af479..3a3c50d4a 100644 --- a/packages/backend/src/models/entities/meta.ts +++ b/packages/backend/src/models/entities/meta.ts @@ -516,4 +516,9 @@ export class Meta { default: true, }) public enableActiveEmailValidation: boolean; + + @Column('jsonb', { + default: {}, + }) + public experimentalFeatures: Record; } diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 89928af11..b83579cd3 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -2,6 +2,7 @@ import config from "@/config/index.js"; import { fetchMeta } from "@/misc/fetch-meta.js"; import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js"; import define from "../../define.js"; +import { Exp } from "@tensorflow/tfjs"; export const meta = { tags: ["meta"], @@ -470,10 +471,20 @@ export const meta = { optional: false, nullable: false, }, + experimentalFeatures: { + type: "object", + optional: true, + nullable: true, + ref: "MetaExperimentalFeatures", + }, }, }, } as const; +export type MetaExperimentalFeatures = { + postEditing: boolean; +}; + export const paramDef = { type: "object", properties: {}, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 7f92e5e29..586378d15 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -170,6 +170,13 @@ export const paramDef = { objectStorageS3ForcePathStyle: { type: "boolean" }, enableIpLogging: { type: "boolean" }, enableActiveEmailValidation: { type: "boolean" }, + experimentalFeatures: { + type: "object", + nullable: true, + properties: { + postEditing: { type: "boolean" }, + }, + }, }, required: [], } as const; diff --git a/packages/calckey-js/src/entities.ts b/packages/calckey-js/src/entities.ts index 5a581a54c..7ecfba649 100644 --- a/packages/calckey-js/src/entities.ts +++ b/packages/calckey-js/src/entities.ts @@ -304,6 +304,7 @@ export type LiteInstanceMetadata = { url: string; imageUrl: string; }[]; + experimentalFeatures?: Record; }; export type DetailedInstanceMetadata = LiteInstanceMetadata & { diff --git a/packages/client/src/pages/admin/experiments.vue b/packages/client/src/pages/admin/experiments.vue new file mode 100644 index 000000000..2f04b88ab --- /dev/null +++ b/packages/client/src/pages/admin/experiments.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue index 34d0404fc..ddc5fc680 100644 --- a/packages/client/src/pages/admin/index.vue +++ b/packages/client/src/pages/admin/index.vue @@ -300,6 +300,12 @@ const menuDef = $computed(() => [ to: "/admin/database", active: currentPage?.route.name === "database", }, + { + icon: "ph-flask ph-bold ph-lg", + text: i18n.ts._experiments.title, + to: "/admin/experiments", + active: currentPage?.route.name === "experiments", + }, ], }, ] diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts index 7f03279dd..a84c2c8b3 100644 --- a/packages/client/src/router.ts +++ b/packages/client/src/router.ts @@ -540,6 +540,11 @@ export const routes = [ name: "other-settings", component: page(() => import("./pages/admin/custom-css.vue")), }, + { + path: "/experiments", + name: "experiments", + component: page(() => import("./pages/admin/experiments.vue")), + }, { path: "/", component: page(() => import("./pages/_empty_.vue")), diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index d36eff609..72147c1c6 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -421,7 +421,7 @@ export function getNoteMenu(props: { null, - isAppearAuthor + instance.experimentalFeatures?.postEditing && isAppearAuthor ? { icon: "ph-pencil-line ph-bold ph-lg", text: i18n.ts.edit, @@ -430,12 +430,14 @@ export function getNoteMenu(props: { } : undefined, - { - icon: "ph-copy ph-bold ph-lg", - text: i18n.ts.duplicate, - textStyle: "color: var(--accent)", - action: duplicate, - }, + instance.experimentalFeatures?.postEditing + ? { + icon: "ph-copy ph-bold ph-lg", + text: i18n.ts.duplicate, + textStyle: "color: var(--accent)", + action: duplicate, + } + : undefined, isAppearAuthor || isModerator ? { From 988d7cba06a1e30d16a23a5fe7fc3a8da487c8dc Mon Sep 17 00:00:00 2001 From: Kaity A Date: Sat, 13 May 2023 23:57:55 +1000 Subject: [PATCH 06/10] fix meta update --- .../backend/src/server/api/endpoints/admin/update-meta.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 586378d15..c5d534879 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -564,6 +564,10 @@ export default define(meta, paramDef, async (ps, me) => { set.enableActiveEmailValidation = ps.enableActiveEmailValidation; } + if (ps.experimentalFeatures !== undefined) { + set.experimentalFeatures = ps.experimentalFeatures || undefined; + } + await db.transaction(async (transactionalEntityManager) => { const metas = await transactionalEntityManager.find(Meta, { order: { From ab4b798db521b03438c9362ba710c44f38c9f0bf Mon Sep 17 00:00:00 2001 From: Kaity A Date: Sun, 14 May 2023 00:04:24 +1000 Subject: [PATCH 07/10] Add page header to experiments --- .../client/src/pages/admin/experiments.vue | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/packages/client/src/pages/admin/experiments.vue b/packages/client/src/pages/admin/experiments.vue index 2f04b88ab..52ea4d8c3 100644 --- a/packages/client/src/pages/admin/experiments.vue +++ b/packages/client/src/pages/admin/experiments.vue @@ -1,23 +1,31 @@ + + + - - - + + + + + @@ -59,6 +67,10 @@ function save() { }); } +const headerActions = $computed(() => []); + +const headerTabs = $computed(() => []); + definePageMetadata({ title: i18n.ts._experiments.title, icon: "ph-flask ph-bold ph-lg", From d974562b73e56f630c3edf36aa7a7032887df26b Mon Sep 17 00:00:00 2001 From: Kaity A Date: Sun, 14 May 2023 00:18:30 +1000 Subject: [PATCH 08/10] Update meta to return experimentals --- packages/backend/src/server/api/endpoints/admin/meta.ts | 1 + packages/backend/src/server/api/endpoints/meta.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index b83579cd3..803021152 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -591,5 +591,6 @@ export default define(meta, paramDef, async (ps, me) => { libreTranslateApiKey: instance.libreTranslateApiKey, enableIpLogging: instance.enableIpLogging, enableActiveEmailValidation: instance.enableActiveEmailValidation, + experimentalFeatures: instance.experimentalFeatures, }; }); diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 23989750f..64f25e2ba 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -525,6 +525,8 @@ export default define(meta, paramDef, async (ps, me) => { serviceWorker: instance.enableServiceWorker, miauth: true, }; + + response.experimentalFeatures = instance.experimentalFeatures; } return response; From 01329d3dad1d8d7f702db8cd2070bfb691c91673 Mon Sep 17 00:00:00 2001 From: Kaity A Date: Sun, 14 May 2023 00:51:31 +1000 Subject: [PATCH 09/10] Update meta to include feaures --- packages/backend/src/server/api/endpoints/meta.ts | 3 +-- packages/calckey-js/src/entities.ts | 1 - packages/client/src/instance.ts | 4 ++-- packages/client/src/scripts/get-note-menu.ts | 4 ++-- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 64f25e2ba..2f409eb19 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -523,10 +523,9 @@ export default define(meta, paramDef, async (ps, me) => { github: instance.enableGithubIntegration, discord: instance.enableDiscordIntegration, serviceWorker: instance.enableServiceWorker, + postEditing: instance.experimentalFeatures?.postEditing || false, miauth: true, }; - - response.experimentalFeatures = instance.experimentalFeatures; } return response; diff --git a/packages/calckey-js/src/entities.ts b/packages/calckey-js/src/entities.ts index 7ecfba649..5a581a54c 100644 --- a/packages/calckey-js/src/entities.ts +++ b/packages/calckey-js/src/entities.ts @@ -304,7 +304,6 @@ export type LiteInstanceMetadata = { url: string; imageUrl: string; }[]; - experimentalFeatures?: Record; }; export type DetailedInstanceMetadata = LiteInstanceMetadata & { diff --git a/packages/client/src/instance.ts b/packages/client/src/instance.ts index ad83528e1..3381684a0 100644 --- a/packages/client/src/instance.ts +++ b/packages/client/src/instance.ts @@ -8,7 +8,7 @@ const instanceData = localStorage.getItem("instance"); // TODO: instanceをリアクティブにするかは再考の余地あり -export const instance: Misskey.entities.InstanceMetadata = reactive( +export const instance: Misskey.entities.DetailedInstanceMetadata = reactive( instanceData ? JSON.parse(instanceData) : { @@ -18,7 +18,7 @@ export const instance: Misskey.entities.InstanceMetadata = reactive( export async function fetchInstance() { const meta = await api("meta", { - detail: false, + detail: true, }); for (const [k, v] of Object.entries(meta)) { diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index 72147c1c6..1ec36d1ef 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -421,7 +421,7 @@ export function getNoteMenu(props: { null, - instance.experimentalFeatures?.postEditing && isAppearAuthor + instance.features.postEditing && isAppearAuthor ? { icon: "ph-pencil-line ph-bold ph-lg", text: i18n.ts.edit, @@ -430,7 +430,7 @@ export function getNoteMenu(props: { } : undefined, - instance.experimentalFeatures?.postEditing + instance.features.postEditing ? { icon: "ph-copy ph-bold ph-lg", text: i18n.ts.duplicate, From 46fb825ad57a06a56fc64f2cefeb56894854521a Mon Sep 17 00:00:00 2001 From: Kaity A Date: Sun, 14 May 2023 00:56:47 +1000 Subject: [PATCH 10/10] update translation --- locales/en-US.yml | 4 ++-- packages/client/src/pages/admin/experiments.vue | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index e7c25de71..cff6d0c7d 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -2013,5 +2013,5 @@ _experiments: alpha: "Alpha" beta: "Beta" release: "Release" - enableExperimentalPostEditing: "Enable experimental post editing" - experimentalPostEditingCaption: "Enables the option for users to edit their posts in post options" + enablePostEditing: "Enable post editing" + postEditingCaption: "Shows the option for users to edit their existing posts via the post options menu" diff --git a/packages/client/src/pages/admin/experiments.vue b/packages/client/src/pages/admin/experiments.vue index 52ea4d8c3..f540cea9d 100644 --- a/packages/client/src/pages/admin/experiments.vue +++ b/packages/client/src/pages/admin/experiments.vue @@ -9,19 +9,19 @@ @@ -39,7 +39,7 @@ import { fetchInstance } from "@/instance"; import { i18n } from "@/i18n"; import { definePageMetadata } from "@/scripts/page-metadata"; -let enableExperimentalPostEditing = $ref(false); +let enablePostEditing = $ref(false); let meta = $ref(null); type MetaExperiments = { @@ -52,14 +52,13 @@ async function init() { meta = (await os.api("admin/meta")) as MetaExperiments; if (!meta) return; - enableExperimentalPostEditing = - meta.experimentalFeatures?.postEditing ?? false; + enablePostEditing = meta.experimentalFeatures?.postEditing ?? false; } function save() { const experiments: MetaExperiments = { experimentalFeatures: { - postEditing: enableExperimentalPostEditing, + postEditing: enablePostEditing, }, }; os.apiWithDialog("admin/update-meta", experiments).then(() => {