Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### General
- Feat: ジョブキュー管理画面からキューの一時停止/再開ができるように
- Feat: アンテナのタイムラインから個別のノートを削除できるように

### Client
-
Expand Down
2 changes: 2 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,8 @@ optional: "任意"
createNewClip: "新しいクリップを作成"
unclip: "クリップ解除"
confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれています。ノートをこのクリップから除外しますか?"
removeFromAntenna: "このアンテナから削除"
removeNoteFromAntennaConfirm: "「{name}」からこのノートを削除しますか?"
public: "パブリック"
private: "非公開"
i18nInfo: "Misskeyは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。"
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/core/FanoutTimelineService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,9 @@ export class FanoutTimelineService {
public purge(name: FanoutTimelineName) {
return this.redisForTimelines.del('list:' + name);
}

@bindThis
public remove(name: FanoutTimelineName, id: string) {
return this.redisForTimelines.lrem('list:' + name, 1, id);
}
}
1 change: 1 addition & 0 deletions packages/backend/src/server/api/endpoint-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export * as 'antennas/create' from './endpoints/antennas/create.js';
export * as 'antennas/delete' from './endpoints/antennas/delete.js';
export * as 'antennas/list' from './endpoints/antennas/list.js';
export * as 'antennas/notes' from './endpoints/antennas/notes.js';
export * as 'antennas/remove-note' from './endpoints/antennas/remove-note.js';
export * as 'antennas/show' from './endpoints/antennas/show.js';
export * as 'antennas/update' from './endpoints/antennas/update.js';
export * as 'ap/get' from './endpoints/ap/get.js';
Expand Down
61 changes: 61 additions & 0 deletions packages/backend/src/server/api/endpoints/antennas/remove-note.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AntennasRepository } from '@/models/_.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';

export const meta = {
tags: ['antennas', 'account', 'notes'],

requireCredential: true,

prohibitMoved: true,

kind: 'write:account',

errors: {
noSuchAntenna: {
message: 'No such antenna.',
code: 'NO_SUCH_ANTENNA',
id: '850926e0-fd3b-49b6-b69a-b28a5dbd82fe',
},
},
} as const;

export const paramDef = {
type: 'object',
properties: {
antennaId: { type: 'string', format: 'misskey:id' },
noteId: { type: 'string', format: 'misskey:id' },
},
required: ['antennaId', 'noteId'],
} as const;

@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,

private fanoutTimelineService: FanoutTimelineService,
) {
super(meta, paramDef, async (ps, me) => {
const antenna = await this.antennasRepository.findOneBy({
id: ps.antennaId,
userId: me.id,
});

if (antenna == null) {
throw new ApiError(meta.errors.noSuchAntenna);
}

await this.fanoutTimelineService.remove(`antennaTimeline:${antenna.id}`, ps.noteId);
});
}
}
75 changes: 75 additions & 0 deletions packages/backend/test/e2e/antennas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,81 @@ describe('アンテナ', () => {
assert.deepStrictEqual(response, expected);
});

test('から指定したノートだけ削除でき、ノート本体や他人のアンテナには影響しないこと。', async () => {
const keyword = 'キーワード';
const antenna = await successfulApiCall({
endpoint: 'antennas/create',
parameters: { ...defaultParam, keywords: [[keyword]] },
user: alice,
});
const otherAntenna = await successfulApiCall({
endpoint: 'antennas/create',
parameters: { ...defaultParam, keywords: [[keyword]] },
user: bob,
});
const remainingNote = await post(bob, { text: `test ${keyword} remaining` });
const removedNote = await post(bob, { text: `test ${keyword} removed` });

await successfulApiCall({
endpoint: 'antennas/remove-note',
parameters: { antennaId: antenna.id, noteId: removedNote.id },
user: alice,
});

const response = await successfulApiCall({
endpoint: 'antennas/notes',
parameters: { antennaId: antenna.id },
user: alice,
});
assert.deepStrictEqual(response, [remainingNote]);

const note = await successfulApiCall({
endpoint: 'notes/show',
parameters: { noteId: removedNote.id },
user: alice,
});
assert.deepStrictEqual(note, removedNote);

const otherResponse = await successfulApiCall({
endpoint: 'antennas/notes',
parameters: { antennaId: otherAntenna.id },
user: bob,
});
assert.deepStrictEqual(otherResponse, [removedNote, remainingNote]);
});

test('から存在しないノートを削除しても成功すること。', async () => {
const antenna = await successfulApiCall({
endpoint: 'antennas/create',
parameters: defaultParam,
user: alice,
});

await successfulApiCall({
endpoint: 'antennas/remove-note',
parameters: { antennaId: antenna.id, noteId: 'doesnotexist' },
user: alice,
});
});

test('から他人のアンテナを指定してノートを削除できないこと。', async () => {
const antenna = await successfulApiCall({
endpoint: 'antennas/create',
parameters: defaultParam,
user: alice,
});

await failedApiCall({
endpoint: 'antennas/remove-note',
parameters: { antennaId: antenna.id, noteId: alicePost.id },
user: bob,
}, {
status: 400,
code: 'NO_SUCH_ANTENNA',
id: '850926e0-fd3b-49b6-b69a-b28a5dbd82fe',
});
});

const keyword = 'キーワード';
test.each([
{
Expand Down
5 changes: 3 additions & 2 deletions packages/frontend/src/components/MkNote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ const inTimeline = inject<boolean>('inTimeline', false);
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
const inChannel = inject(DI.inChannel, null);
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
const currentAntenna = inject<Ref<Misskey.entities.Antenna | null> | null>('currentAntenna', null);

let note = deepClone(props.note);

Expand Down Expand Up @@ -606,7 +607,7 @@ function onContextmenu(ev: PointerEvent): void {
ev.preventDefault();
react();
} else {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, currentClip: currentClip?.value });
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, currentClip: currentClip?.value, currentAntenna: currentAntenna?.value ?? undefined });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
Expand All @@ -616,7 +617,7 @@ function showMenu(): void {
return;
}

const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, currentClip: currentClip?.value });
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, currentClip: currentClip?.value, currentAntenna: currentAntenna?.value ?? undefined });
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
}

Expand Down
6 changes: 6 additions & 0 deletions packages/frontend/src/components/MkStreamingNotesTimeline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@ useGlobalEvent('noteDeleted', (noteId) => {
paginator.removeItem(noteId);
});

useGlobalEvent('noteRemovedFromAntenna', (antennaId, noteId) => {
if (props.src === 'antenna' && props.antenna === antennaId) {
paginator.removeItem(noteId);
}
});

function releaseQueue() {
paginator.releaseQueue();
scrollToTop(rootEl.value!);
Expand Down
1 change: 1 addition & 0 deletions packages/frontend/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Events = {
clientNotification: (notification: Misskey.entities.Notification) => void;
notePosted: (note: Misskey.entities.Note) => void;
noteDeleted: (noteId: Misskey.entities.Note['id']) => void;
noteRemovedFromAntenna: (antennaId: Misskey.entities.Antenna['id'], noteId: Misskey.entities.Note['id']) => void;
driveFileCreated: (file: Misskey.entities.DriveFile) => void;
driveFilesUpdated: (files: Misskey.entities.DriveFile[]) => void;
driveFilesDeleted: (files: Misskey.entities.DriveFile[]) => void;
Expand Down
4 changes: 3 additions & 1 deletion packages/frontend/src/pages/antenna-timeline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>

<script lang="ts" setup>
import { computed, watch, ref, useTemplateRef } from 'vue';
import { computed, watch, ref, useTemplateRef, provide } from 'vue';
import * as Misskey from 'misskey-js';
import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue';
import * as os from '@/os.js';
Expand All @@ -37,6 +37,8 @@ const props = defineProps<{
const antenna = ref<Misskey.entities.Antenna | null>(null);
const tlEl = useTemplateRef('tlEl');

provide('currentAntenna', antenna);

function settings() {
router.push('/my/antennas/:antennaId', {
params: {
Expand Down
30 changes: 30 additions & 0 deletions packages/frontend/src/utility/get-note-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export function getNoteMenu(props: {
translation: Ref<Misskey.entities.NotesTranslateResponse | null>;
translating: Ref<boolean>;
currentClip?: Misskey.entities.Clip;
currentAntenna?: Misskey.entities.Antenna;
}) {
const appearNote = getAppearNote(props.note) ?? props.note;
const link = appearNote.url ?? appearNote.uri;
Expand Down Expand Up @@ -262,6 +263,19 @@ export function getNoteMenu(props: {
os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id });
}

async function removeFromAntenna(): Promise<void> {
if (!props.currentAntenna) return;

const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.removeNoteFromAntennaConfirm({ name: props.currentAntenna.name }),
});
if (canceled) return;

await os.apiWithDialog('antennas/remove-note', { antennaId: props.currentAntenna.id, noteId: appearNote.id });
globalEvents.emit('noteRemovedFromAntenna', props.currentAntenna.id, appearNote.id);
}

async function _promote(): Promise<void> {
const { canceled, result: days } = await os.inputNumber({
title: i18n.ts.numberOfDays,
Expand Down Expand Up @@ -502,12 +516,28 @@ export function getNoteMenu(props: {
action: delEdit,
});
}
if (props.currentAntenna != null) {
menuItems.push({
icon: 'ti ti-trash',
text: i18n.ts.removeFromAntenna,
danger: true,
action: removeFromAntenna,
});
}
menuItems.push({
icon: 'ti ti-trash',
text: i18n.ts.delete,
danger: true,
action: del,
});
} else if (props.currentAntenna != null) {
menuItems.push({ type: 'divider' });
menuItems.push({
icon: 'ti ti-trash',
text: i18n.ts.removeFromAntenna,
danger: true,
action: removeFromAntenna,
});
}
} else {
menuItems.push({
Expand Down
8 changes: 8 additions & 0 deletions packages/i18n/src/autogen/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3024,6 +3024,14 @@ export interface Locale extends ILocale {
* このノートはすでにクリップ「{name}」に含まれています。ノートをこのクリップから除外しますか?
*/
"confirmToUnclipAlreadyClippedNote": ParameterizedString<"name">;
/**
* このアンテナから削除
*/
"removeFromAntenna": string;
/**
* 「{name}」からこのノートを削除しますか?
*/
"removeNoteFromAntennaConfirm": ParameterizedString<"name">;
/**
* パブリック
*/
Expand Down
4 changes: 4 additions & 0 deletions packages/misskey-js/etc/misskey-js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,9 @@ type AntennasNotesRequest = operations['antennas___notes']['requestBody']['conte
// @public (undocumented)
type AntennasNotesResponse = operations['antennas___notes']['responses']['200']['content']['application/json'];

// @public (undocumented)
type AntennasRemoveNoteRequest = operations['antennas___remove-note']['requestBody']['content']['application/json'];

// @public (undocumented)
type AntennasShowRequest = operations['antennas___show']['requestBody']['content']['application/json'];

Expand Down Expand Up @@ -1679,6 +1682,7 @@ declare namespace entities {
AntennasListResponse,
AntennasNotesRequest,
AntennasNotesResponse,
AntennasRemoveNoteRequest,
AntennasShowRequest,
AntennasShowResponse,
AntennasUpdateRequest,
Expand Down
11 changes: 11 additions & 0 deletions packages/misskey-js/src/autogen/apiClientJSDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;

/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:account*
*/
request<E extends 'antennas/remove-note', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;

/**
* No description provided.
*
Expand Down
2 changes: 2 additions & 0 deletions packages/misskey-js/src/autogen/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ import type {
AntennasListResponse,
AntennasNotesRequest,
AntennasNotesResponse,
AntennasRemoveNoteRequest,
AntennasShowRequest,
AntennasShowResponse,
AntennasUpdateRequest,
Expand Down Expand Up @@ -773,6 +774,7 @@ export type Endpoints = {
'antennas/delete': { req: AntennasDeleteRequest; res: EmptyResponse };
'antennas/list': { req: EmptyRequest; res: AntennasListResponse };
'antennas/notes': { req: AntennasNotesRequest; res: AntennasNotesResponse };
'antennas/remove-note': { req: AntennasRemoveNoteRequest; res: EmptyResponse };
'antennas/show': { req: AntennasShowRequest; res: AntennasShowResponse };
'antennas/update': { req: AntennasUpdateRequest; res: AntennasUpdateResponse };
'ap/get': { req: ApGetRequest; res: ApGetResponse };
Expand Down
1 change: 1 addition & 0 deletions packages/misskey-js/src/autogen/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export type AntennasDeleteRequest = operations['antennas___delete']['requestBody
export type AntennasListResponse = operations['antennas___list']['responses']['200']['content']['application/json'];
export type AntennasNotesRequest = operations['antennas___notes']['requestBody']['content']['application/json'];
export type AntennasNotesResponse = operations['antennas___notes']['responses']['200']['content']['application/json'];
export type AntennasRemoveNoteRequest = operations['antennas___remove-note']['requestBody']['content']['application/json'];
export type AntennasShowRequest = operations['antennas___show']['requestBody']['content']['application/json'];
export type AntennasShowResponse = operations['antennas___show']['responses']['200']['content']['application/json'];
export type AntennasUpdateRequest = operations['antennas___update']['requestBody']['content']['application/json'];
Expand Down
Loading
Loading