Merge branch 'develop' into feat-12997

This commit is contained in:
かっこかり 2024-05-20 18:12:38 +09:00 committed by GitHub
commit e2c70bb6c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 105 additions and 29 deletions

View File

@ -17,6 +17,7 @@
- 「アカウントを見つけやすくする」が有効なユーザーか - 「アカウントを見つけやすくする」が有効なユーザーか
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正 - Fix: Play作成時に設定した公開範囲が機能していない問題を修正
- Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正 - Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正
- Fix: みつけるのアンケート欄にてチャンネルのアンケートが含まれてしまう問題を修正
### Client ### Client
- Feat: アップロードするファイルの名前をランダム文字列にできるように - Feat: アップロードするファイルの名前をランダム文字列にできるように
@ -80,6 +81,8 @@
- Fix: グローバルタイムラインで返信が表示されないことがある問題を修正 - Fix: グローバルタイムラインで返信が表示されないことがある問題を修正
- Fix: リノートをミュートしたユーザの投稿のリノートがミュートされる問題を修正 - Fix: リノートをミュートしたユーザの投稿のリノートがミュートされる問題を修正
- Fix: AP Link等は添付ファイル扱いしないようになど (#13754) - Fix: AP Link等は添付ファイル扱いしないようになど (#13754)
- Fix: FTTが有効かつsinceIdのみを指定した場合に帰って来るレスポンスが逆順である問題を修正
- Fix: `/i/notifications``includeTypes`か`excludeTypes`を指定しているとき、通知が存在するのに空配列を返すことがある問題を修正
## 2024.3.1 ## 2024.3.1

View File

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class ChannelIdDenormalizedForMiPoll1716129964060 {
name = 'ChannelIdDenormalizedForMiPoll1716129964060'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "poll" ADD "channelId" character varying(32)`);
await queryRunner.query(`COMMENT ON COLUMN "poll"."channelId" IS '[Denormalized]'`);
await queryRunner.query(`CREATE INDEX "IDX_c1240fcc9675946ea5d6c2860e" ON "poll" ("channelId") `);
await queryRunner.query(`UPDATE "poll" SET "channelId" = "note"."channelId" FROM "note" WHERE "poll"."noteId" = "note"."id"`);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_c1240fcc9675946ea5d6c2860e"`);
await queryRunner.query(`COMMENT ON COLUMN "poll"."channelId" IS '[Denormalized]'`);
await queryRunner.query(`ALTER TABLE "poll" DROP COLUMN "channelId"`);
}
}

View File

@ -61,8 +61,8 @@ export class FanoutTimelineEndpointService {
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える // 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]); if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
const shouldPrepend = ps.sinceId && !ps.untilId; const ascending = ps.sinceId && !ps.untilId;
const idCompare: (a: string, b: string) => number = shouldPrepend ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1; const idCompare: (a: string, b: string) => number = ascending ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1;
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId); const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
@ -142,9 +142,7 @@ export class FanoutTimelineEndpointService {
if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) { if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) {
// 十分Redisからとれた // 十分Redisからとれた
const result = redisTimeline.slice(0, ps.limit); return redisTimeline.slice(0, ps.limit);
if (shouldPrepend) result.reverse();
return result;
} }
} }
@ -152,8 +150,7 @@ export class FanoutTimelineEndpointService {
const remainingToRead = ps.limit - redisTimeline.length; const remainingToRead = ps.limit - redisTimeline.length;
let dbUntil: string | null; let dbUntil: string | null;
let dbSince: string | null; let dbSince: string | null;
if (shouldPrepend) { if (ascending) {
redisTimeline.reverse();
dbUntil = ps.untilId; dbUntil = ps.untilId;
dbSince = noteIds[noteIds.length - 1]; dbSince = noteIds[noteIds.length - 1];
} else { } else {
@ -161,7 +158,7 @@ export class FanoutTimelineEndpointService {
dbSince = ps.sinceId; dbSince = ps.sinceId;
} }
const gotFromDb = await ps.dbFallback(dbUntil, dbSince, remainingToRead); const gotFromDb = await ps.dbFallback(dbUntil, dbSince, remainingToRead);
return shouldPrepend ? [...gotFromDb, ...redisTimeline] : [...redisTimeline, ...gotFromDb]; return [...redisTimeline, ...gotFromDb];
} }
return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit); return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit);

View File

@ -473,6 +473,7 @@ export class NoteCreateService implements OnApplicationShutdown {
noteVisibility: insert.visibility, noteVisibility: insert.visibility,
userId: user.id, userId: user.id,
userHost: user.host, userHost: user.host,
channelId: insert.channelId,
}); });
await transactionalEntityManager.insert(MiPoll, poll); await transactionalEntityManager.insert(MiPoll, poll);

View File

