From eb740e2c72ae6854b244ad099c927c069008720e Mon Sep 17 00:00:00 2001
From: syuilo
Date: Thu, 28 Sep 2023 11:41:41 +0900
Subject: enhance: タイムラインからRenoteを除外するオプションを追加
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/server/api/endpoints/notes/user-list-timeline.ts | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
(limited to 'packages/backend/src/server/api/endpoints/notes')
diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
index 6932073791..c20274b2ba 100644
--- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -49,6 +49,8 @@ export const paramDef = {
includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true },
+ withReplies: { type: 'boolean', default: false },
+ withRenotes: { type: 'boolean', default: true },
withFiles: {
type: 'boolean',
default: false,
@@ -130,6 +132,20 @@ export default class extends Endpoint { // eslint-
}));
}
+ if (!ps.withReplies) {
+ query.andWhere('note.replyId IS NULL');
+ }
+
+ if (ps.withRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere(new Brackets(qb => {
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ }));
+ }));
+ }
+
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
--
cgit v1.2.3-freya
From 772d2432b6e84a7a7c0fa8ad1852701cdc600f88 Mon Sep 17 00:00:00 2001
From: syuilo
Date: Thu, 28 Sep 2023 15:32:47 +0900
Subject: enhance: タイムラインからRenoteを除外するオプションを追加
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../server/api/endpoints/notes/global-timeline.ts | 12 ++++++++
.../server/api/endpoints/notes/hybrid-timeline.ts | 11 ++++++++
.../server/api/endpoints/notes/local-timeline.ts | 11 ++++++++
.../src/server/api/endpoints/notes/timeline.ts | 11 ++++++++
packages/frontend/src/ui/deck/deck-store.ts | 2 ++
packages/frontend/src/ui/deck/tl-column.vue | 33 ++++++++++++++++++++--
6 files changed, 78 insertions(+), 2 deletions(-)
(limited to 'packages/backend/src/server/api/endpoints/notes')
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
index 0b3b5c902e..8784e86153 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -4,6 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import { Brackets } from 'typeorm';
import type { NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
@@ -40,6 +41,7 @@ export const paramDef = {
properties: {
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
+ withRenotes: { type: 'boolean', default: true },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
@@ -88,6 +90,16 @@ export default class extends Endpoint { // eslint-
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
+
+ if (ps.withRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere(new Brackets(qb => {
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ }));
+ }));
+ }
//#endregion
const timeline = await query.limit(ps.limit).getMany();
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index e9ae5dc755..9bde5dee21 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -52,6 +52,7 @@ export const paramDef = {
includeLocalRenotes: { type: 'boolean', default: true },
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
+ withRenotes: { type: 'boolean', default: true },
},
required: [],
} as const;
@@ -137,6 +138,16 @@ export default class extends Endpoint { // eslint-
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
+
+ if (ps.withRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere(new Brackets(qb => {
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ }));
+ }));
+ }
//#endregion
const timeline = await query.limit(ps.limit).getMany();
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
index af1e0398dc..0fefddc51b 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -42,6 +42,7 @@ export const paramDef = {
properties: {
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
+ withRenotes: { type: 'boolean', default: true },
fileType: { type: 'array', items: {
type: 'string',
} },
@@ -110,6 +111,16 @@ export default class extends Endpoint { // eslint-
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
}
}
+
+ if (ps.withRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere(new Brackets(qb => {
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ }));
+ }));
+ }
//#endregion
const timeline = await query.limit(ps.limit).getMany();
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index 042115ab84..0d47cc1702 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -42,6 +42,7 @@ export const paramDef = {
includeLocalRenotes: { type: 'boolean', default: true },
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
+ withRenotes: { type: 'boolean', default: true },
},
required: [],
} as const;
@@ -126,6 +127,16 @@ export default class extends Endpoint { // eslint-
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
+
+ if (ps.withRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere(new Brackets(qb => {
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ }));
+ }));
+ }
//#endregion
const timeline = await query.limit(ps.limit).getMany();
diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts
index f910c5181d..034d675b0e 100644
--- a/packages/frontend/src/ui/deck/deck-store.ts
+++ b/packages/frontend/src/ui/deck/deck-store.ts
@@ -30,6 +30,8 @@ export type Column = {
roleId?: string;
includingTypes?: typeof notificationTypes[number][];
tl?: 'home' | 'local' | 'social' | 'global';
+ withRenotes?: boolean;
+ withReplies?: boolean;
};
export const deckStore = markRaw(new Storage('deck', {
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index 813b801d21..073898409c 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -20,12 +20,19 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._disabledTimeline.description }}
-
+
--
cgit v1.2.3-freya
From c106db89e1d54c20c6466e42dde540e0d5c5c4eb Mon Sep 17 00:00:00 2001
From: syuilo
Date: Thu, 28 Sep 2023 17:21:16 +0900
Subject: feat: note edit
---
CHANGELOG.md | 2 +
locales/index.d.ts | 1 +
locales/ja-JP.yml | 1 +
packages/backend/src/core/RoleService.ts | 3 +
packages/backend/src/server/api/EndpointsModule.ts | 4 +
packages/backend/src/server/api/endpoints.ts | 2 +
.../src/server/api/endpoints/notes/update.ts | 88 ++++++++++++++++++++++
packages/backend/src/server/api/stream/types.ts | 4 +
packages/frontend/src/components/MkPostForm.vue | 8 +-
.../frontend/src/components/MkPostFormDialog.vue | 1 +
packages/frontend/src/const.ts | 1 +
packages/frontend/src/pages/admin/roles.editor.vue | 20 +++++
packages/frontend/src/pages/admin/roles.vue | 8 ++
packages/frontend/src/scripts/get-note-menu.ts | 9 +++
packages/frontend/src/scripts/use-note-capture.ts | 6 ++
packages/misskey-js/src/streaming.types.ts | 7 ++
16 files changed, 162 insertions(+), 3 deletions(-)
create mode 100644 packages/backend/src/server/api/endpoints/notes/update.ts
(limited to 'packages/backend/src/server/api/endpoints/notes')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c711572655..d24364c57c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,8 @@
## next
### General
+- Feat: ノートの編集をできるように
+ - ロールで編集可否を設定可能
- Enhance: タイムラインからRenoteを除外するオプションを追加
- Enhance: ユーザーページのノート一覧でRenoteを除外できるように
diff --git a/locales/index.d.ts b/locales/index.d.ts
index eb2793c710..8c6b724623 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -1538,6 +1538,7 @@ export interface Locale {
"gtlAvailable": string;
"ltlAvailable": string;
"canPublicNote": string;
+ "canEditNote": string;
"canInvite": string;
"inviteLimit": string;
"inviteLimitCycle": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 637d580d6a..c31b4a5c27 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1459,6 +1459,7 @@ _role:
gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可"
+ canEditNote: "ノートの編集"
canInvite: "サーバー招待コードの発行"
inviteLimit: "招待コードの作成可能数"
inviteLimitCycle: "招待コードの発行間隔"
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 934b7d676b..ec4d804219 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -26,6 +26,7 @@ export type RolePolicies = {
gtlAvailable: boolean;
ltlAvailable: boolean;
canPublicNote: boolean;
+ canEditNote: boolean;
canInvite: boolean;
inviteLimit: number;
inviteLimitCycle: number;
@@ -50,6 +51,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
gtlAvailable: true,
ltlAvailable: true,
canPublicNote: true,
+ canEditNote: true,
canInvite: false,
inviteLimit: 0,
inviteLimitCycle: 60 * 24 * 7,
@@ -294,6 +296,7 @@ export class RoleService implements OnApplicationShutdown {
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
+ canEditNote: calc('canEditNote', vs => vs.some(v => v === true)),
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 41a11bfb19..c883c96ba2 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -258,6 +258,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_update from './endpoints/notes/update.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';
@@ -606,6 +607,7 @@ const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes
const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default };
const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default };
const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default };
+const $notes_update: Provider = { provide: 'ep:notes/update', useClass: ep___notes_update.default };
const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default };
const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
@@ -958,6 +960,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_conversation,
$notes_create,
$notes_delete,
+ $notes_update,
$notes_favorites_create,
$notes_favorites_delete,
$notes_featured,
@@ -1304,6 +1307,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_conversation,
$notes_create,
$notes_delete,
+ $notes_update,
$notes_favorites_create,
$notes_favorites_delete,
$notes_featured,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index ab20a708ef..b40d654f9c 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -258,6 +258,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_update from './endpoints/notes/update.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';
@@ -604,6 +605,7 @@ const eps = [
['notes/conversation', ep___notes_conversation],
['notes/create', ep___notes_create],
['notes/delete', ep___notes_delete],
+ ['notes/update', ep___notes_update],
['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/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts
new file mode 100644
index 0000000000..ccd2878d3c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/update.ts
@@ -0,0 +1,88 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import ms from 'ms';
+import { Inject, Injectable } from '@nestjs/common';
+import type { UsersRepository, NotesRepository } from '@/models/_.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { NoteDeleteService } from '@/core/NoteDeleteService.js';
+import { DI } from '@/di-symbols.js';
+import { GetterService } from '@/server/api/GetterService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+ tags: ['notes'],
+
+ requireCredential: true,
+ requireRolePolicy: 'canEditNote',
+
+ kind: 'write:notes',
+
+ limit: {
+ duration: ms('1hour'),
+ max: 10,
+ minInterval: ms('1sec'),
+ },
+
+ errors: {
+ noSuchNote: {
+ message: 'No such note.',
+ code: 'NO_SUCH_NOTE',
+ id: 'a6584e14-6e01-4ad3-b566-851e7bf0d474',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ noteId: { type: 'string', format: 'misskey:id' },
+ text: {
+ type: 'string',
+ minLength: 1,
+ maxLength: MAX_NOTE_TEXT_LENGTH,
+ nullable: false,
+ },
+ cw: { type: 'string', nullable: true, maxLength: 100 },
+ },
+ required: ['noteId', 'text', 'cw'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
+
+ private getterService: GetterService,
+ private globalEventService: GlobalEventService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const note = await this.getterService.getNote(ps.noteId).catch(err => {
+ if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+ throw err;
+ });
+
+ if (note.userId !== me.id) {
+ throw new ApiError(meta.errors.noSuchNote);
+ }
+
+ await this.notesRepository.update({ id: note.id }, {
+ cw: ps.cw,
+ text: ps.text,
+ });
+
+ this.globalEventService.publishNoteStream(note.id, 'updated', {
+ cw: ps.cw,
+ text: ps.text,
+ });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts
index 90e0a61f26..2436750cd6 100644
--- a/packages/backend/src/server/api/stream/types.ts
+++ b/packages/backend/src/server/api/stream/types.ts
@@ -130,6 +130,10 @@ export interface NoteStreamTypes {
deleted: {
deletedAt: Date;
};
+ updated: {
+ cw: string | null;
+ text: string;
+ };
reacted: {
reaction: string;
emoji?: {
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 1f4f75d5ed..b82ca3ef19 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -143,6 +143,7 @@ const props = withDefaults(defineProps<{
fixed?: boolean;
autofocus?: boolean;
freezeAfterPosted?: boolean;
+ updateMode?: boolean;
}>(), {
initialVisibleUsers: () => [],
autofocus: true,
@@ -698,17 +699,18 @@ async function post(ev?: MouseEvent) {
}
let postData = {
- text: text === '' ? undefined : text,
+ text: text === '' ? null : text,
fileIds: files.length > 0 ? files.map(f => f.id) : undefined,
replyId: props.reply ? props.reply.id : undefined,
renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
channelId: props.channel ? props.channel.id : undefined,
poll: poll,
- cw: useCw ? cw ?? '' : undefined,
+ cw: useCw ? cw ?? '' : null,
localOnly: localOnly,
visibility: visibility,
visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined,
reactionAcceptance,
+ noteId: props.updateMode ? props.initialNote?.id : undefined,
};
if (withHashtags && hashtags && hashtags.trim() !== '') {
@@ -731,7 +733,7 @@ async function post(ev?: MouseEvent) {
}
posting = true;
- os.api('notes/create', postData, token).then(() => {
+ os.api(props.updateMode ? 'notes/update' : 'notes/create', postData, token).then(() => {
if (props.freezeAfterPosted) {
posted = true;
} else {
diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue
index c07a166a83..f33d498f93 100644
--- a/packages/frontend/src/components/MkPostFormDialog.vue
+++ b/packages/frontend/src/components/MkPostFormDialog.vue
@@ -30,6 +30,7 @@ const props = defineProps<{
instant?: boolean;
fixed?: boolean;
autofocus?: boolean;
+ updateMode?: boolean;
}>();
const emit = defineEmits<{
diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts
index 15038b1063..9fd6d40d72 100644
--- a/packages/frontend/src/const.ts
+++ b/packages/frontend/src/const.ts
@@ -61,6 +61,7 @@ export const ROLE_POLICIES = [
'gtlAvailable',
'ltlAvailable',
'canPublicNote',
+ 'canEditNote',
'canInvite',
'inviteLimit',
'inviteLimitCycle',
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index 2ef3e254cd..1b72e1d332 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -160,6 +160,26 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ {{ i18n.ts._role._options.canEditNote }}
+
+ {{ i18n.ts._role.useBaseValue }}
+ {{ role.policies.canEditNote.value ? i18n.ts.yes : i18n.ts.no }}
+
+
+
+
+ {{ i18n.ts._role.useBaseValue }}
+
+
+ {{ i18n.ts.enable }}
+
+
+ {{ i18n.ts._role.priority }}
+
+
+
+
{{ i18n.ts._role._options.canInvite }}
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index 8d23335430..e1306d04b9 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -48,6 +48,14 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ {{ i18n.ts._role._options.canEditNote }}
+ {{ policies.canEditNote ? i18n.ts.yes : i18n.ts.no }}
+
+ {{ i18n.ts.enable }}
+
+
+
{{ i18n.ts._role._options.canInvite }}
{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 0948741fc5..45fb622069 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -172,6 +172,10 @@ export function getNoteMenu(props: {
});
}
+ function edit(): void {
+ os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel, updateMode: true });
+ }
+
function toggleFavorite(favorite: boolean): void {
claimAchievement('noteFavorited1');
os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
@@ -352,6 +356,11 @@ export function getNoteMenu(props: {
),
...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
null,
+ appearNote.userId === $i.id && $i.policies.canEditNote ? {
+ icon: 'ti ti-edit',
+ text: i18n.ts.edit,
+ action: edit,
+ } : undefined,
appearNote.userId === $i.id ? {
icon: 'ti ti-edit',
text: i18n.ts.deleteAndEdit,
diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts
index c618532570..e815e74444 100644
--- a/packages/frontend/src/scripts/use-note-capture.ts
+++ b/packages/frontend/src/scripts/use-note-capture.ts
@@ -71,6 +71,12 @@ export function useNoteCapture(props: {
break;
}
+ case 'updated': {
+ note.value.cw = body.cw;
+ note.value.text = body.text;
+ break;
+ }
+
case 'deleted': {
props.isDeletedRef.value = true;
break;
diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts
index 96ac7787e1..ce29a00032 100644
--- a/packages/misskey-js/src/streaming.types.ts
+++ b/packages/misskey-js/src/streaming.types.ts
@@ -133,6 +133,13 @@ export type NoteUpdatedEvent = {
body: {
deletedAt: string;
};
+} | {
+ id: Note['id'];
+ type: 'updated';
+ body: {
+ cw: string | null;
+ text: string;
+ };
} | {
id: Note['id'];
type: 'pollVoted';
--
cgit v1.2.3-freya
From 2438c047a71b7b46d3cc7304a1c78a27a6789f02 Mon Sep 17 00:00:00 2001
From: syuilo
Date: Thu, 28 Sep 2023 21:06:14 +0900
Subject: enhance: 編集されたノートかどうか分かるように
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
locales/index.d.ts | 1 +
locales/ja-JP.yml | 1 +
packages/backend/migration/1695901659683-note-updated-at.js | 11 +++++++++++
packages/backend/src/core/entities/NoteEntityService.ts | 1 +
packages/backend/src/models/Note.ts | 5 +++++
packages/backend/src/models/json-schema/note.ts | 7 ++++++-
packages/backend/src/server/api/endpoints/notes/update.ts | 1 +
packages/frontend/src/components/MkNoteDetailed.vue | 3 +++
packages/frontend/src/components/MkNoteHeader.vue | 1 +
packages/frontend/src/scripts/use-note-capture.ts | 1 +
packages/misskey-js/etc/misskey-js.api.md | 3 ++-
packages/misskey-js/src/entities.ts | 1 +
12 files changed, 34 insertions(+), 2 deletions(-)
create mode 100644 packages/backend/migration/1695901659683-note-updated-at.js
(limited to 'packages/backend/src/server/api/endpoints/notes')
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 8c6b724623..75f867e691 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -1125,6 +1125,7 @@ export interface Locale {
"authenticationRequiredToContinue": string;
"dateAndTime": string;
"showRenotes": string;
+ "edited": string;
"_announcement": {
"forExistingUsers": string;
"forExistingUsersDescription": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index c31b4a5c27..cfa9143dde 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1122,6 +1122,7 @@ authentication: "認証"
authenticationRequiredToContinue: "続けるには認証を行ってください"
dateAndTime: "日時"
showRenotes: "リノートを表示"
+edited: "編集済み"
_announcement:
forExistingUsers: "既存ユーザーのみ"
diff --git a/packages/backend/migration/1695901659683-note-updated-at.js b/packages/backend/migration/1695901659683-note-updated-at.js
new file mode 100644
index 0000000000..d8a151a1f7
--- /dev/null
+++ b/packages/backend/migration/1695901659683-note-updated-at.js
@@ -0,0 +1,11 @@
+export class NoteUpdatedAt1695901659683 {
+ name = 'NoteUpdatedAt1695901659683'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`);
+ }
+}
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index bf42e98ce0..a024286b48 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -308,6 +308,7 @@ export class NoteEntityService implements OnModuleInit {
const packed: Packed<'Note'> = await awaitAll({
id: note.id,
createdAt: note.createdAt.toISOString(),
+ updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
userId: note.userId,
user: this.userEntityService.pack(note.user ?? note.userId, me, {
detail: false,
diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts
index ed86d4549e..f396a0cd7a 100644
--- a/packages/backend/src/models/Note.ts
+++ b/packages/backend/src/models/Note.ts
@@ -24,6 +24,11 @@ export class MiNote {
})
public createdAt: Date;
+ @Column('timestamp with time zone', {
+ default: null,
+ })
+ public updatedAt: Date | null;
+
@Index()
@Column({
...id(),
diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts
index eb744aa109..ad0cb3c45d 100644
--- a/packages/backend/src/models/json-schema/note.ts
+++ b/packages/backend/src/models/json-schema/note.ts
@@ -17,6 +17,11 @@ export const packedNoteSchema = {
optional: false, nullable: false,
format: 'date-time',
},
+ updatedAt: {
+ type: 'string',
+ optional: true, nullable: true,
+ format: 'date-time',
+ },
deletedAt: {
type: 'string',
optional: true, nullable: true,
@@ -142,7 +147,7 @@ export const packedNoteSchema = {
isSensitive: {
type: 'boolean',
optional: true, nullable: false,
- }
+ },
},
},
},
diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts
index ccd2878d3c..cdf7f085e0 100644
--- a/packages/backend/src/server/api/endpoints/notes/update.ts
+++ b/packages/backend/src/server/api/endpoints/notes/update.ts
@@ -75,6 +75,7 @@ export default class extends Endpoint { // eslint-
}
await this.notesRepository.update({ id: note.id }, {
+ updatedAt: new Date(),
cw: ps.cw,
text: ps.text,
});
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 06663d0477..ab8886e8ba 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -93,6 +93,9 @@ SPDX-License-Identifier: AGPL-3.0-only