diff --git a/locales/index.d.ts b/locales/index.d.ts index ea9b85fab7fe..3d1c30739ab5 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -10328,6 +10328,18 @@ export interface Locale extends ILocale { * いつでも花びらを降らせる */ "flowerEffect": string; + /** + * 激しい動きあり + */ + "hasMovement": string; + /** + * 激しい動きを含むカスタム絵文字のアニメーションだけを止める + */ + "stopAnimatingEmojisWithMovement": string; + /** + * 激しい動きを含むとモデレーターが判断したものだけ、アニメーションを停止します。その他の絵文字(動きがゆるいもの等)は通常通りアニメーションされます。絵文字のアニメーションを完全に停止させたい場合は、「アニメーション画像を再生しない」を利用してください。 + */ + "stopAnimatingEmojisWithMovementDescription": string; "_inDevelopment": { /** * この機能は開発中です diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index df5517465033..9aeb66adb49d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2754,6 +2754,9 @@ _hana: hanaModeShort: "はな" hanaModeTutorialDescription: "はなみすきーの主要な独自機能として「はなモード」があります。はなモードを有効にするかどうかで、はなみすきーでのSNS体験は大きく変わってきます。以下に主な違いとおすすめのユースケースを挙げますので、どちらか選択して進んでください。" flowerEffect: "いつでも花びらを降らせる" + hasMovement: "激しい動きあり" + stopAnimatingEmojisWithMovement: "激しい動きを含むカスタム絵文字のアニメーションだけを止める" + stopAnimatingEmojisWithMovementDescription: "激しい動きを含むとモデレーターが判断したものだけ、アニメーションを停止します。その他の絵文字(動きがゆるいもの等)は通常通りアニメーションされます。絵文字のアニメーションを完全に停止させたい場合は、「アニメーション画像を再生しない」を利用してください。" _inDevelopment: title: "この機能は開発中です" description: "はなみすきーは新規機能盛りだくさんで鋭意開発中です!\nどんな機能が実装されるかはお楽しみ。" diff --git a/packages/backend/migration/1723367092501-EmojiHasMovement.js b/packages/backend/migration/1723367092501-EmojiHasMovement.js new file mode 100644 index 000000000000..67175ac12b8c --- /dev/null +++ b/packages/backend/migration/1723367092501-EmojiHasMovement.js @@ -0,0 +1,11 @@ +export class EmojiHasMovement1723367092501 { + name = 'EmojiHasMovement1723367092501' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" ADD "hasMovement" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "hasMovement"`); + } +} diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 5db3c5b98035..44ea0a2ec530 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -67,6 +67,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive: boolean; localOnly: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][]; + hasMovement: boolean; }, moderator?: MiUser): Promise { const emoji = await this.emojisRepository.insertOne({ id: this.idService.gen(), @@ -82,6 +83,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive: data.isSensitive, localOnly: data.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction, + hasMovement: data.hasMovement, }); if (data.host == null) { @@ -112,6 +114,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive?: boolean; localOnly?: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][]; + hasMovement?: boolean; }, moderator?: MiUser): Promise { const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() }); @@ -129,6 +132,7 @@ export class CustomEmojiService implements OnApplicationShutdown { publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined, + hasMovement: data.hasMovement, }); this.localEmojisCache.refresh(); diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 841bd731c0cb..6a02c91f539f 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -34,6 +34,8 @@ export class EmojiEntityService { localOnly: emoji.localOnly ? true : undefined, isSensitive: emoji.isSensitive ? true : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, + + hasMovement: emoji.hasMovement, }; } @@ -62,6 +64,8 @@ export class EmojiEntityService { isSensitive: emoji.isSensitive, localOnly: emoji.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, + + hasMovement: emoji.hasMovement, }; } diff --git a/packages/backend/src/models/Emoji.ts b/packages/backend/src/models/Emoji.ts index d62b6e9f6f19..903b46d60801 100644 --- a/packages/backend/src/models/Emoji.ts +++ b/packages/backend/src/models/Emoji.ts @@ -81,4 +81,9 @@ export class MiEmoji { array: true, length: 128, default: '{}', }) public roleIdsThatCanBeUsedThisEmojiAsReaction: string[]; + + @Column('boolean', { + default: false, + }) + public hasMovement: boolean; } diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index 62686ad5ae62..d3737ffde0ee 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -44,6 +44,11 @@ export const packedEmojiSimpleSchema = { format: 'id', }, }, + + hasMovement: { + type: 'boolean', + optional: true, nullable: false, + }, }, } as const; @@ -102,5 +107,10 @@ export const packedEmojiDetailedSchema = { format: 'id', }, }, + + hasMovement: { + type: 'boolean', + optional: true, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 171809d25c3a..60169a610376 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -103,6 +103,7 @@ export class ImportCustomEmojisProcessorService { isSensitive: emojiInfo.isSensitive, localOnly: emojiInfo.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: [], + hasMovement: emojiInfo.hasMovement, }); } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 796f273330fb..65415cdff5e4 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -56,6 +56,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { type: 'string', } }, + hasMovement: { type: 'boolean' }, }, required: ['name', 'fileId'], } as const; @@ -88,6 +89,7 @@ export default class extends Endpoint { // eslint- isSensitive: ps.isSensitive ?? false, localOnly: ps.localOnly ?? false, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], + hasMovement: ps.hasMovement ?? false, }, me); return this.emojiEntityService.packDetailed(emoji); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index 975f892df9b6..5249c2289531 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -95,6 +95,7 @@ export default class extends Endpoint { // eslint- isSensitive: emoji.isSensitive, localOnly: emoji.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, + hasMovement: emoji.hasMovement, }, me); return this.emojiEntityService.packDetailed(addedEmoji); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 22609a16a39a..4bf9ec3d37e7 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -56,6 +56,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { type: 'string', } }, + hasMovement: { type: 'boolean' }, }, anyOf: [ { required: ['id'] }, @@ -103,6 +104,7 @@ export default class extends Endpoint { // eslint- isSensitive: ps.isSensitive, localOnly: ps.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, + hasMovement: ps.hasMovement, }, me); }); } diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue index c7f128872929..62a1301f3370 100644 --- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue +++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue @@ -39,6 +39,10 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index dff56cd7f0d7..09c00baf7f05 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, inject, ref } from 'vue'; import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js'; import { defaultStore } from '@/store.js'; +import { hanaStore } from '@/hana/store.js'; import { customEmojisMap } from '@/custom-emojis.js'; import * as os from '@/os.js'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; @@ -63,6 +64,13 @@ const rawUrl = computed(() => { return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`; }); +const shouldStopAnimatingByHanaHasMovement = computed(() => { + return ( + hanaStore.reactiveState.stopAnimatingEmojisWithMovement.value && + customEmojisMap.get(customEmojiName.value)?.hasMovement + ); +}); + const url = computed(() => { if (rawUrl.value == null) return undefined; @@ -75,7 +83,7 @@ const url = computed(() => { false, true, ); - return defaultStore.reactiveState.disableShowingAnimatedImages.value + return (defaultStore.reactiveState.disableShowingAnimatedImages.value || shouldStopAnimatingByHanaHasMovement.value) ? getStaticImageUrl(proxied) : proxied; }); diff --git a/packages/frontend/src/hana/store.ts b/packages/frontend/src/hana/store.ts index a4c12f219416..83e1a02a5f1f 100644 --- a/packages/frontend/src/hana/store.ts +++ b/packages/frontend/src/hana/store.ts @@ -9,4 +9,8 @@ export const hanaStore = markRaw(new Storage('hanaMain', { where: 'device', default: false, }, + stopAnimatingEmojisWithMovement: { + where: 'device', + default: true, + }, })); diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index 853c1d6b0b5d..044a70d62099 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -68,6 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only isSensitive {{ i18n.ts.localOnly }} + {{ i18n.ts._hana.hasMovement }} {{ i18n.ts.delete }} @@ -108,6 +109,7 @@ const localOnly = ref(props.emoji ? props.emoji.localOnly : false); const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); const rolesThatCanBeUsedThisEmojiAsReaction = ref([]); const file = ref(); +const hasMovement = ref(props.emoji ? props.emoji.hasMovement : false); watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => { rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); @@ -153,6 +155,7 @@ async function done() { isSensitive: isSensitive.value, localOnly: localOnly.value, roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id), + hasMovement: hasMovement.value, }; if (file.value) { diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index b347e0caf0af..fdc7a1e76f09 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -138,6 +138,12 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._hana.flowerEffect }} {{ i18n.ts.useNativeUIForVideoAudioPlayer }} +
+ + + + +
@@ -328,6 +334,8 @@ const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSette const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu')); const flowerEffect = computed(hanaStore.makeGetterSetter('flowerEffect')); +const stopAnimatingEmojisWithMovement = computed(hanaStore.makeGetterSetter('stopAnimatingEmojisWithMovement')); + watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); miLocalStorage.removeItem('locale'); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 199c39184405..a510fa382cff 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4662,6 +4662,7 @@ export type components = { localOnly?: boolean; isSensitive?: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; + hasMovement?: boolean; }; EmojiDetailed: { /** Format: id */ @@ -4676,6 +4677,7 @@ export type components = { isSensitive: boolean; localOnly: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction: string[]; + hasMovement?: boolean; }; Flash: { /** @@ -6983,6 +6985,7 @@ export type operations = { isSensitive?: boolean; localOnly?: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; + hasMovement?: boolean; }; }; }; @@ -7613,6 +7616,7 @@ export type operations = { isSensitive?: boolean; localOnly?: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; + hasMovement?: boolean; }; }; };