@ -8,6 +8,7 @@ import { noteVisibilities } from '@/types.js';
import { id } from './util/id.js'; import { id } from './util/id.js';
import { MiNote } from './Note.js'; import { MiNote } from './Note.js';
import type { MiUser } from './User.js'; import type { MiUser } from './User.js';
import type { MiChannel } from "@/models/Channel.js";
@Entity('poll') @Entity('poll')
export class MiPoll { export class MiPoll {
@ -58,6 +59,14 @@ export class MiPoll {
comment: '[Denormalized]', comment: '[Denormalized]',
}) })
public userHost: string | null; public userHost: string | null;
@Index()
@Column({
...id(),
nullable: true,
comment: '[Denormalized]',
})
public channelId: MiChannel['id'] | null;
//#endregion //#endregion
constructor(data: Partial<MiPoll>) { constructor(data: Partial<MiPoll>) {

View File

@ -7,7 +7,7 @@ import { In } from 'typeorm';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js'; import type { NotesRepository } from '@/models/_.js';
import { obsoleteNotificationTypes, notificationTypes, FilterUnionByProperty } from '@/types.js'; import { FilterUnionByProperty, notificationTypes, obsoleteNotificationTypes } from '@/types.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteReadService } from '@/core/NoteReadService.js'; import { NoteReadService } from '@/core/NoteReadService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
@ -84,27 +84,51 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 let sinceTime = ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime().toString() : null;
const notificationsRes = await this.redisClient.xrevrange( let untilTime = ps.untilId ? this.idService.parse(ps.untilId).date.getTime().toString() : null;
`notificationTimeline:${me.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-',
'COUNT', limit);
if (notificationsRes.length === 0) { let notifications: MiNotification[];
return []; for (;;) {
} let notificationsRes: [id: string, fields: string[]][];
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[]; // sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
if (sinceTime && !untilTime) {
notificationsRes = await this.redisClient.xrange(
`notificationTimeline:${me.id}`,
'(' + sinceTime,
'+',
'COUNT', ps.limit);
} else {
notificationsRes = await this.redisClient.xrevrange(
`notificationTimeline:${me.id}`,
untilTime ? '(' + untilTime : '+',
sinceTime ? '(' + sinceTime : '-',
'COUNT', ps.limit);
}
if (includeTypes && includeTypes.length > 0) { if (notificationsRes.length === 0) {
notifications = notifications.filter(notification => includeTypes.includes(notification.type)); return [];
} else if (excludeTypes && excludeTypes.length > 0) { }
notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
}
if (notifications.length === 0) { notifications = notificationsRes.map(x => JSON.parse(x[1][1])) as MiNotification[];
return [];
if (includeTypes && includeTypes.length > 0) {
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
} else if (excludeTypes && excludeTypes.length > 0) {
notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
}
if (notifications.length !== 0) {
// 通知が1件以上ある場合は返す
break;
}
// フィルタしたことで通知が0件になった場合、次のページを取得する
if (ps.sinceId && !ps.untilId) {
sinceTime = notificationsRes[notificationsRes.length - 1][0];
} else {
untilTime = notificationsRes[notificationsRes.length - 1][0];
}
} }
// Mark all as read // Mark all as read

View File

@ -32,6 +32,7 @@ export const paramDef = {
properties: { properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 }, offset: { type: 'integer', default: 0 },
excludeChannels: { type: 'boolean', default: false },
}, },
required: [], required: [],
} as const; } as const;
@ -86,6 +87,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.setParameters(mutingQuery.getParameters()); query.setParameters(mutingQuery.getParameters());
//#endregion //#endregion
//#region exclude channels
if (ps.excludeChannels) {
query.andWhere('poll.channelId IS NULL');
}
//#endregion
const polls = await query const polls = await query
.orderBy('poll.noteId', 'DESC') .orderBy('poll.noteId', 'DESC')
.limit(ps.limit) .limit(ps.limit)

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous"> <link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous">
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous"> <link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@3.3.0/tabler-icons.min.css"> <link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@3.3.0/dist/tabler-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css"> <link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css">
<style> <style>
html { html {

View File

@ -276,8 +276,11 @@ const align = () => {
const onOpened = () => { const onOpened = () => {
emit('opened'); emit('opened');
// NOTE: Chromatic undefined
if (content.value == null) return;
// //
const el = content.value!.children[0]; const el = content.value.children[0];
el.addEventListener('mousedown', ev => { el.addEventListener('mousedown', ev => {
contentClicking = true; contentClicking = true;
window.addEventListener('mouseup', ev => { window.addEventListener('mouseup', ev => {

View File

@ -11,6 +11,10 @@ import { i18n } from '@/i18n.js';
let lock: Promise<undefined> | undefined; let lock: Promise<undefined> | undefined;
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const common = { const common = {
render(args) { render(args) {
return { return {
@ -43,6 +47,8 @@ const common = {
lock = new Promise(r => resolve = r); lock = new Promise(r => resolve = r);
try { try {
// NOTE: sleep しないと何故か落ちる
await sleep(100);
const canvas = within(canvasElement); const canvas = within(canvasElement);
const a = canvas.getByRole<HTMLAnchorElement>('link'); const a = canvas.getByRole<HTMLAnchorElement>('link');
// await expect(a.href).toMatch(/^https?:\/\/.*#test$/); // await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
@ -53,7 +59,7 @@ const common = {
const i = buttons[0]; const i = buttons[0];
await expect(i).toBeInTheDocument(); await expect(i).toBeInTheDocument();
await userEvent.click(i); await userEvent.click(i);
// await expect(canvasElement).toHaveTextContent(i18n.ts._ad.back); await expect(canvasElement).toHaveTextContent(i18n.ts._ad.back);
await expect(a).not.toBeInTheDocument(); await expect(a).not.toBeInTheDocument();
await expect(i).not.toBeInTheDocument(); await expect(i).not.toBeInTheDocument();
buttons = canvas.getAllByRole<HTMLButtonElement>('button'); buttons = canvas.getAllByRole<HTMLButtonElement>('button');

View File

@ -29,6 +29,9 @@ const paginationForPolls = {
endpoint: 'notes/polls/recommendation' as const, endpoint: 'notes/polls/recommendation' as const,
limit: 10, limit: 10,
offsetMode: true, offsetMode: true,
params: {
excludeChannels: true,
},
}; };
const tab = ref('notes'); const tab = ref('notes');

View File

@ -21022,6 +21022,8 @@ export type operations = {
limit?: number; limit?: number;
/** @default 0 */ /** @default 0 */
offset?: number; offset?: number;
/** @default false */
excludeChannels?: boolean;
}; };
}; };
}; };