diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index fcd67f39ff..19e05e5c17 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -278,6 +278,7 @@ import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete import * as ep___notes_renotes from './endpoints/notes/renotes.js'; import * as ep___notes_replies from './endpoints/notes/replies.js'; import * as ep___notes_edit from './endpoints/notes/edit.js'; +import * as ep___notes_versions from './endpoints/notes/versions.js'; import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js'; import * as ep___notes_search from './endpoints/notes/search.js'; import * as ep___notes_show from './endpoints/notes/show.js'; @@ -646,6 +647,7 @@ const $notes_translate: Provider = { provide: 'ep:notes/translate', useClass: ep const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep___notes_unrenote.default }; const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default }; const $notes_edit: Provider = { provide: 'ep:notes/edit', useClass: ep___notes_edit.default }; +const $notes_versions: Provider = { provide: 'ep:notes/versions', useClass: ep___notes_versions.default }; const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default }; const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default }; const $notifications_testNotification: Provider = { provide: 'ep:notifications/test-notification', useClass: ep___notifications_testNotification.default }; @@ -1008,6 +1010,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $notes_unrenote, $notes_userListTimeline, $notes_edit, + $notes_versions, $notifications_create, $notifications_markAllAsRead, $notifications_testNotification, @@ -1364,6 +1367,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $notes_unrenote, $notes_userListTimeline, $notes_edit, + $notes_versions, $notifications_create, $notifications_markAllAsRead, $pagePush, diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index e2b98c34e7..2616cbb761 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UsersRepository } from '@/models/_.js'; +import type { NotesRepository, UsersRepository, NoteEditRepository } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; @@ -21,6 +21,9 @@ export class GetterService { @Inject(DI.notesRepository) private notesRepository: NotesRepository, + @Inject(DI.noteEditRepository) + private noteEditRepository: NoteEditRepository, + private userEntityService: UserEntityService, ) { } @@ -39,6 +42,18 @@ export class GetterService { return note; } + /** + * Get note for API processing + */ + @bindThis + public async getEdits(noteId: MiNote['id']) { + const edits = await this.noteEditRepository.findBy({ noteId: noteId }).catch(() => { + throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); + }); + + return edits; + } + /** * Get user for API processing */ diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index be5e66cab0..7d82d116db 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -288,6 +288,7 @@ import * as ep___notes_translate from './endpoints/notes/translate.js'; import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; import * as ep___notes_edit from './endpoints/notes/edit.js'; +import * as ep___notes_versions from './endpoints/notes/versions.js'; import * as ep___notifications_create from './endpoints/notifications/create.js'; import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js'; @@ -644,6 +645,7 @@ const eps = [ ['notes/unrenote', ep___notes_unrenote], ['notes/user-list-timeline', ep___notes_userListTimeline], ['notes/edit', ep___notes_edit], + ['notes/versions', ep___notes_versions], ['notifications/create', ep___notifications_create], ['notifications/mark-all-as-read', ep___notifications_markAllAsRead], ['notifications/test-notification', ep___notifications_testNotification], diff --git a/packages/backend/src/server/api/endpoints/notes/versions.ts b/packages/backend/src/server/api/endpoints/notes/versions.ts new file mode 100644 index 0000000000..9733d781a4 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/versions.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: false, + + res: { + type: 'object', + optional: false, nullable: false, + }, + + errors: { + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + noteId: { type: 'string', format: 'misskey:id' }, + }, + required: ['noteId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const edits = await this.getterService.getEdits(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + let editArray = []; + + for (const edit of edits) { + editArray.push({ + updatedAt: new Date(edit.updatedAt).toLocaleString('UTC', { hour: 'numeric', minute: 'numeric', second: 'numeric', year: 'numeric', month: 'short', day: 'numeric' }), + text: edit.text, + }); + } + + editArray = editArray.sort((a, b) => { return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); }); + + return editArray; + }); + } +} diff --git a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts index b0b5147a48..a8c45b98f7 100644 --- a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts +++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts @@ -3,7 +3,7 @@ import megalodon, { Entity, MegalodonInterface } from 'megalodon'; import querystring from 'querystring'; import { IsNull } from 'typeorm'; import multer from 'fastify-multer'; -import type { NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { Config } from '@/config.js'; @@ -31,6 +31,8 @@ export class MastodonApiServerService { private notesRepository: NotesRepository, @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + @Inject(DI.noteEditRepository) + private noteEditRepository: NoteEditRepository, @Inject(DI.config) private config: Config, private metaService: MetaService, @@ -754,7 +756,7 @@ export class MastodonApiServerService { //#endregion //#region Timelines - const TLEndpoint = new ApiTimelineMastodon(fastify, this.config, this.usersRepository, this.notesRepository, this.userEntityService); + const TLEndpoint = new ApiTimelineMastodon(fastify, this.config, this.usersRepository, this.notesRepository, this.noteEditRepository, this.userEntityService); // GET Endpoints TLEndpoint.getTL(); @@ -779,7 +781,7 @@ export class MastodonApiServerService { //#endregion //#region Status - const NoteEndpoint = new ApiStatusMastodon(fastify, this.config, this.usersRepository, this.notesRepository, this.userEntityService); + const NoteEndpoint = new ApiStatusMastodon(fastify, this.config, this.usersRepository, this.notesRepository, this.noteEditRepository, this.userEntityService); // GET Endpoints NoteEndpoint.getStatus(); diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts index 69b0ad93ff..cbd2550f92 100644 --- a/packages/backend/src/server/api/mastodon/converters.ts +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -7,7 +7,7 @@ import { parse } from 'mfm-js'; import { GetterService } from '../GetterService.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import type { MiUser } from '@/models/User.js'; -import type { NotesRepository, UsersRepository } from '@/models/_.js'; +import type { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; const CHAR_COLLECTION = '0123456789abcdefghijklmnopqrstuvwxyz'; @@ -39,11 +39,14 @@ export class MastoConverters { @Inject(DI.notesRepository) private notesRepository: NotesRepository, + + @Inject(DI.noteEditRepository) + private noteEditRepository: NoteEditRepository, private userEntityService: UserEntityService ) { this.MfmService = new MfmService(this.config); - this.GetterService = new GetterService(this.usersRepository, this.notesRepository, this.userEntityService); + this.GetterService = new GetterService(this.usersRepository, this.notesRepository, this.noteEditRepository, this.userEntityService); } private encode(u: MiUser, m: IMentionedRemoteUsers): MastodonEntity.Mention { diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index 46dce65081..2690a1036f 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -6,7 +6,7 @@ import { convertTimelinesArgsId, limitToInt } from './timeline.js'; import type { Entity } from 'megalodon'; import type { FastifyInstance } from 'fastify'; import type { Config } from '@/config.js'; -import { NotesRepository, UsersRepository } from '@/models/_.js'; +import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; function normalizeQuery(data: any) { @@ -18,9 +18,9 @@ export class ApiStatusMastodon { private fastify: FastifyInstance; private mastoconverter: MastoConverters; - constructor(fastify: FastifyInstance, config: Config, usersrepo: UsersRepository, notesrepo: NotesRepository, userentity: UserEntityService) { + constructor(fastify: FastifyInstance, config: Config, usersrepo: UsersRepository, notesrepo: NotesRepository, noteeditrepo: NoteEditRepository, userentity: UserEntityService) { this.fastify = fastify; - this.mastoconverter = new MastoConverters(config, usersrepo, notesrepo, userentity); + this.mastoconverter = new MastoConverters(config, usersrepo, notesrepo, noteeditrepo, userentity); } public async getStatus() { diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts index bb66a7707c..e4f510ea2b 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -4,7 +4,7 @@ import { getClient } from '../MastodonApiServerService.js'; import type { Entity } from 'megalodon'; import type { FastifyInstance } from 'fastify'; import type { Config } from '@/config.js'; -import { NotesRepository, UsersRepository } from '@/models/_.js'; +import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; export function limitToInt(q: ParsedUrlQuery) { @@ -43,9 +43,9 @@ export class ApiTimelineMastodon { private fastify: FastifyInstance; private mastoconverter: MastoConverters; - constructor(fastify: FastifyInstance, config: Config, usersRepository: UsersRepository, notesRepository: NotesRepository, userEntityService: UserEntityService) { + constructor(fastify: FastifyInstance, config: Config, usersRepository: UsersRepository, notesRepository: NotesRepository, noteEditRepository: NoteEditRepository, userEntityService: UserEntityService) { this.fastify = fastify; - this.mastoconverter = new MastoConverters(config, usersRepository, notesRepository, userEntityService); + this.mastoconverter = new MastoConverters(config, usersRepository, notesRepository, noteEditRepository, userEntityService); } public async getTL() { diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 42e9e79376..f2c24841fc 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
@@ -177,6 +177,7 @@ import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js'; +import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js'; import { useNoteCapture } from '@/scripts/use-note-capture.js'; import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; @@ -224,6 +225,7 @@ const isRenote = ( const el = shallowRef(); const menuButton = shallowRef(); +const menuVersionsButton = shallowRef(); const renoteButton = shallowRef(); const renoteTime = shallowRef(); const reactButton = shallowRef(); @@ -563,6 +565,13 @@ function menu(viaKeyboard = false): void { }).then(focus).finally(cleanup); } +async function menuVersions(viaKeyboard = false): Promise { + const { menu, cleanup } = await getNoteVersionsMenu({ note: note, menuVersionsButton }); + os.popupMenu(menu, menuVersionsButton.value, { + viaKeyboard, + }).then(focus).finally(cleanup); +} + async function clip() { os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); } diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 5f204fe3c4..638fb87313 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
@@ -232,6 +232,7 @@ import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js'; +import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js'; import { useNoteCapture } from '@/scripts/use-note-capture.js'; import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; @@ -273,6 +274,7 @@ const isRenote = ( const el = shallowRef(); const menuButton = shallowRef(); +const menuVersionsButton = shallowRef(); const renoteButton = shallowRef(); const renoteTime = shallowRef(); const reactButton = shallowRef(); @@ -612,6 +614,13 @@ function menu(viaKeyboard = false): void { }).then(focus).finally(cleanup); } +async function menuVersions(viaKeyboard = false): Promise { + const { menu, cleanup } = await getNoteVersionsMenu({ note: note, menuVersionsButton }); + os.popupMenu(menu, menuVersionsButton.value, { + viaKeyboard, + }).then(focus).finally(cleanup); +} + async function clip() { os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus); } diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index 1247c0fb6e..1b899933cc 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -30,15 +30,25 @@ SPDX-License-Identifier: AGPL-3.0-only