Compare commits
10 Commits
multiple-r
...
featured-s
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f881465f4a | ||
![]() |
7246e6ff5b | ||
![]() |
e78110a5cd | ||
![]() |
6c5593d456 | ||
![]() |
621626aad3 | ||
![]() |
f4f55ef012 | ||
![]() |
2e8a1029a4 | ||
![]() |
b53ee54e4f | ||
![]() |
b708b27bc8 | ||
![]() |
9ce44b24b8 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,3 +1,15 @@
|
|||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### General
|
||||||
|
- Enhance: ハイライトからセンシティブなメディアを含むノートを除外するオプション
|
||||||
|
|
||||||
|
### Client
|
||||||
|
-
|
||||||
|
|
||||||
|
### Server
|
||||||
|
-
|
||||||
|
|
||||||
|
|
||||||
## 2024.8.0
|
## 2024.8.0
|
||||||
|
|
||||||
### General
|
### General
|
||||||
@@ -33,6 +45,8 @@
|
|||||||
- Fix: 無制限にストリーミングのチャンネルに接続できる問題を修正
|
- Fix: 無制限にストリーミングのチャンネルに接続できる問題を修正
|
||||||
- Fix: ベースロールのポリシーを変更した際にモデログに記録されないのを修正
|
- Fix: ベースロールのポリシーを変更した際にモデログに記録されないのを修正
|
||||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/700)
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/700)
|
||||||
|
- Fix: Prevent memory leak from memory caches (#14310)
|
||||||
|
- Fix: More reliable memory cache eviction (#14311)
|
||||||
|
|
||||||
## 2024.7.0
|
## 2024.7.0
|
||||||
|
|
||||||
|
@@ -2316,6 +2316,7 @@ _pages:
|
|||||||
eyeCatchingImageSet: "Set thumbnail"
|
eyeCatchingImageSet: "Set thumbnail"
|
||||||
eyeCatchingImageRemove: "Delete thumbnail"
|
eyeCatchingImageRemove: "Delete thumbnail"
|
||||||
chooseBlock: "Add a block"
|
chooseBlock: "Add a block"
|
||||||
|
enterSectionTitle: "Enter a section title"
|
||||||
selectType: "Select a type"
|
selectType: "Select a type"
|
||||||
contentBlocks: "Content"
|
contentBlocks: "Content"
|
||||||
inputBlocks: "Input"
|
inputBlocks: "Input"
|
||||||
@@ -2499,7 +2500,10 @@ _moderationLogTypes:
|
|||||||
createAbuseReportNotificationRecipient: "Create a recipient for abuse reports"
|
createAbuseReportNotificationRecipient: "Create a recipient for abuse reports"
|
||||||
updateAbuseReportNotificationRecipient: "Update recipients for abuse reports"
|
updateAbuseReportNotificationRecipient: "Update recipients for abuse reports"
|
||||||
deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports"
|
deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports"
|
||||||
|
deleteAccount: "Delete the account"
|
||||||
|
deletePage: "Delete the page"
|
||||||
deleteFlash: "Delete Play"
|
deleteFlash: "Delete Play"
|
||||||
|
deleteGalleryPost: "Delete the gallery post"
|
||||||
_fileViewer:
|
_fileViewer:
|
||||||
title: "File details"
|
title: "File details"
|
||||||
type: "File type"
|
type: "File type"
|
||||||
|
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
@@ -6686,10 +6686,6 @@ export interface Locale extends ILocale {
|
|||||||
* ノートのピン留めの最大数
|
* ノートのピン留めの最大数
|
||||||
*/
|
*/
|
||||||
"pinMax": string;
|
"pinMax": string;
|
||||||
/**
|
|
||||||
* 一つのノートに対する最大リアクション数
|
|
||||||
*/
|
|
||||||
"reactionsPerNoteLimit": string;
|
|
||||||
/**
|
/**
|
||||||
* アンテナの作成可能数
|
* アンテナの作成可能数
|
||||||
*/
|
*/
|
||||||
|
@@ -1728,7 +1728,6 @@ _role:
|
|||||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||||
canUpdateBioMedia: "アイコンとバナーの更新を許可"
|
canUpdateBioMedia: "アイコンとバナーの更新を許可"
|
||||||
pinMax: "ノートのピン留めの最大数"
|
pinMax: "ノートのピン留めの最大数"
|
||||||
reactionsPerNoteLimit: "一つのノートに対する最大リアクション数"
|
|
||||||
antennaMax: "アンテナの作成可能数"
|
antennaMax: "アンテナの作成可能数"
|
||||||
wordMuteMax: "ワードミュートの最大文字数"
|
wordMuteMax: "ワードミュートの最大文字数"
|
||||||
webhookMax: "Webhookの作成可能数"
|
webhookMax: "Webhookの作成可能数"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2024.8.0-rc.3",
|
"version": "2024.8.0",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class MultipleReactions1721117896543 {
|
|
||||||
name = 'MultipleReactions1721117896543';
|
|
||||||
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query('DROP INDEX "public"."IDX_ad0c221b25672daf2df320a817"');
|
|
||||||
await queryRunner.query('CREATE UNIQUE INDEX "IDX_a7751b74317122d11575bff31c" ON "note_reaction" ("userId", "noteId", "reaction") ');
|
|
||||||
await queryRunner.query('CREATE INDEX "IDX_ad0c221b25672daf2df320a817" ON "note_reaction" ("userId", "noteId") ');
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(queryRunner) {
|
|
||||||
await queryRunner.query('DROP INDEX "public"."IDX_ad0c221b25672daf2df320a817"');
|
|
||||||
await queryRunner.query('DROP INDEX "public"."IDX_a7751b74317122d11575bff31c"');
|
|
||||||
await queryRunner.query('CREATE UNIQUE INDEX "IDX_ad0c221b25672daf2df320a817" ON "note_reaction" ("userId", "noteId") ');
|
|
||||||
}
|
|
||||||
}
|
|
@@ -29,7 +29,7 @@ export class AvatarDecorationService implements OnApplicationShutdown {
|
|||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30);
|
this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30); // 30s
|
||||||
|
|
||||||
this.redisForSub.on('message', this.onMessage);
|
this.redisForSub.on('message', this.onMessage);
|
||||||
}
|
}
|
||||||
|
@@ -56,10 +56,10 @@ export class CacheService implements OnApplicationShutdown {
|
|||||||
) {
|
) {
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
//this.onMessage = this.onMessage.bind(this);
|
||||||
|
|
||||||
this.userByIdCache = new MemoryKVCache<MiUser>(Infinity);
|
this.userByIdCache = new MemoryKVCache<MiUser>(1000 * 60 * 5); // 5m
|
||||||
this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(Infinity);
|
this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(1000 * 60 * 5); // 5m
|
||||||
this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(Infinity);
|
this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(1000 * 60 * 5); // 5m
|
||||||
this.uriPersonCache = new MemoryKVCache<MiUser | null>(Infinity);
|
this.uriPersonCache = new MemoryKVCache<MiUser | null>(1000 * 60 * 5); // 5m
|
||||||
|
|
||||||
this.userProfileCache = new RedisKVCache<MiUserProfile>(this.redisClient, 'userProfile', {
|
this.userProfileCache = new RedisKVCache<MiUserProfile>(this.redisClient, 'userProfile', {
|
||||||
lifetime: 1000 * 60 * 30, // 30m
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
@@ -135,14 +135,14 @@ export class CacheService implements OnApplicationShutdown {
|
|||||||
if (user == null) {
|
if (user == null) {
|
||||||
this.userByIdCache.delete(body.id);
|
this.userByIdCache.delete(body.id);
|
||||||
this.localUserByIdCache.delete(body.id);
|
this.localUserByIdCache.delete(body.id);
|
||||||
for (const [k, v] of this.uriPersonCache.cache.entries()) {
|
for (const [k, v] of this.uriPersonCache.entries) {
|
||||||
if (v.value?.id === body.id) {
|
if (v.value?.id === body.id) {
|
||||||
this.uriPersonCache.delete(k);
|
this.uriPersonCache.delete(k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.userByIdCache.set(user.id, user);
|
this.userByIdCache.set(user.id, user);
|
||||||
for (const [k, v] of this.uriPersonCache.cache.entries()) {
|
for (const [k, v] of this.uriPersonCache.entries) {
|
||||||
if (v.value?.id === user.id) {
|
if (v.value?.id === user.id) {
|
||||||
this.uriPersonCache.set(k, user);
|
this.uriPersonCache.set(k, user);
|
||||||
}
|
}
|
||||||
|
@@ -24,7 +24,7 @@ const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CustomEmojiService implements OnApplicationShutdown {
|
export class CustomEmojiService implements OnApplicationShutdown {
|
||||||
private cache: MemoryKVCache<MiEmoji | null>;
|
private emojisCache: MemoryKVCache<MiEmoji | null>;
|
||||||
public localEmojisCache: RedisSingleCache<Map<string, MiEmoji>>;
|
public localEmojisCache: RedisSingleCache<Map<string, MiEmoji>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -40,7 +40,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12);
|
this.emojisCache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12); // 12h
|
||||||
|
|
||||||
this.localEmojisCache = new RedisSingleCache<Map<string, MiEmoji>>(this.redisClient, 'localEmojis', {
|
this.localEmojisCache = new RedisSingleCache<Map<string, MiEmoji>>(this.redisClient, 'localEmojis', {
|
||||||
lifetime: 1000 * 60 * 30, // 30m
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
@@ -334,7 +334,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
host,
|
host,
|
||||||
})) ?? null;
|
})) ?? null;
|
||||||
|
|
||||||
const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);
|
const emoji = await this.emojisCache.fetch(`${name} ${host}`, queryOrNull);
|
||||||
|
|
||||||
if (emoji == null) return null;
|
if (emoji == null) return null;
|
||||||
return emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
return emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
@@ -361,7 +361,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
|
public async prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
|
||||||
const notCachedEmojis = emojis.filter(emoji => this.cache.get(`${emoji.name} ${emoji.host}`) == null);
|
const notCachedEmojis = emojis.filter(emoji => this.emojisCache.get(`${emoji.name} ${emoji.host}`) == null);
|
||||||
const emojisQuery: any[] = [];
|
const emojisQuery: any[] = [];
|
||||||
const hosts = new Set(notCachedEmojis.map(e => e.host));
|
const hosts = new Set(notCachedEmojis.map(e => e.host));
|
||||||
for (const host of hosts) {
|
for (const host of hosts) {
|
||||||
@@ -376,7 +376,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
||||||
}) : [];
|
}) : [];
|
||||||
for (const emoji of _emojis) {
|
for (const emoji of _emojis) {
|
||||||
this.cache.set(`${emoji.name} ${emoji.host}`, emoji);
|
this.emojisCache.set(`${emoji.name} ${emoji.host}`, emoji);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +401,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.cache.dispose();
|
this.emojisCache.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@@ -174,12 +174,24 @@ export class ReactionService {
|
|||||||
reaction,
|
reaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Create reaction
|
||||||
try {
|
try {
|
||||||
await this.noteReactionsRepository.insert(record);
|
await this.noteReactionsRepository.insert(record);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isDuplicateKeyValueError(e)) {
|
if (isDuplicateKeyValueError(e)) {
|
||||||
// 同じリアクションがすでにされていたらエラー
|
const exists = await this.noteReactionsRepository.findOneByOrFail({
|
||||||
throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
|
noteId: note.id,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exists.reaction !== reaction) {
|
||||||
|
// 別のリアクションがすでにされていたら置き換える
|
||||||
|
await this.delete(user, note);
|
||||||
|
await this.noteReactionsRepository.insert(record);
|
||||||
|
} else {
|
||||||
|
// 同じリアクションがすでにされていたらエラー
|
||||||
|
throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@@ -274,12 +286,11 @@ export class ReactionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, _reaction?: string | null) {
|
public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote) {
|
||||||
// if already unreacted
|
// if already unreacted
|
||||||
const exist = await this.noteReactionsRepository.findOneBy({
|
const exist = await this.noteReactionsRepository.findOneBy({
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
reaction: _reaction ?? FALLBACK,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist == null) {
|
if (exist == null) {
|
||||||
|
@@ -35,7 +35,7 @@ export class RelayService {
|
|||||||
private createSystemUserService: CreateSystemUserService,
|
private createSystemUserService: CreateSystemUserService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
) {
|
) {
|
||||||
this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10);
|
this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10); // 10m
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@@ -49,7 +49,6 @@ export type RolePolicies = {
|
|||||||
alwaysMarkNsfw: boolean;
|
alwaysMarkNsfw: boolean;
|
||||||
canUpdateBioMedia: boolean;
|
canUpdateBioMedia: boolean;
|
||||||
pinLimit: number;
|
pinLimit: number;
|
||||||
reactionsPerNoteLimit: number;
|
|
||||||
antennaLimit: number;
|
antennaLimit: number;
|
||||||
wordMuteLimit: number;
|
wordMuteLimit: number;
|
||||||
webhookLimit: number;
|
webhookLimit: number;
|
||||||
@@ -79,7 +78,6 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||||||
alwaysMarkNsfw: false,
|
alwaysMarkNsfw: false,
|
||||||
canUpdateBioMedia: true,
|
canUpdateBioMedia: true,
|
||||||
pinLimit: 5,
|
pinLimit: 5,
|
||||||
reactionsPerNoteLimit: 1,
|
|
||||||
antennaLimit: 5,
|
antennaLimit: 5,
|
||||||
wordMuteLimit: 200,
|
wordMuteLimit: 200,
|
||||||
webhookLimit: 3,
|
webhookLimit: 3,
|
||||||
@@ -129,10 +127,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
) {
|
) {
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
|
||||||
|
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
|
||||||
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60 * 1);
|
|
||||||
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 60 * 1);
|
|
||||||
|
|
||||||
this.redisForSub.on('message', this.onMessage);
|
this.redisForSub.on('message', this.onMessage);
|
||||||
}
|
}
|
||||||
@@ -382,7 +378,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||||||
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
||||||
canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
|
canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
|
||||||
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
|
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
|
||||||
reactionsPerNoteLimit: calc('reactionsPerNoteLimit', vs => Math.max(...vs)),
|
|
||||||
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
|
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
|
||||||
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
|
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
|
||||||
webhookLimit: calc('webhookLimit', vs => Math.max(...vs)),
|
webhookLimit: calc('webhookLimit', vs => Math.max(...vs)),
|
||||||
|
@@ -25,7 +25,7 @@ export class UserKeypairService implements OnApplicationShutdown {
|
|||||||
) {
|
) {
|
||||||
this.cache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', {
|
this.cache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', {
|
||||||
lifetime: 1000 * 60 * 60 * 24, // 24h
|
lifetime: 1000 * 60 * 60 * 24, // 24h
|
||||||
memoryCacheLifetime: Infinity,
|
memoryCacheLifetime: 1000 * 60 * 60, // 1h
|
||||||
fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
|
fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
|
||||||
toRedisConverter: (value) => JSON.stringify(value),
|
toRedisConverter: (value) => JSON.stringify(value),
|
||||||
fromRedisConverter: (value) => JSON.parse(value),
|
fromRedisConverter: (value) => JSON.parse(value),
|
||||||
|
@@ -54,8 +54,8 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
|||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private apPersonService: ApPersonService,
|
private apPersonService: ApPersonService,
|
||||||
) {
|
) {
|
||||||
this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
|
this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
|
||||||
this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
|
this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@@ -170,10 +170,10 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async populateMyReaction(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: {
|
public async populateMyReaction(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: {
|
||||||
myReactionsMap: Map<MiNote['id'], string | null>;
|
myReactions: Map<MiNote['id'], string | null>;
|
||||||
}) {
|
}) {
|
||||||
if (_hint_?.myReactionsMap) {
|
if (_hint_?.myReactions) {
|
||||||
const reaction = _hint_.myReactionsMap.get(note.id);
|
const reaction = _hint_.myReactions.get(note.id);
|
||||||
if (reaction) {
|
if (reaction) {
|
||||||
return this.reactionService.convertLegacyReaction(reaction);
|
return this.reactionService.convertLegacyReaction(reaction);
|
||||||
} else {
|
} else {
|
||||||
|
@@ -7,23 +7,23 @@ import * as Redis from 'ioredis';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
export class RedisKVCache<T> {
|
export class RedisKVCache<T> {
|
||||||
private redisClient: Redis.Redis;
|
private readonly lifetime: number;
|
||||||
private name: string;
|
private readonly memoryCache: MemoryKVCache<T>;
|
||||||
private lifetime: number;
|
private readonly fetcher: (key: string) => Promise<T>;
|
||||||
private memoryCache: MemoryKVCache<T>;
|
private readonly toRedisConverter: (value: T) => string;
|
||||||
private fetcher: (key: string) => Promise<T>;
|
private readonly fromRedisConverter: (value: string) => T | undefined;
|
||||||
private toRedisConverter: (value: T) => string;
|
|
||||||
private fromRedisConverter: (value: string) => T | undefined;
|
|
||||||
|
|
||||||
constructor(redisClient: RedisKVCache<T>['redisClient'], name: RedisKVCache<T>['name'], opts: {
|
constructor(
|
||||||
lifetime: RedisKVCache<T>['lifetime'];
|
private redisClient: Redis.Redis,
|
||||||
memoryCacheLifetime: number;
|
private name: string,
|
||||||
fetcher: RedisKVCache<T>['fetcher'];
|
opts: {
|
||||||
toRedisConverter: RedisKVCache<T>['toRedisConverter'];
|
lifetime: RedisKVCache<T>['lifetime'];
|
||||||
fromRedisConverter: RedisKVCache<T>['fromRedisConverter'];
|
memoryCacheLifetime: number;
|
||||||
}) {
|
fetcher: RedisKVCache<T>['fetcher'];
|
||||||
this.redisClient = redisClient;
|
toRedisConverter: RedisKVCache<T>['toRedisConverter'];
|
||||||
this.name = name;
|
fromRedisConverter: RedisKVCache<T>['fromRedisConverter'];
|
||||||
|
},
|
||||||
|
) {
|
||||||
this.lifetime = opts.lifetime;
|
this.lifetime = opts.lifetime;
|
||||||
this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime);
|
this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime);
|
||||||
this.fetcher = opts.fetcher;
|
this.fetcher = opts.fetcher;
|
||||||
@@ -55,7 +55,13 @@ export class RedisKVCache<T> {
|
|||||||
|
|
||||||
const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`);
|
const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`);
|
||||||
if (cached == null) return undefined;
|
if (cached == null) return undefined;
|
||||||
return this.fromRedisConverter(cached);
|
|
||||||
|
const value = this.fromRedisConverter(cached);
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.memoryCache.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -66,6 +72,10 @@ export class RedisKVCache<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||||
|
* This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons:
|
||||||
|
* * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster.
|
||||||
|
* * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value.
|
||||||
|
* * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses.
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetch(key: string): Promise<T> {
|
public async fetch(key: string): Promise<T> {
|
||||||
@@ -77,14 +87,14 @@ export class RedisKVCache<T> {
|
|||||||
|
|
||||||
// Cache MISS
|
// Cache MISS
|
||||||
const value = await this.fetcher(key);
|
const value = await this.fetcher(key);
|
||||||
this.set(key, value);
|
await this.set(key, value);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async refresh(key: string) {
|
public async refresh(key: string) {
|
||||||
const value = await this.fetcher(key);
|
const value = await this.fetcher(key);
|
||||||
this.set(key, value);
|
await this.set(key, value);
|
||||||
|
|
||||||
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
|
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
|
||||||
}
|
}
|
||||||
@@ -101,23 +111,23 @@ export class RedisKVCache<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class RedisSingleCache<T> {
|
export class RedisSingleCache<T> {
|
||||||
private redisClient: Redis.Redis;
|
private readonly lifetime: number;
|
||||||
private name: string;
|
private readonly memoryCache: MemorySingleCache<T>;
|
||||||
private lifetime: number;
|
private readonly fetcher: () => Promise<T>;
|
||||||
private memoryCache: MemorySingleCache<T>;
|
private readonly toRedisConverter: (value: T) => string;
|
||||||
private fetcher: () => Promise<T>;
|
private readonly fromRedisConverter: (value: string) => T | undefined;
|
||||||
private toRedisConverter: (value: T) => string;
|
|
||||||
private fromRedisConverter: (value: string) => T | undefined;
|
|
||||||
|
|
||||||
constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: {
|
constructor(
|
||||||
lifetime: RedisSingleCache<T>['lifetime'];
|
private redisClient: Redis.Redis,
|
||||||
memoryCacheLifetime: number;
|
private name: string,
|
||||||
fetcher: RedisSingleCache<T>['fetcher'];
|
opts: {
|
||||||
toRedisConverter: RedisSingleCache<T>['toRedisConverter'];
|
lifetime: number;
|
||||||
fromRedisConverter: RedisSingleCache<T>['fromRedisConverter'];
|
memoryCacheLifetime: number;
|
||||||
}) {
|
fetcher: RedisSingleCache<T>['fetcher'];
|
||||||
this.redisClient = redisClient;
|
toRedisConverter: RedisSingleCache<T>['toRedisConverter'];
|
||||||
this.name = name;
|
fromRedisConverter: RedisSingleCache<T>['fromRedisConverter'];
|
||||||
|
},
|
||||||
|
) {
|
||||||
this.lifetime = opts.lifetime;
|
this.lifetime = opts.lifetime;
|
||||||
this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime);
|
this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime);
|
||||||
this.fetcher = opts.fetcher;
|
this.fetcher = opts.fetcher;
|
||||||
@@ -149,7 +159,13 @@ export class RedisSingleCache<T> {
|
|||||||
|
|
||||||
const cached = await this.redisClient.get(`singlecache:${this.name}`);
|
const cached = await this.redisClient.get(`singlecache:${this.name}`);
|
||||||
if (cached == null) return undefined;
|
if (cached == null) return undefined;
|
||||||
return this.fromRedisConverter(cached);
|
|
||||||
|
const value = this.fromRedisConverter(cached);
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.memoryCache.set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -160,6 +176,10 @@ export class RedisSingleCache<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||||
|
* This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons:
|
||||||
|
* * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster.
|
||||||
|
* * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value.
|
||||||
|
* * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses.
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetch(): Promise<T> {
|
public async fetch(): Promise<T> {
|
||||||
@@ -171,14 +191,14 @@ export class RedisSingleCache<T> {
|
|||||||
|
|
||||||
// Cache MISS
|
// Cache MISS
|
||||||
const value = await this.fetcher();
|
const value = await this.fetcher();
|
||||||
this.set(value);
|
await this.set(value);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async refresh() {
|
public async refresh() {
|
||||||
const value = await this.fetcher();
|
const value = await this.fetcher();
|
||||||
this.set(value);
|
await this.set(value);
|
||||||
|
|
||||||
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
|
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
|
||||||
}
|
}
|
||||||
@@ -187,22 +207,12 @@ export class RedisSingleCache<T> {
|
|||||||
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
||||||
|
|
||||||
export class MemoryKVCache<T> {
|
export class MemoryKVCache<T> {
|
||||||
/**
|
private readonly cache = new Map<string, { date: number; value: T; }>();
|
||||||
* データを持つマップ
|
private readonly gcIntervalHandle = setInterval(() => this.gc(), 1000 * 60 * 3); // 3m
|
||||||
* @deprecated これを直接操作するべきではない
|
|
||||||
*/
|
|
||||||
public cache: Map<string, { date: number; value: T; }>;
|
|
||||||
private lifetime: number;
|
|
||||||
private gcIntervalHandle: NodeJS.Timeout;
|
|
||||||
|
|
||||||
constructor(lifetime: MemoryKVCache<never>['lifetime']) {
|
constructor(
|
||||||
this.cache = new Map();
|
private readonly lifetime: number,
|
||||||
this.lifetime = lifetime;
|
) {}
|
||||||
|
|
||||||
this.gcIntervalHandle = setInterval(() => {
|
|
||||||
this.gc();
|
|
||||||
}, 1000 * 60 * 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
/**
|
/**
|
||||||
@@ -287,10 +297,14 @@ export class MemoryKVCache<T> {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public gc(): void {
|
public gc(): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
for (const [key, { date }] of this.cache.entries()) {
|
for (const [key, { date }] of this.cache.entries()) {
|
||||||
if ((now - date) > this.lifetime) {
|
// The map is ordered from oldest to youngest.
|
||||||
this.cache.delete(key);
|
// We can stop once we find an entry that's still active, because all following entries must *also* be active.
|
||||||
}
|
const age = now - date;
|
||||||
|
if (age < this.lifetime) break;
|
||||||
|
|
||||||
|
this.cache.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,16 +312,19 @@ export class MemoryKVCache<T> {
|
|||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
clearInterval(this.gcIntervalHandle);
|
clearInterval(this.gcIntervalHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get entries() {
|
||||||
|
return this.cache.entries();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MemorySingleCache<T> {
|
export class MemorySingleCache<T> {
|
||||||
private cachedAt: number | null = null;
|
private cachedAt: number | null = null;
|
||||||
private value: T | undefined;
|
private value: T | undefined;
|
||||||
private lifetime: number;
|
|
||||||
|
|
||||||
constructor(lifetime: MemorySingleCache<never>['lifetime']) {
|
constructor(
|
||||||
this.lifetime = lifetime;
|
private lifetime: number,
|
||||||
}
|
) {}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public set(value: T): void {
|
public set(value: T): void {
|
||||||
|
@@ -9,8 +9,7 @@ import { MiUser } from './User.js';
|
|||||||
import { MiNote } from './Note.js';
|
import { MiNote } from './Note.js';
|
||||||
|
|
||||||
@Entity('note_reaction')
|
@Entity('note_reaction')
|
||||||
@Index(['userId', 'noteId'])
|
@Index(['userId', 'noteId'], { unique: true })
|
||||||
@Index(['userId', 'noteId', 'reaction'], { unique: true })
|
|
||||||
export class MiNoteReaction {
|
export class MiNoteReaction {
|
||||||
@PrimaryColumn(id())
|
@PrimaryColumn(id())
|
||||||
public id: string;
|
public id: string;
|
||||||
|
@@ -236,10 +236,6 @@ export const packedRolePoliciesSchema = {
|
|||||||
type: 'integer',
|
type: 'integer',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
reactionsPerNoteLimit: {
|
|
||||||
type: 'integer',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
antennaLimit: {
|
antennaLimit: {
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@@ -45,7 +45,7 @@ export class DeliverProcessorService {
|
|||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
|
this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
|
||||||
this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60);
|
this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60); // 1h
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@@ -134,7 +134,7 @@ export class NodeinfoServerService {
|
|||||||
return document;
|
return document;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
|
const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10); // 10m
|
||||||
|
|
||||||
fastify.get(nodeinfo2_1path, async (request, reply) => {
|
fastify.get(nodeinfo2_1path, async (request, reply) => {
|
||||||
const base = await cache.fetch(() => nodeinfo2(21));
|
const base = await cache.fetch(() => nodeinfo2(21));
|
||||||
|
@@ -37,7 +37,7 @@ export class AuthenticateService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
) {
|
) {
|
||||||
this.appCache = new MemoryKVCache<MiApp>(Infinity);
|
this.appCache = new MemoryKVCache<MiApp>(1000 * 60 * 60 * 24 * 7); // 1w
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@@ -36,6 +36,7 @@ export const paramDef = {
|
|||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
channelId: { type: 'string', nullable: true, format: 'misskey:id' },
|
channelId: { type: 'string', nullable: true, format: 'misskey:id' },
|
||||||
|
withSensitive: { type: 'boolean', default: true },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
@@ -103,7 +104,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
|
|
||||||
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
|
|
||||||
return await this.noteEntityService.packMany(notes, me);
|
let packed = await this.noteEntityService.packMany(notes, me);
|
||||||
|
|
||||||
|
if (!ps.withSensitive) {
|
||||||
|
packed = packed.filter(note => note.files?.length === 0 || note.files?.every(file => !file.isSensitive));
|
||||||
|
}
|
||||||
|
|
||||||
|
return packed;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -89,7 +89,6 @@ export const ROLE_POLICIES = [
|
|||||||
'alwaysMarkNsfw',
|
'alwaysMarkNsfw',
|
||||||
'canUpdateBioMedia',
|
'canUpdateBioMedia',
|
||||||
'pinLimit',
|
'pinLimit',
|
||||||
'reactionsPerNoteLimit',
|
|
||||||
'antennaLimit',
|
'antennaLimit',
|
||||||
'wordMuteLimit',
|
'wordMuteLimit',
|
||||||
'webhookLimit',
|
'webhookLimit',
|
||||||
|
@@ -417,25 +417,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.reactionsPerNoteLimit, 'reactionsPerNoteLimit'])">
|
|
||||||
<template #label>{{ i18n.ts._role._options.reactionsPerNoteLimit }}</template>
|
|
||||||
<template #suffix>
|
|
||||||
<span v-if="role.policies.reactionsPerNoteLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
|
||||||
<span v-else>{{ role.policies.reactionsPerNoteLimit.value }}</span>
|
|
||||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.reactionsPerNoteLimit)"></i></span>
|
|
||||||
</template>
|
|
||||||
<div class="_gaps">
|
|
||||||
<MkSwitch v-model="role.policies.reactionsPerNoteLimit.useDefault" :readonly="readonly">
|
|
||||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
|
||||||
</MkSwitch>
|
|
||||||
<MkInput v-model="role.policies.reactionsPerNoteLimit.value" :disabled="role.policies.reactionsPerNoteLimit.useDefault" type="number" :readonly="readonly">
|
|
||||||
</MkInput>
|
|
||||||
<MkRange v-model="role.policies.reactionsPerNoteLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
|
||||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
|
||||||
</MkRange>
|
|
||||||
</div>
|
|
||||||
</MkFolder>
|
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])">
|
||||||
<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
|
<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
@@ -149,13 +149,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</MkInput>
|
</MkInput>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.reactionsPerNoteLimit, 'reactionsPerNoteLimit'])">
|
|
||||||
<template #label>{{ i18n.ts._role._options.reactionsPerNoteLimit }}</template>
|
|
||||||
<template #suffix>{{ policies.reactionsPerNoteLimit }}</template>
|
|
||||||
<MkInput v-model="policies.reactionsPerNoteLimit" type="number">
|
|
||||||
</MkInput>
|
|
||||||
</MkFolder>
|
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])">
|
||||||
<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
|
<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
|
||||||
<template #suffix>{{ policies.antennaLimit }}</template>
|
<template #suffix>{{ policies.antennaLimit }}</template>
|
||||||
|
@@ -7,9 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkSpacer :contentMax="800">
|
<MkSpacer :contentMax="800">
|
||||||
<MkTab v-model="tab" style="margin-bottom: var(--margin);">
|
<MkTab v-model="tab" style="margin-bottom: var(--margin);">
|
||||||
<option value="notes">{{ i18n.ts.notes }}</option>
|
<option value="notes">{{ i18n.ts.notes }}</option>
|
||||||
|
<option value="notesWithSensitive">{{ i18n.ts.notes }} (+{{ i18n.ts.sensitive }})</option>
|
||||||
<option value="polls">{{ i18n.ts.poll }}</option>
|
<option value="polls">{{ i18n.ts.poll }}</option>
|
||||||
</MkTab>
|
</MkTab>
|
||||||
<MkNotes v-if="tab === 'notes'" :pagination="paginationForNotes"/>
|
<MkNotes v-if="tab === 'notes'" :pagination="paginationForNotes"/>
|
||||||
|
<MkNotes v-else-if="tab === 'notesWithSensitive'" :pagination="paginationForNotesWithSensitive"/>
|
||||||
<MkNotes v-else-if="tab === 'polls'" :pagination="paginationForPolls"/>
|
<MkNotes v-else-if="tab === 'polls'" :pagination="paginationForPolls"/>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</template>
|
</template>
|
||||||
@@ -23,6 +25,17 @@ import { i18n } from '@/i18n.js';
|
|||||||
const paginationForNotes = {
|
const paginationForNotes = {
|
||||||
endpoint: 'notes/featured' as const,
|
endpoint: 'notes/featured' as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
params: {
|
||||||
|
withSensitive: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const paginationForNotesWithSensitive = {
|
||||||
|
endpoint: 'notes/featured' as const,
|
||||||
|
limit: 10,
|
||||||
|
params: {
|
||||||
|
withSensitive: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const paginationForPolls = {
|
const paginationForPolls = {
|
||||||
|
@@ -16,21 +16,57 @@ function containsFocusTrappedElements(el: HTMLElement): boolean {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getZIndex(el: HTMLElement): number {
|
||||||
|
const zIndex = parseInt(window.getComputedStyle(el).zIndex || '0', 10);
|
||||||
|
if (isNaN(zIndex)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return zIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHighestZIndexElement(): { el: HTMLElement; zIndex: number; } | null {
|
||||||
|
let highestZIndexElement: HTMLElement | null = null;
|
||||||
|
let highestZIndex = -Infinity;
|
||||||
|
|
||||||
|
focusTrapElements.forEach((el) => {
|
||||||
|
const zIndex = getZIndex(el);
|
||||||
|
if (zIndex > highestZIndex) {
|
||||||
|
highestZIndex = zIndex;
|
||||||
|
highestZIndexElement = el;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return highestZIndexElement == null ? null : {
|
||||||
|
el: highestZIndexElement,
|
||||||
|
zIndex: highestZIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function releaseFocusTrap(el: HTMLElement): void {
|
function releaseFocusTrap(el: HTMLElement): void {
|
||||||
focusTrapElements.delete(el);
|
focusTrapElements.delete(el);
|
||||||
if (el.inert === true) {
|
if (el.inert === true) {
|
||||||
el.inert = false;
|
el.inert = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const highestZIndexElement = getHighestZIndexElement();
|
||||||
|
|
||||||
if (el.parentElement != null && el !== document.body) {
|
if (el.parentElement != null && el !== document.body) {
|
||||||
el.parentElement.childNodes.forEach((siblingNode) => {
|
el.parentElement.childNodes.forEach((siblingNode) => {
|
||||||
const siblingEl = getHTMLElementOrNull(siblingNode);
|
const siblingEl = getHTMLElementOrNull(siblingNode);
|
||||||
if (!siblingEl) return;
|
if (!siblingEl) return;
|
||||||
if (siblingEl !== el && (focusTrapElements.has(siblingEl) || containsFocusTrappedElements(siblingEl) || focusTrapElements.size === 0)) {
|
if (
|
||||||
|
siblingEl !== el &&
|
||||||
|
(
|
||||||
|
highestZIndexElement == null ||
|
||||||
|
siblingEl === highestZIndexElement.el ||
|
||||||
|
siblingEl.contains(highestZIndexElement.el)
|
||||||
|
)
|
||||||
|
) {
|
||||||
siblingEl.inert = false;
|
siblingEl.inert = false;
|
||||||
} else if (
|
} else if (
|
||||||
focusTrapElements.size > 0 &&
|
highestZIndexElement != null &&
|
||||||
!containsFocusTrappedElements(siblingEl) &&
|
siblingEl !== highestZIndexElement.el &&
|
||||||
!focusTrapElements.has(siblingEl) &&
|
!siblingEl.contains(highestZIndexElement.el) &&
|
||||||
!ignoreElements.includes(siblingEl.tagName.toLowerCase())
|
!ignoreElements.includes(siblingEl.tagName.toLowerCase())
|
||||||
) {
|
) {
|
||||||
siblingEl.inert = true;
|
siblingEl.inert = true;
|
||||||
@@ -45,9 +81,29 @@ function releaseFocusTrap(el: HTMLElement): void {
|
|||||||
export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls: boolean, parent: true): void;
|
export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls: boolean, parent: true): void;
|
||||||
export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls?: boolean, parent?: false): { release: () => void; };
|
export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls?: boolean, parent?: false): { release: () => void; };
|
||||||
export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls = false, parent = false): { release: () => void; } | void {
|
export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls = false, parent = false): { release: () => void; } | void {
|
||||||
|
const highestZIndexElement = getHighestZIndexElement();
|
||||||
|
|
||||||
|
const highestZIndex = highestZIndexElement == null ? -Infinity : highestZIndexElement.zIndex;
|
||||||
|
const zIndex = getZIndex(el);
|
||||||
|
|
||||||
|
// If the element has a lower z-index than the highest z-index element, focus trap the highest z-index element instead
|
||||||
|
// Focus trapping for this element will be done in the release function
|
||||||
|
if (!parent && zIndex < highestZIndex) {
|
||||||
|
focusTrapElements.add(el);
|
||||||
|
if (highestZIndexElement) {
|
||||||
|
focusTrap(highestZIndexElement.el, hasInteractionWithOtherFocusTrappedEls);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
release: () => {
|
||||||
|
releaseFocusTrap(el);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (el.inert === true) {
|
if (el.inert === true) {
|
||||||
el.inert = false;
|
el.inert = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.parentElement != null && el !== document.body) {
|
if (el.parentElement != null && el !== document.body) {
|
||||||
el.parentElement.childNodes.forEach((siblingNode) => {
|
el.parentElement.childNodes.forEach((siblingNode) => {
|
||||||
const siblingEl = getHTMLElementOrNull(siblingNode);
|
const siblingEl = getHTMLElementOrNull(siblingNode);
|
||||||
|
@@ -89,7 +89,6 @@
|
|||||||
X11: 'rgba(0, 0, 0, 0.3)',
|
X11: 'rgba(0, 0, 0, 0.3)',
|
||||||
X12: 'rgba(255, 255, 255, 0.1)',
|
X12: 'rgba(255, 255, 255, 0.1)',
|
||||||
X13: 'rgba(255, 255, 255, 0.15)',
|
X13: 'rgba(255, 255, 255, 0.15)',
|
||||||
X14: ':alpha<0.5<@navBg',
|
|
||||||
X15: ':alpha<0<@panel',
|
X15: ':alpha<0<@panel',
|
||||||
X16: ':alpha<0.7<@panel',
|
X16: ':alpha<0.7<@panel',
|
||||||
X17: ':alpha<0.8<@bg',
|
X17: ':alpha<0.8<@bg',
|
||||||
|
@@ -89,7 +89,6 @@
|
|||||||
X11: 'rgba(0, 0, 0, 0.1)',
|
X11: 'rgba(0, 0, 0, 0.1)',
|
||||||
X12: 'rgba(0, 0, 0, 0.1)',
|
X12: 'rgba(0, 0, 0, 0.1)',
|
||||||
X13: 'rgba(0, 0, 0, 0.15)',
|
X13: 'rgba(0, 0, 0, 0.15)',
|
||||||
X14: ':alpha<0.5<@navBg',
|
|
||||||
X15: ':alpha<0<@panel',
|
X15: ':alpha<0<@panel',
|
||||||
X16: ':alpha<0.7<@panel',
|
X16: ':alpha<0.7<@panel',
|
||||||
X17: ':alpha<0.8<@bg',
|
X17: ':alpha<0.8<@bg',
|
||||||
|
@@ -82,6 +82,8 @@ function more() {
|
|||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.root {
|
||||||
|
--nav-bg-transparent: color-mix(in srgb, var(--navBg), transparent 50%);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -91,7 +93,7 @@ function more() {
|
|||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
background: var(--X14);
|
background: var(--nav-bg-transparent);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
backdrop-filter: var(--blur, blur(8px));
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
}
|
}
|
||||||
@@ -125,7 +127,7 @@ function more() {
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
background: var(--X14);
|
background: var(--nav-bg-transparent);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
backdrop-filter: var(--blur, blur(8px));
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
}
|
}
|
||||||
|
@@ -111,6 +111,7 @@ function more(ev: MouseEvent) {
|
|||||||
.root {
|
.root {
|
||||||
--nav-width: 250px;
|
--nav-width: 250px;
|
||||||
--nav-icon-only-width: 80px;
|
--nav-icon-only-width: 80px;
|
||||||
|
--nav-bg-transparent: color-mix(in srgb, var(--navBg), transparent 50%);
|
||||||
|
|
||||||
flex: 0 0 var(--nav-width);
|
flex: 0 0 var(--nav-width);
|
||||||
width: var(--nav-width);
|
width: var(--nav-width);
|
||||||
@@ -144,7 +145,7 @@ function more(ev: MouseEvent) {
|
|||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
background: var(--X14);
|
background: var(--nav-bg-transparent);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
backdrop-filter: var(--blur, blur(8px));
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
}
|
}
|
||||||
@@ -187,7 +188,7 @@ function more(ev: MouseEvent) {
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
background: var(--X14);
|
background: var(--nav-bg-transparent);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
backdrop-filter: var(--blur, blur(8px));
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
}
|
}
|
||||||
@@ -378,7 +379,7 @@ function more(ev: MouseEvent) {
|
|||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
background: var(--X14);
|
background: var(--nav-bg-transparent);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
backdrop-filter: var(--blur, blur(8px));
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
}
|
}
|
||||||
@@ -408,7 +409,7 @@ function more(ev: MouseEvent) {
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
background: var(--X14);
|
background: var(--nav-bg-transparent);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
backdrop-filter: var(--blur, blur(8px));
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "misskey-js",
|
"name": "misskey-js",
|
||||||
"version": "2024.8.0-rc.3",
|
"version": "2024.8.0",
|
||||||
"description": "Misskey SDK for JavaScript",
|
"description": "Misskey SDK for JavaScript",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
|
Reference in New Issue
Block a user