diff --git a/CHANGELOG.md b/CHANGELOG.md index bf98a44ac4..9210a669ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - 従来のWebsocket接続を行うモードはリアルタイムモードとして再定義されました - チャットなど、一部の機能は引き続き設定に関わらずWebsocket接続が行われます - Enhance: メモリ使用量を軽減しました +- Enhance: 画像の高品質なプレースホルダを無効化してパフォーマンスを向上させるオプションを追加 - Enhance: 招待されているが参加していないルームを開いたときに、招待を承認するかどうか尋ねるように - Enhance: リプライ元にアンケートがあることが表示されるように - Enhance: ノートのサーバー情報のデザインを改善・パフォーマンス向上 @@ -22,8 +23,10 @@ - Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正 ### Server +- Enhance: チャットルームの最大メンバー数を30人から50人に調整 - Enhance: ノートのレスポンスにアンケートが添付されているかどうかを示すフラグ`hasPoll`を追加 - Enhance: チャットルームのレスポンスに招待されているかどうかを示すフラグ`invitationExists`を追加 +- Enhance: レートリミットの計算方法を調整 (#13997) - Fix: チャットルームが削除された場合・チャットルームから抜けた場合に、未読状態が残り続けることがあるのを修正 - Fix: ユーザ除外アンテナをインポートできない問題を修正 - Fix: アンテナのセンシティブなチャンネルのノートを含むかどうかの情報がエクスポートされない問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index a49f7b4d35..afe99c6887 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5713,6 +5713,14 @@ export interface Locale extends ILocale { * アイコンをスクロールに追従させる */ "useStickyIcons": string; + /** + * 高品質な画像のプレースホルダを表示 + */ + "enableHighQualityImagePlaceholders": string; + /** + * UIのアニメーション + */ + "uiAnimations": string; /** * ナビゲーションバーに副ボタンを表示 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8b2d31f7cd..f1ffc19796 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1428,6 +1428,8 @@ _settings: makeEveryTextElementsSelectable: "全てのテキスト要素を選択可能にする" makeEveryTextElementsSelectable_description: "有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。" useStickyIcons: "アイコンをスクロールに追従させる" + enableHighQualityImagePlaceholders: "高品質な画像のプレースホルダを表示" + uiAnimations: "UIのアニメーション" showNavbarSubButtons: "ナビゲーションバーに副ボタンを表示" ifOn: "オンのとき" ifOff: "オフのとき" diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts index 2ceff341cc..4e81847a52 100644 --- a/packages/backend/src/core/ChatService.ts +++ b/packages/backend/src/core/ChatService.ts @@ -29,7 +29,7 @@ import { emojiRegex } from '@/misc/emoji-regex.js'; import { NotificationService } from '@/core/NotificationService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; -const MAX_ROOM_MEMBERS = 30; +const MAX_ROOM_MEMBERS = 50; const MAX_REACTIONS_PER_MESSAGE = 100; const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/; diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index a42fdaf730..7a4af407a3 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -326,19 +326,15 @@ export class ApiCallService implements OnApplicationShutdown { if (factor > 0) { // Rate limit - await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor).catch(err => { - if ('info' in err) { - // errはLimiter.LimiterInfoであることが期待される - throw new ApiError({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - httpStatusCode: 429, - }, err.info); - } else { - throw new TypeError('information must be a rate-limiter information.'); - } - }); + const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor); + if (rateLimit != null) { + throw new ApiError({ + message: 'Rate limit exceeded. Please try again later.', + code: 'RATE_LIMIT_EXCEEDED', + id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', + httpStatusCode: 429, + }, rateLimit.info); + } } } diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts index 52d73baa0a..58fd6c7d8b 100644 --- a/packages/backend/src/server/api/RateLimiterService.ts +++ b/packages/backend/src/server/api/RateLimiterService.ts @@ -12,6 +12,14 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import type { IEndpointMeta } from './endpoints.js'; +type RateLimitInfo = { + code: 'BRIEF_REQUEST_INTERVAL', + info: Limiter.LimiterInfo, +} | { + code: 'RATE_LIMIT_EXCEEDED', + info: Limiter.LimiterInfo, +} + @Injectable() export class RateLimiterService { private logger: Logger; @@ -31,77 +39,57 @@ export class RateLimiterService { } @bindThis - public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string, factor = 1) { - { - if (this.disabled) { - return Promise.resolve(); - } + private checkLimiter(options: Limiter.LimiterOption): Promise { + return new Promise((resolve, reject) => { + new Limiter(options).get((err, info) => { + if (err) { + return reject(err); + } + resolve(info); + }); + }); + } - // Short-term limit - const min = new Promise((ok, reject) => { - const minIntervalLimiter = new Limiter({ - id: `${actor}:${limitation.key}:min`, - duration: limitation.minInterval! * factor, - max: 1, - db: this.redisClient, - }); + @bindThis + public async limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string, factor = 1): Promise { + if (this.disabled) { + return null; + } - minIntervalLimiter.get((err, info) => { - if (err) { - return reject({ code: 'ERR', info }); - } - - this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); - - if (info.remaining === 0) { - return reject({ code: 'BRIEF_REQUEST_INTERVAL', info }); - } else { - if (hasLongTermLimit) { - return max.then(ok, reject); - } else { - return ok(); - } - } - }); + // Short-term limit + if (limitation.minInterval != null) { + const info = await this.checkLimiter({ + id: `${actor}:${limitation.key}:min`, + duration: limitation.minInterval * factor, + max: 1, + db: this.redisClient, }); - // Long term limit - const max = new Promise((ok, reject) => { - const limiter = new Limiter({ - id: `${actor}:${limitation.key}`, - duration: limitation.duration! * factor, - max: limitation.max! / factor, - db: this.redisClient, - }); + this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); - limiter.get((err, info) => { - if (err) { - return reject({ code: 'ERR', info }); - } - - this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); - - if (info.remaining === 0) { - return reject({ code: 'RATE_LIMIT_EXCEEDED', info }); - } else { - return ok(); - } - }); - }); - - const hasShortTermLimit = typeof limitation.minInterval === 'number'; - - const hasLongTermLimit = - typeof limitation.duration === 'number' && - typeof limitation.max === 'number'; - - if (hasShortTermLimit) { - return min; - } else if (hasLongTermLimit) { - return max; - } else { - return Promise.resolve(); + if (info.remaining === 0) { + // eslint-disable-next-line no-throw-literal + return { code: 'BRIEF_REQUEST_INTERVAL', info }; } } + + // Long term limit + if (limitation.duration != null && limitation.max != null) { + const info = await this.checkLimiter({ + id: `${actor}:${limitation.key}`, + duration: limitation.duration, + max: limitation.max / factor, + db: this.redisClient, + }); + + this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); + + if (info.remaining === 0) { + // eslint-disable-next-line no-throw-literal + return { code: 'RATE_LIMIT_EXCEEDED', info }; + } + } + + return null; } } diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 1d983ca4bc..3e889372d8 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -89,10 +89,9 @@ export class SigninApiService { return { error }; } - try { // not more than 1 attempt per second and not more than 10 attempts per hour - await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip)); - } catch (err) { + const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip)); + if (rateLimit != null) { reply.code(429); return { error: { diff --git a/packages/frontend-embed/src/components/EmMediaImage.vue b/packages/frontend-embed/src/components/EmMediaImage.vue index 2c96ce3215..94f0268da4 100644 --- a/packages/frontend-embed/src/components/EmMediaImage.vue +++ b/packages/frontend-embed/src/components/EmMediaImage.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only target="_blank" rel="noopener" > - import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; -import ImgWithBlurhash from '@/components/EmImgWithBlurhash.vue'; +import EmImgWithBlurhash from '@/components/EmImgWithBlurhash.vue'; import { i18n } from '@/i18n.js'; const props = withDefaults(defineProps<{ diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue index 6e0ae36880..88afdef114 100644 --- a/packages/frontend/src/components/MkDriveFileThumbnail.vue +++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue @@ -11,15 +11,24 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.large]: large, }]" > - + @@ -36,7 +45,8 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/packages/frontend/src/components/MkMarqueeText.vue b/packages/frontend/src/components/MkMarqueeText.vue new file mode 100644 index 0000000000..a2c365afe9 --- /dev/null +++ b/packages/frontend/src/components/MkMarqueeText.vue @@ -0,0 +1,89 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index bb42cbecf9..68ba7dfbe9 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only style: 'cursor: zoom-in;' }" > - - + +
diff --git a/packages/frontend/src/components/global/MkSystemIcon.vue b/packages/frontend/src/components/global/MkSystemIcon.vue index 66592da9c8..3454cdc9f2 100644 --- a/packages/frontend/src/components/global/MkSystemIcon.vue +++ b/packages/frontend/src/components/global/MkSystemIcon.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -87,14 +87,6 @@ const props = defineProps<{ transform: rotate(-90deg); } -.animCircleSuccess { - stroke-dasharray: var(--l); - stroke-dashoffset: var(--l); - animation: circleSuccess var(--duration, 0.5s) cubic-bezier(0,0,.25,1) 1 forwards; - animation-delay: var(--delay, 0s); - transform-origin: center; -} - .animFade { opacity: 0; animation: fade-in var(--duration, 0.5s) cubic-bezier(0,0,.25,1) 1 forwards; @@ -112,19 +104,6 @@ const props = defineProps<{ } } -@keyframes circleSuccess { - 0% { - stroke-dashoffset: var(--l); - opacity: 0; - transform: rotate(-90deg); - } - 100% { - stroke-dashoffset: 0; - opacity: 1; - transform: rotate(90deg); - } -} - @keyframes fade-in { 0% { opacity: 0; diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index 7c63c8c1ef..7916cc7834 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -390,6 +390,7 @@ const patrons = [ 'まゆつな空高', 'asata', 'ruru', + 'みりめい', ]; const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure')); diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue index c6732e7787..e0c13d6ffa 100644 --- a/packages/frontend/src/pages/settings/preferences.vue +++ b/packages/frontend/src/pages/settings/preferences.vue @@ -557,6 +557,15 @@ SPDX-License-Identifier: AGPL-3.0-only
+ + + + + + + + + @@ -575,6 +584,15 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + + + + @@ -808,6 +826,7 @@ const defaultFollowWithReplies = prefer.model('defaultFollowWithReplies'); const chatShowSenderName = prefer.model('chat.showSenderName'); const chatSendOnEnter = prefer.model('chat.sendOnEnter'); const useStickyIcons = prefer.model('useStickyIcons'); +const enableHighQualityImagePlaceholders = prefer.model('enableHighQualityImagePlaceholders'); const reduceAnimation = prefer.model('animation', v => !v, v => !v); const animatedMfm = prefer.model('animatedMfm'); const disableShowingAnimatedImages = prefer.model('disableShowingAnimatedImages'); @@ -866,6 +885,7 @@ watch([ enableSeasonalScreenEffect, chatShowSenderName, useStickyIcons, + enableHighQualityImagePlaceholders, keepScreenOn, contextMenu, fontSize, @@ -873,6 +893,7 @@ watch([ makeEveryTextElementsSelectable, enableHorizontalSwipe, enablePullToRefresh, + reduceAnimation, ], async () => { await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); }); diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue index d131c17340..c2cf937c71 100644 --- a/packages/frontend/src/pages/welcome.entrance.a.vue +++ b/packages/frontend/src/pages/welcome.entrance.a.vue @@ -17,13 +17,13 @@ SPDX-License-Identifier: AGPL-3.0-only
- + {{ instance.host }} - +
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import XTimeline from './welcome.timeline.vue'; -import MarqueeText from '@/components/MkMarquee.vue'; +import MkMarqueeText from '@/components/MkMarqueeText.vue'; import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue'; import misskeysvg from '/client-assets/misskey.svg'; import { misskeyApiGet } from '@/utility/misskey-api.js'; diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts index af4423c6a4..f6cd2c0cb9 100644 --- a/packages/frontend/src/preferences/def.ts +++ b/packages/frontend/src/preferences/def.ts @@ -202,6 +202,9 @@ export const PREF_DEF = { useStickyIcons: { default: true, }, + enableHighQualityImagePlaceholders: { + default: true, + }, showFixedPostForm: { default: false, }, diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue index 16e72fa227..7248e8826b 100644 --- a/packages/frontend/src/ui/_common_/statusbar-federation.vue +++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only :leaveToClass="$style.transition_change_leaveTo" mode="default" > - + @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +