Merge branch 'develop' into sw-file
This commit is contained in:
commit
ba6effb57d
|
@ -109,6 +109,7 @@ jobs:
|
|||
name: E2E tests (backend)
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version-file:
|
||||
- .node-version
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
## Unreleased
|
||||
|
||||
### General
|
||||
-
|
||||
- ノートを削除した際、関連するノートが同時に削除されないようになりました
|
||||
- APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります
|
||||
|
||||
### Client
|
||||
-
|
||||
- Fix: 一部の設定検索結果が存在しないパスになる問題を修正
|
||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
|
||||
|
||||
### Server
|
||||
-
|
||||
- Enhance: ノートの削除処理の効率化
|
||||
- Enhance: 全体的なパフォーマンスの向上
|
||||
|
||||
|
||||
## 2025.7.0
|
||||
|
|
|
@ -2567,11 +2567,11 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"serviceworkerInfo": string;
|
||||
/**
|
||||
* 削除された投稿
|
||||
* 削除されたノート
|
||||
*/
|
||||
"deletedNote": string;
|
||||
/**
|
||||
* 非公開の投稿
|
||||
* 非公開のノート
|
||||
*/
|
||||
"invisibleNote": string;
|
||||
/**
|
||||
|
|
|
@ -637,8 +637,8 @@ addRelay: "リレーの追加"
|
|||
inboxUrl: "inboxのURL"
|
||||
addedRelays: "追加済みのリレー"
|
||||
serviceworkerInfo: "プッシュ通知を行うには有効にする必要があります。"
|
||||
deletedNote: "削除された投稿"
|
||||
invisibleNote: "非公開の投稿"
|
||||
deletedNote: "削除されたノート"
|
||||
invisibleNote: "非公開のノート"
|
||||
enableInfiniteScroll: "自動でもっと見る"
|
||||
visibility: "公開範囲"
|
||||
poll: "アンケート"
|
||||
|
|
|
@ -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`);
|
||||
}
|
||||
}
|
|
@ -20,6 +20,8 @@ import { CacheService } from '@/core/CacheService.js';
|
|||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||
|
||||
type NoteFilter = (note: MiNote) => boolean;
|
||||
|
||||
type TimelineOptions = {
|
||||
untilId: string | null,
|
||||
sinceId: string | null,
|
||||
|
@ -28,7 +30,7 @@ type TimelineOptions = {
|
|||
me?: { id: MiUser['id'] } | undefined | null,
|
||||
useDbFallback: boolean,
|
||||
redisTimelines: FanoutTimelineName[],
|
||||
noteFilter?: (note: MiNote) => boolean,
|
||||
noteFilter?: NoteFilter,
|
||||
alwaysIncludeMyNotes?: boolean;
|
||||
ignoreAuthorFromBlock?: boolean;
|
||||
ignoreAuthorFromMute?: boolean;
|
||||
|
@ -79,7 +81,7 @@ export class FanoutTimelineEndpointService {
|
|||
const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
|
||||
|
||||
if (!shouldFallbackToDb) {
|
||||
let filter = ps.noteFilter ?? (_note => true);
|
||||
let filter = ps.noteFilter ?? (_note => true) as NoteFilter;
|
||||
|
||||
if (ps.alwaysIncludeMyNotes && ps.me) {
|
||||
const me = ps.me;
|
||||
|
@ -145,15 +147,11 @@ export class FanoutTimelineEndpointService {
|
|||
{
|
||||
const parentFilter = filter;
|
||||
filter = (note) => {
|
||||
const noteJoined = note as MiNote & {
|
||||
renoteUser: MiUser | null;
|
||||
replyUser: MiUser | null;
|
||||
};
|
||||
if (!ps.ignoreAuthorFromUserSuspension) {
|
||||
if (note.user!.isSuspended) return false;
|
||||
}
|
||||
if (note.userId !== note.renoteUserId && noteJoined.renoteUser?.isSuspended) return false;
|
||||
if (note.userId !== note.replyUserId && noteJoined.replyUser?.isSuspended) return false;
|
||||
if (note.userId !== note.renoteUserId && note.renote?.user?.isSuspended) return false;
|
||||
if (note.userId !== note.replyUserId && note.reply?.user?.isSuspended) return false;
|
||||
|
||||
return parentFilter(note);
|
||||
};
|
||||
|
@ -200,7 +198,7 @@ export class FanoutTimelineEndpointService {
|
|||
return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit);
|
||||
}
|
||||
|
||||
private async getAndFilterFromDb(noteIds: string[], noteFilter: (note: MiNote) => boolean, idCompare: (a: string, b: string) => number): Promise<MiNote[]> {
|
||||
private async getAndFilterFromDb(noteIds: string[], noteFilter: NoteFilter, idCompare: (a: string, b: string) => number): Promise<MiNote[]> {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
|
|
|
@ -421,7 +421,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
emojis,
|
||||
userId: user.id,
|
||||
localOnly: data.localOnly!,
|
||||
reactionAcceptance: data.reactionAcceptance,
|
||||
reactionAcceptance: data.reactionAcceptance ?? null,
|
||||
visibility: data.visibility as any,
|
||||
visibleUserIds: data.visibility === 'specified'
|
||||
? data.visibleUsers
|
||||
|
@ -483,7 +483,11 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
await this.notesRepository.insert(insert);
|
||||
}
|
||||
|
||||
return insert;
|
||||
return {
|
||||
...insert,
|
||||
reply: data.reply ?? null,
|
||||
renote: data.renote ?? null,
|
||||
};
|
||||
} catch (e) {
|
||||
// duplicate key error
|
||||
if (isDuplicateKeyValueError(e)) {
|
||||
|
|
|
@ -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) {
|
||||
const deletedAt = new Date();
|
||||
const cascadingNotes = await this.findCascadingNotes(note);
|
||||
|
||||
if (note.replyId) {
|
||||
await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1);
|
||||
|
@ -90,15 +89,6 @@ export class NoteDeleteService {
|
|||
|
||||
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
|
||||
|
||||
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);
|
||||
|
||||
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
|
||||
private async getMentionedRemoteUsers(note: MiNote) {
|
||||
const where = [] as any[];
|
||||
|
|
|
@ -360,7 +360,7 @@ export class QueryService {
|
|||
public generateSuspendedUserQueryForNote(q: SelectQueryBuilder<any>, excludeAuthor?: boolean): void {
|
||||
if (excludeAuthor) {
|
||||
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}.isSuspended = FALSE`));
|
||||
q
|
||||
|
@ -368,7 +368,7 @@ export class QueryService {
|
|||
.andWhere(brakets('renoteUser'));
|
||||
} else {
|
||||
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`));
|
||||
q
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
|
|
|
@ -17,6 +17,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
||||
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
||||
import { type SystemWebhookPayload } from '@/core/SystemWebhookService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { type UserWebhookPayload } from './UserWebhookService.js';
|
||||
import type {
|
||||
DbJobData,
|
||||
|
@ -39,7 +40,6 @@ import type {
|
|||
} from './QueueModule.js';
|
||||
import type httpSignature from '@peertube/http-signature';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
export const QUEUE_TYPES = [
|
||||
'system',
|
||||
|
@ -69,61 +69,85 @@ export class QueueService {
|
|||
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
||||
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
||||
) {
|
||||
this.systemQueue.add('tickCharts', {
|
||||
this.systemQueue.upsertJobScheduler('tickCharts', {
|
||||
pattern: '55 * * * *',
|
||||
}, {
|
||||
repeat: { pattern: '55 * * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
name: 'tickCharts',
|
||||
opts: {
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
},
|
||||
});
|
||||
|
||||
this.systemQueue.add('resyncCharts', {
|
||||
this.systemQueue.upsertJobScheduler('resyncCharts', {
|
||||
pattern: '0 0 * * *',
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
name: 'resyncCharts',
|
||||
opts: {
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
},
|
||||
});
|
||||
|
||||
this.systemQueue.add('cleanCharts', {
|
||||
this.systemQueue.upsertJobScheduler('cleanCharts', {
|
||||
pattern: '0 0 * * *',
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
name: 'cleanCharts',
|
||||
opts: {
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
},
|
||||
});
|
||||
|
||||
this.systemQueue.add('aggregateRetention', {
|
||||
this.systemQueue.upsertJobScheduler('aggregateRetention', {
|
||||
pattern: '0 0 * * *',
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
name: 'aggregateRetention',
|
||||
opts: {
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
},
|
||||
});
|
||||
|
||||
this.systemQueue.add('clean', {
|
||||
this.systemQueue.upsertJobScheduler('clean', {
|
||||
pattern: '0 0 * * *',
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
name: 'clean',
|
||||
opts: {
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
},
|
||||
});
|
||||
|
||||
this.systemQueue.add('checkExpiredMutings', {
|
||||
this.systemQueue.upsertJobScheduler('checkExpiredMutings', {
|
||||
pattern: '*/5 * * * *',
|
||||
}, {
|
||||
repeat: { pattern: '*/5 * * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
name: 'checkExpiredMutings',
|
||||
opts: {
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
},
|
||||
});
|
||||
|
||||
this.systemQueue.add('bakeBufferedReactions', {
|
||||
this.systemQueue.upsertJobScheduler('bakeBufferedReactions', {
|
||||
pattern: '0 0 * * *',
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
name: 'bakeBufferedReactions',
|
||||
opts: {
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
},
|
||||
});
|
||||
|
||||
this.systemQueue.add('checkModeratorsActivity', {
|
||||
}, {
|
||||
this.systemQueue.upsertJobScheduler('checkModeratorsActivity', {
|
||||
// 毎時30分に起動
|
||||
repeat: { pattern: '30 * * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
pattern: '30 * * * *',
|
||||
}, {
|
||||
name: 'checkModeratorsActivity',
|
||||
opts: {
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { EntityNotFoundError, In } from 'typeorm';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
|
@ -46,6 +46,17 @@ function getAppearNoteIds(notes: MiNote[]): Set<string> {
|
|||
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()
|
||||
export class NoteEntityService implements OnModuleInit {
|
||||
private userEntityService: UserEntityService;
|
||||
|
@ -436,19 +447,21 @@ export class NoteEntityService implements OnModuleInit {
|
|||
...(opts.detail ? {
|
||||
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,
|
||||
skipHide: opts.skipHide,
|
||||
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
|
||||
_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,
|
||||
skipHide: opts.skipHide,
|
||||
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
|
||||
_hint_: options?._hint_,
|
||||
}) : undefined,
|
||||
})) : undefined,
|
||||
|
||||
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
|
||||
|
||||
|
@ -591,7 +604,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
private findNoteOrFail(id: string): Promise<MiNote> {
|
||||
return this.notesRepository.findOneOrFail({
|
||||
where: { id },
|
||||
relations: ['user'],
|
||||
relations: ['user', 'renote', 'reply'],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ export class MiNote {
|
|||
public replyId: MiNote['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiNote, {
|
||||
onDelete: 'CASCADE',
|
||||
createForeignKeyConstraints: false,
|
||||
})
|
||||
@JoinColumn()
|
||||
public reply: MiNote | null;
|
||||
|
@ -50,7 +50,7 @@ export class MiNote {
|
|||
public renoteId: MiNote['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiNote, {
|
||||
onDelete: 'CASCADE',
|
||||
createForeignKeyConstraints: false,
|
||||
})
|
||||
@JoinColumn()
|
||||
public renote: MiNote | null;
|
||||
|
|
|
@ -40,8 +40,8 @@ export class GetterService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async getNoteWithUser(noteId: MiNote['id']) {
|
||||
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] });
|
||||
public async getNoteWithRelations(noteId: MiNote['id']) {
|
||||
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user', 'reply', 'renote', 'reply.user', 'renote.user'] });
|
||||
|
||||
if (note == null) {
|
||||
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
|
||||
|
|
|
@ -269,7 +269,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
let renote: MiNote | null = null;
|
||||
if (ps.renoteId != null) {
|
||||
// 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) {
|
||||
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;
|
||||
if (ps.replyId != null) {
|
||||
// Fetch reply
|
||||
reply = await this.notesRepository.findOneBy({ id: ps.replyId });
|
||||
reply = await this.notesRepository.findOne({
|
||||
where: { id: ps.replyId },
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
if (reply == null) {
|
||||
throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||
|
|
|
@ -55,7 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private getterService: GetterService,
|
||||
) {
|
||||
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);
|
||||
throw err;
|
||||
});
|
||||
|
|
|
@ -237,7 +237,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere('note.renoteId IS NULL');
|
||||
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
|
||||
|
||||
|
|
|
@ -586,7 +586,7 @@ export class ClientServerService {
|
|||
id: request.params.note,
|
||||
visibility: In(['public', 'home']),
|
||||
},
|
||||
relations: ['user'],
|
||||
relations: ['user', 'reply', 'renote'],
|
||||
});
|
||||
|
||||
if (
|
||||
|
@ -827,8 +827,11 @@ export class ClientServerService {
|
|||
fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
|
||||
reply.removeHeader('X-Frame-Options');
|
||||
|
||||
const note = await this.notesRepository.findOneBy({
|
||||
id: request.params.note,
|
||||
const note = await this.notesRepository.findOne({
|
||||
where: {
|
||||
id: request.params.note,
|
||||
},
|
||||
relations: ['user', 'reply', 'renote'],
|
||||
});
|
||||
|
||||
if (note == null) return;
|
||||
|
|
|
@ -63,7 +63,6 @@ describe('Note', () => {
|
|||
deepStrictEqualWithExcludedFields(note, resolvedNote, [
|
||||
'id',
|
||||
'emojis',
|
||||
'reactionAcceptance',
|
||||
'replyId',
|
||||
'reply',
|
||||
'userId',
|
||||
|
@ -105,7 +104,6 @@ describe('Note', () => {
|
|||
deepStrictEqualWithExcludedFields(note, resolvedNote, [
|
||||
'id',
|
||||
'emojis',
|
||||
'reactionAcceptance',
|
||||
'renoteId',
|
||||
'renote',
|
||||
'userId',
|
||||
|
|
|
@ -673,7 +673,6 @@ describe('アンテナ', () => {
|
|||
assert.deepStrictEqual(response, expected);
|
||||
});
|
||||
|
||||
|
||||
test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { });
|
||||
test.each([
|
||||
{ label: 'ID指定', offsetBy: 'id' },
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -18,5 +18,5 @@ onmessage = (event) => {
|
|||
|
||||
render(event.data.hash, canvas);
|
||||
const bitmap = canvas.transferToImageBitmap();
|
||||
postMessage({ id: event.data.id, bitmap });
|
||||
postMessage({ id: event.data.id, bitmap }, [bitmap]);
|
||||
};
|
||||
|
|
|
@ -495,7 +495,7 @@ function done(query?: string): boolean | void {
|
|||
|
||||
function settings() {
|
||||
emit('esc');
|
||||
router.push('settings/emoji-palette');
|
||||
router.push('/settings/emoji-palette');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
@ -52,15 +52,20 @@ import TestWebGL2 from '@/workers/test-webgl2?worker';
|
|||
import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js';
|
||||
import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
|
||||
|
||||
// テスト環境で Web Worker インスタンスは作成できない
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
const isTest = (import.meta.env.MODE === 'test' || window.Cypress != null);
|
||||
|
||||
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
|
||||
// テスト環境で Web Worker インスタンスは作成できない
|
||||
if (import.meta.env.MODE === 'test') {
|
||||
if (isTest) {
|
||||
const canvas = window.document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
resolve(canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
const testWorker = new TestWebGL2();
|
||||
testWorker.addEventListener('message', event => {
|
||||
if (event.data.result) {
|
||||
|
@ -189,7 +194,7 @@ function drawAvg() {
|
|||
}
|
||||
|
||||
async function draw() {
|
||||
if (import.meta.env.MODE === 'test' && props.hash == null) return;
|
||||
if (isTest && props.hash == null) return;
|
||||
|
||||
drawAvg();
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]"
|
||||
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="isRenote" :class="$style.renote">
|
||||
<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">
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
||||
</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">
|
||||
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
|
||||
</button>
|
||||
|
@ -282,7 +282,7 @@ let note = deepClone(props.note);
|
|||
//}
|
||||
|
||||
const isRenote = Misskey.note.isPureRenote(note);
|
||||
const appearNote = getAppearNote(note);
|
||||
const appearNote = getAppearNote(note) ?? note;
|
||||
const { $note: $appearNote, subscribe: subscribeManuallyToNoteCapture } = useNoteCapture({
|
||||
note: appearNote,
|
||||
parentNote: note,
|
||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/>
|
||||
</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">
|
||||
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
|
||||
<i class="ti ti-repeat" style="margin-right: 4px;"></i>
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<div v-if="note" :class="$style.root">
|
||||
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
|
||||
<div :class="$style.main">
|
||||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||
|
@ -19,6 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.deleted">
|
||||
{{ i18n.ts.deletedNote }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -27,9 +30,10 @@ import * as Misskey from 'misskey-js';
|
|||
import MkNoteHeader from '@/components/MkNoteHeader.vue';
|
||||
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
||||
import MkCwButton from '@/components/MkCwButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
note: Misskey.entities.Note | null;
|
||||
}>();
|
||||
|
||||
const showContent = ref(false);
|
||||
|
@ -101,4 +105,14 @@ const showContent = ref(false);
|
|||
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>
|
||||
|
|
|
@ -4,7 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<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 v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
|
||||
<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';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
note: Misskey.entities.Note | null;
|
||||
detail?: boolean;
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
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 replies = ref<Misskey.entities.Note[]>([]);
|
||||
|
||||
if (props.detail) {
|
||||
if (props.detail && props.note) {
|
||||
misskeyApi('notes/children', {
|
||||
noteId: props.note.id,
|
||||
limit: 5,
|
||||
|
@ -160,4 +163,14 @@ if (props.detail) {
|
|||
margin: 8px 8px 0 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>
|
||||
|
|
|
@ -151,7 +151,7 @@ const contextmenu = computed(() => ([{
|
|||
|
||||
function back() {
|
||||
history.value.pop();
|
||||
windowRouter.replace(history.value.at(-1)!.path);
|
||||
windowRouter.replaceByPath(history.value.at(-1)!.path);
|
||||
}
|
||||
|
||||
function reload() {
|
||||
|
@ -163,7 +163,7 @@ function close() {
|
|||
}
|
||||
|
||||
function expand() {
|
||||
mainRouter.push(windowRouter.getCurrentFullPath(), 'forcePage');
|
||||
mainRouter.pushByPath(windowRouter.getCurrentFullPath(), 'forcePage');
|
||||
windowEl.value?.close();
|
||||
}
|
||||
|
||||
|
|
|
@ -186,7 +186,7 @@ function searchOnKeyDown(ev: KeyboardEvent) {
|
|||
|
||||
if (ev.key === 'Enter' && searchSelectedIndex.value != null) {
|
||||
ev.preventDefault();
|
||||
router.push(searchResult.value[searchSelectedIndex.value].path + '#' + searchResult.value[searchSelectedIndex.value].id);
|
||||
router.pushByPath(searchResult.value[searchSelectedIndex.value].path + '#' + searchResult.value[searchSelectedIndex.value].id);
|
||||
} else if (ev.key === 'ArrowDown') {
|
||||
ev.preventDefault();
|
||||
const current = searchSelectedIndex.value ?? -1;
|
||||
|
|
|
@ -64,7 +64,7 @@ function onContextmenu(ev) {
|
|||
icon: 'ti ti-player-eject',
|
||||
text: i18n.ts.showInPage,
|
||||
action: () => {
|
||||
router.push(props.to, 'forcePage');
|
||||
router.pushByPath(props.to, 'forcePage');
|
||||
},
|
||||
}, { type: 'divider' }, {
|
||||
icon: 'ti ti-external-link',
|
||||
|
@ -99,6 +99,6 @@ function nav(ev: MouseEvent) {
|
|||
return openWindow();
|
||||
}
|
||||
|
||||
router.push(props.to, ev.ctrlKey ? 'forcePage' : null);
|
||||
router.pushByPath(props.to, ev.ctrlKey ? 'forcePage' : null);
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -76,7 +76,7 @@ function mount() {
|
|||
function back() {
|
||||
const prev = tabs.value[tabs.value.length - 2];
|
||||
tabs.value = [...tabs.value.slice(0, tabs.value.length - 1)];
|
||||
router.replace(prev.fullPath);
|
||||
router?.replaceByPath(prev.fullPath);
|
||||
}
|
||||
|
||||
router.useListener('change', ({ resolved }) => {
|
||||
|
|
|
@ -58,7 +58,7 @@ export type RouterEvents = {
|
|||
beforeFullPath: string;
|
||||
fullPath: string;
|
||||
route: RouteDef | null;
|
||||
props: Map<string, string> | null;
|
||||
props: Map<string, string | boolean> | null;
|
||||
}) => void;
|
||||
same: () => void;
|
||||
};
|
||||
|
@ -77,6 +77,110 @@ export type PathResolvedResult = {
|
|||
};
|
||||
};
|
||||
|
||||
//#region Path Types
|
||||
type Prettify<T> = {
|
||||
[K in keyof T]: T[K]
|
||||
} & {};
|
||||
|
||||
type RemoveNever<T> = {
|
||||
[K in keyof T as T[K] extends never ? never : K]: T[K];
|
||||
} & {};
|
||||
|
||||
type IsPathParameter<Part extends string> = Part extends `${string}:${infer Parameter}` ? Parameter : never;
|
||||
|
||||
type GetPathParamKeys<Path extends string> =
|
||||
Path extends `${infer A}/${infer B}`
|
||||
? IsPathParameter<A> | GetPathParamKeys<B>
|
||||
: IsPathParameter<Path>;
|
||||
|
||||
type GetPathParams<Path extends string> = Prettify<{
|
||||
[Param in GetPathParamKeys<Path> as Param extends `${string}?` ? never : Param]: string;
|
||||
} & {
|
||||
[Param in GetPathParamKeys<Path> as Param extends `${infer OptionalParam}?` ? OptionalParam : never]?: string;
|
||||
}>;
|
||||
|
||||
type UnwrapReadOnly<T> = T extends ReadonlyArray<infer U>
|
||||
? U
|
||||
: T extends Readonly<infer U>
|
||||
? U
|
||||
: T;
|
||||
|
||||
type GetPaths<Def extends RouteDef> = Def extends { path: infer Path }
|
||||
? Path extends string
|
||||
? Def extends { children: infer Children }
|
||||
? Children extends RouteDef[]
|
||||
? Path | `${Path}${FlattenAllPaths<Children>}`
|
||||
: Path
|
||||
: Path
|
||||
: never
|
||||
: never;
|
||||
|
||||
type FlattenAllPaths<Defs extends RouteDef[]> = GetPaths<Defs[number]>;
|
||||
|
||||
type GetSinglePathQuery<Def extends RouteDef, Path extends FlattenAllPaths<RouteDef[]>> = RemoveNever<
|
||||
Def extends { path: infer BasePath, children: infer Children }
|
||||
? BasePath extends string
|
||||
? Path extends `${BasePath}${infer ChildPath}`
|
||||
? Children extends RouteDef[]
|
||||
? ChildPath extends FlattenAllPaths<Children>
|
||||
? GetPathQuery<Children, ChildPath>
|
||||
: Record<string, never>
|
||||
: never
|
||||
: never
|
||||
: never
|
||||
: Def['path'] extends Path
|
||||
? Def extends { query: infer Query }
|
||||
? Query extends Record<string, string>
|
||||
? UnwrapReadOnly<{ [Key in keyof Query]?: string; }>
|
||||
: Record<string, never>
|
||||
: Record<string, never>
|
||||
: Record<string, never>
|
||||
>;
|
||||
|
||||
type GetPathQuery<Defs extends RouteDef[], Path extends FlattenAllPaths<Defs>> = GetSinglePathQuery<Defs[number], Path>;
|
||||
|
||||
type RequiredIfNotEmpty<K extends string, T extends Record<string, unknown>> = T extends Record<string, never>
|
||||
? { [Key in K]?: T }
|
||||
: { [Key in K]: T };
|
||||
|
||||
type NotRequiredIfEmpty<T extends Record<string, unknown>> = T extends Record<string, never> ? T | undefined : T;
|
||||
|
||||
type GetRouterOperationProps<Defs extends RouteDef[], Path extends FlattenAllPaths<Defs>> = NotRequiredIfEmpty<RequiredIfNotEmpty<'params', GetPathParams<Path>> & {
|
||||
query?: GetPathQuery<Defs, Path>;
|
||||
hash?: string;
|
||||
}>;
|
||||
//#endregion
|
||||
|
||||
function buildFullPath(args: {
|
||||
path: string;
|
||||
params?: Record<string, string>;
|
||||
query?: Record<string, string>;
|
||||
hash?: string;
|
||||
}) {
|
||||
let fullPath = args.path;
|
||||
|
||||
if (args.params) {
|
||||
for (const key in args.params) {
|
||||
const value = args.params[key];
|
||||
const replaceRegex = new RegExp(`:${key}(\\?)?`, 'g');
|
||||
fullPath = fullPath.replace(replaceRegex, value ? encodeURIComponent(value) : '');
|
||||
}
|
||||
}
|
||||
|
||||
if (args.query) {
|
||||
const queryString = new URLSearchParams(args.query).toString();
|
||||
if (queryString) {
|
||||
fullPath += '?' + queryString;
|
||||
}
|
||||
}
|
||||
|
||||
if (args.hash) {
|
||||
fullPath += '#' + encodeURIComponent(args.hash);
|
||||
}
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
function parsePath(path: string): ParsedPath {
|
||||
const res = [] as ParsedPath;
|
||||
|
||||
|
@ -282,7 +386,7 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvents> {
|
|||
}
|
||||
}
|
||||
|
||||
if (res.route.loginRequired && !this.isLoggedIn) {
|
||||
if (res.route.loginRequired && !this.isLoggedIn && 'component' in res.route) {
|
||||
res.route.component = this.notFoundPageComponent;
|
||||
res.props.set('showLoginPopup', true);
|
||||
}
|
||||
|
@ -310,14 +414,35 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvents> {
|
|||
return this.currentFullPath;
|
||||
}
|
||||
|
||||
public push(fullPath: string, flag?: RouterFlag) {
|
||||
public push<P extends FlattenAllPaths<DEF>>(path: P, props?: GetRouterOperationProps<DEF, P>, flag?: RouterFlag | null) {
|
||||
const fullPath = buildFullPath({
|
||||
path,
|
||||
params: props?.params,
|
||||
query: props?.query,
|
||||
hash: props?.hash,
|
||||
});
|
||||
this.pushByPath(fullPath, flag);
|
||||
}
|
||||
|
||||
public replace<P extends FlattenAllPaths<DEF>>(path: P, props?: GetRouterOperationProps<DEF, P>) {
|
||||
const fullPath = buildFullPath({
|
||||
path,
|
||||
params: props?.params,
|
||||
query: props?.query,
|
||||
hash: props?.hash,
|
||||
});
|
||||
this.replaceByPath(fullPath);
|
||||
}
|
||||
|
||||
/** どうしても必要な場合に使用(パスが確定している場合は `Nirax.push` を使用すること) */
|
||||
public pushByPath(fullPath: string, flag?: RouterFlag | null) {
|
||||
const beforeFullPath = this.currentFullPath;
|
||||
if (fullPath === beforeFullPath) {
|
||||
this.emit('same');
|
||||
return;
|
||||
}
|
||||
if (this.navHook) {
|
||||
const cancel = this.navHook(fullPath, flag);
|
||||
const cancel = this.navHook(fullPath, flag ?? undefined);
|
||||
if (cancel) return;
|
||||
}
|
||||
const res = this.navigate(fullPath);
|
||||
|
@ -333,14 +458,15 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvents> {
|
|||
}
|
||||
}
|
||||
|
||||
public replace(fullPath: string) {
|
||||
/** どうしても必要な場合に使用(パスが確定している場合は `Nirax.replace` を使用すること) */
|
||||
public replaceByPath(fullPath: string) {
|
||||
const res = this.navigate(fullPath);
|
||||
this.emit('replace', {
|
||||
fullPath: res._parsedRoute.fullPath,
|
||||
});
|
||||
}
|
||||
|
||||
public useListener<E extends keyof RouterEvents, L = RouterEvents[E]>(event: E, listener: L) {
|
||||
public useListener<E extends keyof RouterEvents>(event: E, listener: EventEmitter.EventListener<RouterEvents, E>) {
|
||||
this.addListener(event, listener);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
|
|
@ -72,12 +72,20 @@ async function save() {
|
|||
roleId: role.value.id,
|
||||
...data.value,
|
||||
});
|
||||
router.push('/admin/roles/' + role.value.id);
|
||||
router.push('/admin/roles/:id', {
|
||||
params: {
|
||||
id: role.value.id,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const created = await os.apiWithDialog('admin/roles/create', {
|
||||
...data.value,
|
||||
});
|
||||
router.push('/admin/roles/' + created.id);
|
||||
router.push('/admin/roles/:id', {
|
||||
params: {
|
||||
id: created.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -88,7 +88,11 @@ const role = reactive(await misskeyApi('admin/roles/show', {
|
|||
}));
|
||||
|
||||
function edit() {
|
||||
router.push('/admin/roles/' + role.id + '/edit');
|
||||
router.push('/admin/roles/:id/edit', {
|
||||
params: {
|
||||
id: role.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function del() {
|
||||
|
|
|
@ -47,7 +47,11 @@ async function timetravel() {
|
|||
}
|
||||
|
||||
function settings() {
|
||||
router.push(`/my/antennas/${props.antennaId}`);
|
||||
router.push('/my/antennas/:antennaId', {
|
||||
params: {
|
||||
antennaId: props.antennaId,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function focus() {
|
||||
|
|
|
@ -165,7 +165,11 @@ function save() {
|
|||
os.apiWithDialog('channels/update', params);
|
||||
} else {
|
||||
os.apiWithDialog('channels/create', params).then(created => {
|
||||
router.push(`/channels/${created.id}`);
|
||||
router.push('/channels/:channelId', {
|
||||
params: {
|
||||
channelId: created.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,7 +147,11 @@ watch(() => props.channelId, async () => {
|
|||
}, { immediate: true });
|
||||
|
||||
function edit() {
|
||||
router.push(`/channels/${channel.value?.id}/edit`);
|
||||
router.push('/channels/:channelId/edit', {
|
||||
params: {
|
||||
channelId: props.channelId,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openPostForm() {
|
||||
|
|
|
@ -86,7 +86,11 @@ function start(ev: MouseEvent) {
|
|||
async function startUser() {
|
||||
// TODO: localOnly は連合に対応したら消す
|
||||
os.selectUser({ localOnly: true }).then(user => {
|
||||
router.push(`/chat/user/${user.id}`);
|
||||
router.push('/chat/user/:userId', {
|
||||
params: {
|
||||
userId: user.id,
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -101,7 +105,11 @@ async function createRoom() {
|
|||
name: result,
|
||||
});
|
||||
|
||||
router.push(`/chat/room/${room.id}`);
|
||||
router.push('/chat/room/:roomId', {
|
||||
params: {
|
||||
roomId: room.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function search() {
|
||||
|
|
|
@ -61,7 +61,11 @@ async function join(invitation: Misskey.entities.ChatRoomInvitation) {
|
|||
roomId: invitation.room.id,
|
||||
});
|
||||
|
||||
router.push(`/chat/room/${invitation.room.id}`);
|
||||
router.push('/chat/room/:roomId', {
|
||||
params: {
|
||||
roomId: invitation.room.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function ignore(invitation: Misskey.entities.ChatRoomInvitation) {
|
||||
|
|
|
@ -429,7 +429,11 @@ async function save() {
|
|||
script: script.value,
|
||||
visibility: visibility.value,
|
||||
});
|
||||
router.push('/play/' + created.id + '/edit');
|
||||
router.push('/play/:id/edit', {
|
||||
params: {
|
||||
id: created.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -85,7 +85,11 @@ async function save() {
|
|||
fileIds: files.value.map(file => file.id),
|
||||
isSensitive: isSensitive.value,
|
||||
});
|
||||
router.push(`/gallery/${props.postId}`);
|
||||
router.push('/gallery/:postId', {
|
||||
params: {
|
||||
postId: props.postId,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const created = await os.apiWithDialog('gallery/posts/create', {
|
||||
title: title.value,
|
||||
|
@ -93,7 +97,11 @@ async function save() {
|
|||
fileIds: files.value.map(file => file.id),
|
||||
isSensitive: isSensitive.value,
|
||||
});
|
||||
router.push(`/gallery/${created.id}`);
|
||||
router.push('/gallery/:postId', {
|
||||
params: {
|
||||
postId: created.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -150,7 +150,11 @@ async function unlike() {
|
|||
}
|
||||
|
||||
function edit() {
|
||||
router.push(`/gallery/${post.value.id}/edit`);
|
||||
router.push('/gallery/:postId/edit', {
|
||||
params: {
|
||||
postId: props.postId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function reportAbuse() {
|
||||
|
|
|
@ -45,11 +45,20 @@ function fetch() {
|
|||
promise = misskeyApi('ap/show', {
|
||||
uri,
|
||||
});
|
||||
|
||||
promise.then(res => {
|
||||
if (res.type === 'User') {
|
||||
mainRouter.replace(res.object.host ? `/@${res.object.username}@${res.object.host}` : `/@${res.object.username}`);
|
||||
mainRouter.replace('/@:acct/:page?', {
|
||||
params: {
|
||||
acct: res.host != null ? `${res.object.username}@${res.object.host}` : res.object.username,
|
||||
}
|
||||
});
|
||||
} else if (res.type === 'Note') {
|
||||
mainRouter.replace(`/notes/${res.object.id}`);
|
||||
mainRouter.replace('/notes/:noteId/:initialTab?', {
|
||||
params: {
|
||||
noteId: res.object.id,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
|
@ -63,7 +72,11 @@ function fetch() {
|
|||
}
|
||||
promise = misskeyApi('users/show', Misskey.acct.parse(uri));
|
||||
promise.then(user => {
|
||||
mainRouter.replace(user.host ? `/@${user.username}@${user.host}` : `/@${user.username}`);
|
||||
mainRouter.replace('/@:acct/:page?', {
|
||||
params: {
|
||||
acct: user.host != null ? `${user.username}@${user.host}` : user.username,
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -154,7 +154,11 @@ async function save() {
|
|||
|
||||
pageId.value = created.id;
|
||||
currentName.value = name.value.trim();
|
||||
mainRouter.replace(`/pages/edit/${pageId.value}`);
|
||||
mainRouter.replace('/pages/edit/:initPageId', {
|
||||
params: {
|
||||
initPageId: pageId.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -189,7 +193,11 @@ async function duplicate() {
|
|||
pageId.value = created.id;
|
||||
currentName.value = name.value.trim();
|
||||
|
||||
mainRouter.push(`/pages/edit/${pageId.value}`);
|
||||
mainRouter.push('/pages/edit/:initPageId', {
|
||||
params: {
|
||||
initPageId: pageId.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function add() {
|
||||
|
|
|
@ -267,7 +267,11 @@ function showMenu(ev: MouseEvent) {
|
|||
menuItems.push({
|
||||
icon: 'ti ti-pencil',
|
||||
text: i18n.ts.edit,
|
||||
action: () => router.push(`/pages/edit/${page.value.id}`),
|
||||
action: () => router.push('/pages/edit/:initPageId', {
|
||||
params: {
|
||||
initPageId: page.value!.id,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if ($i.pinnedPageId === page.value.id) {
|
||||
|
|
|
@ -168,7 +168,11 @@ function startGame(game: Misskey.entities.ReversiGameDetailed) {
|
|||
playbackRate: 1,
|
||||
});
|
||||
|
||||
router.push(`/reversi/g/${game.id}`);
|
||||
router.push('/reversi/g/:gameId', {
|
||||
params: {
|
||||
gameId: game.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function matchHeatbeat() {
|
||||
|
|
|
@ -264,10 +264,18 @@ async function search() {
|
|||
const res = await apLookup(searchParams.value.query);
|
||||
|
||||
if (res.type === 'User') {
|
||||
router.push(`/@${res.object.username}@${res.object.host}`);
|
||||
router.push('/@:acct/:page?', {
|
||||
params: {
|
||||
acct: `${res.object.username}@${res.object.host}`,
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
} else if (res.type === 'Note') {
|
||||
router.push(`/notes/${res.object.id}`);
|
||||
router.push('/notes/:noteId/:initialTab?', {
|
||||
params: {
|
||||
noteId: res.object.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -282,7 +290,7 @@ async function search() {
|
|||
text: i18n.ts.lookupConfirm,
|
||||
});
|
||||
if (!confirm.canceled) {
|
||||
router.push(`/${searchParams.value.query}`);
|
||||
router.pushByPath(`/${searchParams.value.query}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -293,7 +301,11 @@ async function search() {
|
|||
text: i18n.ts.openTagPageConfirm,
|
||||
});
|
||||
if (!confirm.canceled) {
|
||||
router.push(`/tags/${encodeURIComponent(searchParams.value.query.substring(1))}`);
|
||||
router.push('/tags/:tag', {
|
||||
params: {
|
||||
tag: searchParams.value.query.substring(1),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,10 +77,18 @@ async function search() {
|
|||
const res = await promise;
|
||||
|
||||
if (res.type === 'User') {
|
||||
router.push(`/@${res.object.username}@${res.object.host}`);
|
||||
router.push('/@:acct/:page?', {
|
||||
params: {
|
||||
acct: `${res.object.username}@${res.object.host}`,
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
} else if (res.type === 'Note') {
|
||||
router.push(`/notes/${res.object.id}`);
|
||||
router.push('/notes/:noteId/:initialTab?', {
|
||||
params: {
|
||||
noteId: res.object.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -95,7 +103,7 @@ async function search() {
|
|||
text: i18n.ts.lookupConfirm,
|
||||
});
|
||||
if (!confirm.canceled) {
|
||||
router.push(`/${query}`);
|
||||
router.pushByPath(`/${query}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +114,11 @@ async function search() {
|
|||
text: i18n.ts.openTagPageConfirm,
|
||||
});
|
||||
if (!confirm.canceled) {
|
||||
router.push(`/user-tags/${encodeURIComponent(query.substring(1))}`);
|
||||
router.push('/user-tags/:tag', {
|
||||
params: {
|
||||
tag: query.substring(1),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,7 +135,7 @@ async function del(): Promise<void> {
|
|||
webhookId: props.webhookId,
|
||||
});
|
||||
|
||||
router.push('/settings/webhook');
|
||||
router.push('/settings/connect');
|
||||
}
|
||||
|
||||
async function test(type: Misskey.entities.UserWebhook['on'][number]): Promise<void> {
|
||||
|
|
|
@ -42,7 +42,11 @@ watch(() => props.listId, async () => {
|
|||
}, { immediate: true });
|
||||
|
||||
function settings() {
|
||||
router.push(`/my/lists/${props.listId}`);
|
||||
router.push('/my/lists/:listId', {
|
||||
params: {
|
||||
listId: props.listId,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = computed(() => list.value ? [{
|
||||
|
|
|
@ -603,4 +603,4 @@ export const ROUTE_DEF = [{
|
|||
}, {
|
||||
path: '/:(*)',
|
||||
component: page(() => import('@/pages/not-found.vue')),
|
||||
}] satisfies RouteDef[];
|
||||
}] as const satisfies RouteDef[];
|
||||
|
|
|
@ -20,7 +20,7 @@ export function createRouter(fullPath: string): Router {
|
|||
export const mainRouter = createRouter(window.location.pathname + window.location.search + window.location.hash);
|
||||
|
||||
window.addEventListener('popstate', (event) => {
|
||||
mainRouter.replace(window.location.pathname + window.location.search + window.location.hash);
|
||||
mainRouter.replaceByPath(window.location.pathname + window.location.search + window.location.hash);
|
||||
});
|
||||
|
||||
mainRouter.addListener('push', ctx => {
|
||||
|
|
|
@ -43,7 +43,7 @@ export function swInject() {
|
|||
if (mainRouter.currentRoute.value.path === ev.data.url) {
|
||||
return window.scroll({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
return mainRouter.push(ev.data.url);
|
||||
return mainRouter.pushByPath(ev.data.url);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -158,7 +158,11 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
|||
icon: 'ti ti-user-exclamation',
|
||||
text: i18n.ts.moderation,
|
||||
action: () => {
|
||||
router.push(`/admin/user/${user.id}`);
|
||||
router.push('/admin/user/:userId', {
|
||||
params: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
}, { type: 'divider' });
|
||||
}
|
||||
|
@ -216,7 +220,12 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
|||
icon: 'ti ti-search',
|
||||
text: i18n.ts.searchThisUsersNotes,
|
||||
action: () => {
|
||||
router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`);
|
||||
router.push('/search', {
|
||||
query: {
|
||||
username: user.username,
|
||||
host: user.host ?? undefined,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,12 +19,16 @@ export async function lookup(router?: Router) {
|
|||
if (canceled || query.length <= 1) return;
|
||||
|
||||
if (query.startsWith('@') && !query.includes(' ')) {
|
||||
_router.push(`/${query}`);
|
||||
_router.pushByPath(`/${query}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (query.startsWith('#')) {
|
||||
_router.push(`/tags/${encodeURIComponent(query.substring(1))}`);
|
||||
_router.push('/tags/:tag', {
|
||||
params: {
|
||||
tag: query.substring(1),
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -32,9 +36,17 @@ export async function lookup(router?: Router) {
|
|||
const res = await apLookup(query);
|
||||
|
||||
if (res.type === 'User') {
|
||||
_router.push(`/@${res.object.username}@${res.object.host}`);
|
||||
_router.push('/@:acct/:page?', {
|
||||
params: {
|
||||
acct: `${res.object.username}@${res.object.host}`,
|
||||
},
|
||||
});
|
||||
} else if (res.type === 'Note') {
|
||||
_router.push(`/notes/${res.object.id}`);
|
||||
_router.push('/notes/:noteId/:initialTab?', {
|
||||
params: {
|
||||
noteId: res.object.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
|
@ -24,6 +24,7 @@ for (const item of generated) {
|
|||
const inline = rootMods.get(id);
|
||||
if (inline) {
|
||||
inline.parentId = item.id;
|
||||
inline.path = item.path;
|
||||
} else {
|
||||
console.log('[Settings Search Index] Failed to inline', id);
|
||||
}
|
||||
|
|
|
@ -18,5 +18,5 @@ onmessage = (event) => {
|
|||
|
||||
render(event.data.hash, canvas);
|
||||
const bitmap = canvas.transferToImageBitmap();
|
||||
postMessage({ id: event.data.id, bitmap });
|
||||
postMessage({ id: event.data.id, bitmap }, [bitmap]);
|
||||
};
|
||||
|
|
|
@ -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'>> & {
|
||||
files: [];
|
||||
fileIds: [];
|
||||
} & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
|
||||
} & NonNullableRecord<Pick<Note, 'renoteId'>> & Pick<Note, 'renote'>;
|
||||
|
||||
// @public (undocumented)
|
||||
type QueueCount = components['schemas']['QueueCount'];
|
||||
|
@ -3801,7 +3801,7 @@ type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['respons
|
|||
|
||||
// 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.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
|
||||
|
|
|
@ -33,7 +33,8 @@ export type PureRenote =
|
|||
& AllNullRecord<Pick<Note, 'text'>>
|
||||
& AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>>
|
||||
& { files: []; fileIds: []; }
|
||||
& NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
|
||||
& NonNullableRecord<Pick<Note, 'renoteId'>>
|
||||
& Pick<Note, 'renote'>; // リノート対象が削除された場合、renoteIdはあるがrenoteはnullになる
|
||||
|
||||
export type PageEvent = {
|
||||
pageId: Page['id'];
|
||||
|
|
|
@ -2,8 +2,8 @@ import type { Note, PureRenote } from './entities.js';
|
|||
|
||||
export function isPureRenote(note: Note): note is PureRenote {
|
||||
return (
|
||||
note.renote != null &&
|
||||
note.reply == null &&
|
||||
note.renoteId != null &&
|
||||
note.replyId == null &&
|
||||
note.text == null &&
|
||||
note.cw == null &&
|
||||
(note.fileIds == null || note.fileIds.length === 0) &&
|
||||
|
|
Loading…
Reference in New Issue