Merge branch 'develop' into fix-timelines-e2e

This commit is contained in:
tamaina 2025-07-31 14:51:26 +09:00
commit 457b53d5ea
22 changed files with 115 additions and 80 deletions

View File

@ -1,14 +1,16 @@
## Unreleased ## Unreleased
### General ### General
- - ノートを削除した際、関連するノートが同時に削除されないようになりました
- APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります
### Client ### Client
- Fix: 一部の設定検索結果が存在しないパスになる問題を修正 - Fix: 一部の設定検索結果が存在しないパスになる問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
### Server ### Server
- - Enhance: ノートの削除処理の効率化
- Enhance: 全体的なパフォーマンスの向上
## 2025.7.0 ## 2025.7.0

4
locales/index.d.ts vendored
View File

@ -2567,11 +2567,11 @@ export interface Locale extends ILocale {
*/ */
"serviceworkerInfo": string; "serviceworkerInfo": string;
/** /**
* 稿 *
*/ */
"deletedNote": string; "deletedNote": string;
/** /**
* 稿 *
*/ */
"invisibleNote": string; "invisibleNote": string;
/** /**

View File

@ -637,8 +637,8 @@ addRelay: "リレーの追加"
inboxUrl: "inboxのURL" inboxUrl: "inboxのURL"
addedRelays: "追加済みのリレー" addedRelays: "追加済みのリレー"
serviceworkerInfo: "プッシュ通知を行うには有効にする必要があります。" serviceworkerInfo: "プッシュ通知を行うには有効にする必要があります。"
deletedNote: "削除された投稿" deletedNote: "削除されたノート"
invisibleNote: "非公開の投稿" invisibleNote: "非公開のノート"
enableInfiniteScroll: "自動でもっと見る" enableInfiniteScroll: "自動でもっと見る"
visibility: "公開範囲" visibility: "公開範囲"
poll: "アンケート" poll: "アンケート"

View File

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class RemoveNoteConstraints1753868431598 {
name = 'RemoveNoteConstraints1753868431598'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_52ccc804d7c69037d558bac4c96"`);
await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_52ccc804d7c69037d558bac4c96" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
}

View File

@ -421,7 +421,7 @@ export class NoteCreateService implements OnApplicationShutdown {
emojis, emojis,
userId: user.id, userId: user.id,
localOnly: data.localOnly!, localOnly: data.localOnly!,
reactionAcceptance: data.reactionAcceptance, reactionAcceptance: data.reactionAcceptance ?? null,
visibility: data.visibility as any, visibility: data.visibility as any,
visibleUserIds: data.visibility === 'specified' visibleUserIds: data.visibility === 'specified'
? data.visibleUsers ? data.visibleUsers
@ -483,7 +483,11 @@ export class NoteCreateService implements OnApplicationShutdown {
await this.notesRepository.insert(insert); await this.notesRepository.insert(insert);
} }
return insert; return {
...insert,
reply: data.reply ?? null,
renote: data.renote ?? null,
};
} catch (e) { } catch (e) {
// duplicate key error // duplicate key error
if (isDuplicateKeyValueError(e)) { if (isDuplicateKeyValueError(e)) {

View File

@ -62,7 +62,6 @@ export class NoteDeleteService {
*/ */
async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) { async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) {
const deletedAt = new Date(); const deletedAt = new Date();
const cascadingNotes = await this.findCascadingNotes(note);
if (note.replyId) { if (note.replyId) {
await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1); await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1);
@ -90,15 +89,6 @@ export class NoteDeleteService {
this.deliverToConcerned(user, note, content); this.deliverToConcerned(user, note, content);
} }
// also deliver delete activity to cascaded notes
const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes
for (const cascadingNote of federatedLocalCascadingNotes) {
if (!cascadingNote.user) continue;
if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue;
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user));
this.deliverToConcerned(cascadingNote.user, cascadingNote, content);
}
//#endregion //#endregion
this.notesChart.update(note, false); this.notesChart.update(note, false);
@ -118,9 +108,6 @@ export class NoteDeleteService {
} }
} }
for (const cascadingNote of cascadingNotes) {
this.searchService.unindexNote(cascadingNote);
}
this.searchService.unindexNote(note); this.searchService.unindexNote(note);
await this.notesRepository.delete({ await this.notesRepository.delete({
@ -140,29 +127,6 @@ export class NoteDeleteService {
} }
} }
@bindThis
private async findCascadingNotes(note: MiNote): Promise<MiNote[]> {
const recursive = async (noteId: string): Promise<MiNote[]> => {
const query = this.notesRepository.createQueryBuilder('note')
.where('note.replyId = :noteId', { noteId })
.orWhere(new Brackets(q => {
q.where('note.renoteId = :noteId', { noteId })
.andWhere('note.text IS NOT NULL');
}))
.leftJoinAndSelect('note.user', 'user');
const replies = await query.getMany();
return [
replies,
...await Promise.all(replies.map(reply => recursive(reply.id))),
].flat();
};
const cascadingNotes: MiNote[] = await recursive(note.id);
return cascadingNotes;
}
@bindThis @bindThis
private async getMentionedRemoteUsers(note: MiNote) { private async getMentionedRemoteUsers(note: MiNote) {
const where = [] as any[]; const where = [] as any[];

View File

@ -360,7 +360,7 @@ export class QueryService {
public generateSuspendedUserQueryForNote(q: SelectQueryBuilder<any>, excludeAuthor?: boolean): void { public generateSuspendedUserQueryForNote(q: SelectQueryBuilder<any>, excludeAuthor?: boolean): void {
if (excludeAuthor) { if (excludeAuthor) {
const brakets = (user: string) => new Brackets(qb => qb const brakets = (user: string) => new Brackets(qb => qb
.where(`note.${user}Id IS NULL`) .where(`${user}.id IS NULL`) // そもそもreplyやrenoteではない、もしくはleftjoinなどでuserが存在しなかった場合を考慮
.orWhere(`user.id = ${user}.id`) .orWhere(`user.id = ${user}.id`)
.orWhere(`${user}.isSuspended = FALSE`)); .orWhere(`${user}.isSuspended = FALSE`));
q q
@ -368,7 +368,7 @@ export class QueryService {
.andWhere(brakets('renoteUser')); .andWhere(brakets('renoteUser'));
} else { } else {
const brakets = (user: string) => new Brackets(qb => qb const brakets = (user: string) => new Brackets(qb => qb
.where(`note.${user}Id IS NULL`) .where(`${user}.id IS NULL`) // そもそもreplyやrenoteではない、もしくはleftjoinなどでuserが存在しなかった場合を考慮
.orWhere(`${user}.isSuspended = FALSE`)); .orWhere(`${user}.isSuspended = FALSE`));
q q
.andWhere('user.isSuspended = FALSE') .andWhere('user.isSuspended = FALSE')

View File

@ -4,7 +4,7 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm'; import { EntityNotFoundError, In } from 'typeorm';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
@ -46,6 +46,17 @@ function getAppearNoteIds(notes: MiNote[]): Set<string> {
return appearNoteIds; return appearNoteIds;
} }
async function nullIfEntityNotFound<T>(promise: Promise<T>): Promise<T | null> {
try {
return await promise;
} catch (err) {
if (err instanceof EntityNotFoundError) {
return null;
}
throw err;
}
}
@Injectable() @Injectable()
export class NoteEntityService implements OnModuleInit { export class NoteEntityService implements OnModuleInit {
private userEntityService: UserEntityService; private userEntityService: UserEntityService;
@ -436,19 +447,21 @@ export class NoteEntityService implements OnModuleInit {
...(opts.detail ? { ...(opts.detail ? {
clippedCount: note.clippedCount, clippedCount: note.clippedCount,
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, { // そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される
reply: (note.replyId && note.reply === null) ? null : note.replyId ? nullIfEntityNotFound(this.pack(note.reply ?? note.replyId, me, {
detail: false, detail: false,
skipHide: opts.skipHide, skipHide: opts.skipHide,
withReactionAndUserPairCache: opts.withReactionAndUserPairCache, withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
_hint_: options?._hint_, _hint_: options?._hint_,
}) : undefined, })) : undefined,
renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, { // そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される
renote: (note.renoteId && note.renote === null) ? null : note.renoteId ? nullIfEntityNotFound(this.pack(note.renote ?? note.renoteId, me, {
detail: true, detail: true,
skipHide: opts.skipHide, skipHide: opts.skipHide,
withReactionAndUserPairCache: opts.withReactionAndUserPairCache, withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
_hint_: options?._hint_, _hint_: options?._hint_,
}) : undefined, })) : undefined,
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
@ -591,7 +604,7 @@ export class NoteEntityService implements OnModuleInit {
private findNoteOrFail(id: string): Promise<MiNote> { private findNoteOrFail(id: string): Promise<MiNote> {
return this.notesRepository.findOneOrFail({ return this.notesRepository.findOneOrFail({
where: { id }, where: { id },
relations: ['user'], relations: ['user', 'renote', 'reply'],
}); });
} }

View File

@ -36,7 +36,7 @@ export class MiNote {
public replyId: MiNote['id'] | null; public replyId: MiNote['id'] | null;
@ManyToOne(type => MiNote, { @ManyToOne(type => MiNote, {
onDelete: 'CASCADE', createForeignKeyConstraints: false,
}) })
@JoinColumn() @JoinColumn()
public reply: MiNote | null; public reply: MiNote | null;
@ -50,7 +50,7 @@ export class MiNote {
public renoteId: MiNote['id'] | null; public renoteId: MiNote['id'] | null;
@ManyToOne(type => MiNote, { @ManyToOne(type => MiNote, {
onDelete: 'CASCADE', createForeignKeyConstraints: false,
}) })
@JoinColumn() @JoinColumn()
public renote: MiNote | null; public renote: MiNote | null;

View File

@ -40,8 +40,8 @@ export class GetterService {
} }
@bindThis @bindThis
public async getNoteWithUser(noteId: MiNote['id']) { public async getNoteWithRelations(noteId: MiNote['id']) {
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] }); const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user', 'reply', 'renote', 'reply.user', 'renote.user'] });
if (note == null) { if (note == null) {
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');

View File

@ -269,7 +269,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let renote: MiNote | null = null; let renote: MiNote | null = null;
if (ps.renoteId != null) { if (ps.renoteId != null) {
// Fetch renote to note // Fetch renote to note
renote = await this.notesRepository.findOneBy({ id: ps.renoteId }); renote = await this.notesRepository.findOne({
where: { id: ps.renoteId },
relations: ['user', 'renote', 'reply'],
});
if (renote == null) { if (renote == null) {
throw new ApiError(meta.errors.noSuchRenoteTarget); throw new ApiError(meta.errors.noSuchRenoteTarget);
@ -315,7 +318,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let reply: MiNote | null = null; let reply: MiNote | null = null;
if (ps.replyId != null) { if (ps.replyId != null) {
// Fetch reply // Fetch reply
reply = await this.notesRepository.findOneBy({ id: ps.replyId }); reply = await this.notesRepository.findOne({
where: { id: ps.replyId },
relations: ['user'],
});
if (reply == null) { if (reply == null) {
throw new ApiError(meta.errors.noSuchReplyTarget); throw new ApiError(meta.errors.noSuchReplyTarget);

View File

@ -55,7 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private getterService: GetterService, private getterService: GetterService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const note = await this.getterService.getNoteWithUser(ps.noteId).catch(err => { const note = await this.getterService.getNoteWithRelations(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err; throw err;
}); });

View File

@ -580,7 +580,7 @@ export class ClientServerService {
id: request.params.note, id: request.params.note,
visibility: In(['public', 'home']), visibility: In(['public', 'home']),
}, },
relations: ['user'], relations: ['user', 'reply', 'renote'],
}); });
if ( if (
@ -821,8 +821,11 @@ export class ClientServerService {
fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => { fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
reply.removeHeader('X-Frame-Options'); reply.removeHeader('X-Frame-Options');
const note = await this.notesRepository.findOneBy({ const note = await this.notesRepository.findOne({
id: request.params.note, where: {
id: request.params.note,
},
relations: ['user', 'reply', 'renote'],
}); });
if (note == null) return; if (note == null) return;

View File

@ -63,7 +63,6 @@ describe('Note', () => {
deepStrictEqualWithExcludedFields(note, resolvedNote, [ deepStrictEqualWithExcludedFields(note, resolvedNote, [
'id', 'id',
'emojis', 'emojis',
'reactionAcceptance',
'replyId', 'replyId',
'reply', 'reply',
'userId', 'userId',
@ -105,7 +104,6 @@ describe('Note', () => {
deepStrictEqualWithExcludedFields(note, resolvedNote, [ deepStrictEqualWithExcludedFields(note, resolvedNote, [
'id', 'id',
'emojis', 'emojis',
'reactionAcceptance',
'renoteId', 'renoteId',
'renote', 'renote',
'userId', 'userId',

View File

@ -673,7 +673,6 @@ describe('アンテナ', () => {
assert.deepStrictEqual(response, expected); assert.deepStrictEqual(response, expected);
}); });
test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { }); test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { });
test.each([ test.each([
{ label: 'ID指定', offsetBy: 'id' }, { label: 'ID指定', offsetBy: 'id' },

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]" :class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]"
tabindex="0" tabindex="0"
> >
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/> <MkNoteSub v-if="appearNote.replyId && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div> <div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
<div v-if="isRenote" :class="$style.renote"> <div v-if="isRenote" :class="$style.renote">
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div> <div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
@ -99,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="isEnabledUrlPreview"> <div v-if="isEnabledUrlPreview">
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
</div> </div>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div> <div v-if="appearNote.renoteId" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false"> <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span> <span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
</button> </button>
@ -282,7 +282,7 @@ let note = deepClone(props.note);
//} //}
const isRenote = Misskey.note.isPureRenote(note); const isRenote = Misskey.note.isPureRenote(note);
const appearNote = getAppearNote(note); const appearNote = getAppearNote(note) ?? note;
const { $note: $appearNote, subscribe: subscribeManuallyToNoteCapture } = useNoteCapture({ const { $note: $appearNote, subscribe: subscribeManuallyToNoteCapture } = useNoteCapture({
note: appearNote, note: appearNote,
parentNote: note, parentNote: note,

View File

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/> <MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/>
</div> </div>
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/> <MkNoteSub v-if="appearNote.replyId" :note="appearNote.reply" :class="$style.replyTo"/>
<div v-if="isRenote" :class="$style.renote"> <div v-if="isRenote" :class="$style.renote">
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/> <MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
<i class="ti ti-repeat" style="margin-right: 4px;"></i> <i class="ti ti-repeat" style="margin-right: 4px;"></i>

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div :class="$style.root"> <div v-if="note" :class="$style.root">
<MkAvatar :class="$style.avatar" :user="note.user" link preview/> <MkAvatar :class="$style.avatar" :user="note.user" link preview/>
<div :class="$style.main"> <div :class="$style.main">
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/> <MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
@ -19,6 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
</div> </div>
<div v-else :class="$style.deleted">
{{ i18n.ts.deletedNote }}
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -27,9 +30,10 @@ import * as Misskey from 'misskey-js';
import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue'; import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
import MkCwButton from '@/components/MkCwButton.vue'; import MkCwButton from '@/components/MkCwButton.vue';
import { i18n } from '@/i18n.js';
const props = defineProps<{ const props = defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note | null;
}>(); }>();
const showContent = ref(false); const showContent = ref(false);
@ -101,4 +105,14 @@ const showContent = ref(false);
height: 48px; height: 48px;
} }
} }
.deleted {
text-align: center;
padding: 8px !important;
margin: 8px 8px 0 8px;
--color: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.15));
background-size: auto auto;
background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
border-radius: 8px;
}
</style> </style>

View File

@ -4,7 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div v-if="!muted" :class="[$style.root, { [$style.children]: depth > 1 }]"> <div v-if="note == null" :class="$style.deleted">
{{ i18n.ts.deletedNote }}
</div>
<div v-else-if="!muted" :class="[$style.root, { [$style.children]: depth > 1 }]">
<div :class="$style.main"> <div :class="$style.main">
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div> <div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="note.user" link preview/> <MkAvatar :class="$style.avatar" :user="note.user" link preview/>
@ -53,7 +56,7 @@ import { userPage } from '@/filters/user.js';
import { checkWordMute } from '@/utility/check-word-mute.js'; import { checkWordMute } from '@/utility/check-word-mute.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note | null;
detail?: boolean; detail?: boolean;
// how many notes are in between this one and the note being viewed in detail // how many notes are in between this one and the note being viewed in detail
@ -62,12 +65,12 @@ const props = withDefaults(defineProps<{
depth: 1, depth: 1,
}); });
const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false); const muted = ref(props.note && $i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
const showContent = ref(false); const showContent = ref(false);
const replies = ref<Misskey.entities.Note[]>([]); const replies = ref<Misskey.entities.Note[]>([]);
if (props.detail) { if (props.detail && props.note) {
misskeyApi('notes/children', { misskeyApi('notes/children', {
noteId: props.note.id, noteId: props.note.id,
limit: 5, limit: 5,
@ -160,4 +163,14 @@ if (props.detail) {
margin: 8px 8px 0 8px; margin: 8px 8px 0 8px;
border-radius: 8px; border-radius: 8px;
} }
.deleted {
text-align: center;
padding: 8px !important;
margin: 8px 8px 0 8px;
--color: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.15));
background-size: auto auto;
background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
border-radius: 8px;
}
</style> </style>

View File

@ -3271,7 +3271,7 @@ type PromoReadRequest = operations['promo___read']['requestBody']['content']['ap
type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'text'>> & AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>> & { type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'text'>> & AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>> & {
files: []; files: [];
fileIds: []; fileIds: [];
} & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>; } & NonNullableRecord<Pick<Note, 'renoteId'>> & Pick<Note, 'renote'>;
// @public (undocumented) // @public (undocumented)
type QueueCount = components['schemas']['QueueCount']; type QueueCount = components['schemas']['QueueCount'];
@ -3801,7 +3801,7 @@ type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['respons
// Warnings were encountered during analysis: // Warnings were encountered during analysis:
// //
// src/entities.ts:54:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/entities.ts:55:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts // src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:218:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:218:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:228:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:228:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts

View File

@ -33,7 +33,8 @@ export type PureRenote =
& AllNullRecord<Pick<Note, 'text'>> & AllNullRecord<Pick<Note, 'text'>>
& AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>> & AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>>
& { files: []; fileIds: []; } & { files: []; fileIds: []; }
& NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>; & NonNullableRecord<Pick<Note, 'renoteId'>>
& Pick<Note, 'renote'>; // リート対象が削除された場合、renoteIdはあるがrenoteはnullになる
export type PageEvent = { export type PageEvent = {
pageId: Page['id']; pageId: Page['id'];

View File

@ -2,8 +2,8 @@ import type { Note, PureRenote } from './entities.js';
export function isPureRenote(note: Note): note is PureRenote { export function isPureRenote(note: Note): note is PureRenote {
return ( return (
note.renote != null && note.renoteId != null &&
note.reply == null && note.replyId == null &&
note.text == null && note.text == null &&
note.cw == null && note.cw == null &&
(note.fileIds == null || note.fileIds.length === 0) && (note.fileIds == null || note.fileIds.length === 0) &&