diff --git a/locales/index.d.ts b/locales/index.d.ts index f8c4971655..bf24cf1d9c 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -500,6 +500,24 @@ export interface Locale extends ILocale { * リアクション */ "reactions": string; + /** + * このリアクションをつける権限がありません。 + */ + "reactionDenied": string; + "_reactionDeniedReason": { + /** + * 投稿者がセンシティブなリアクションを許可していないため、リアクションできません。 + */ + "isSensitive": string; + /** + * この絵文字はリモートから見られないように設定されているため、リアクションできません。 + */ + "localOnly": string; + /** + * この絵文字をリアクションとして使うにはロールが必要です。 + */ + "roleIdsThatCanBeUsedThisEmojiAsReaction": string; + }; /** * 絵文字ピッカー */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index cf45c13f75..dbf5d11ace 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -121,6 +121,11 @@ sensitive: "センシティブ" add: "追加" reaction: "リアクション" reactions: "リアクション" +reactionDenied: "このリアクションをつける権限がありません。" +_reactionDeniedReason: + isSensitive: "投稿者がセンシティブなリアクションを許可していないため、リアクションできません。" + localOnly: "この絵文字はリモートから見られないように設定されているため、リアクションできません。" + roleIdsThatCanBeUsedThisEmojiAsReaction: "この絵文字をリアクションとして使うにはロールが必要です。" emojiPicker: "絵文字ピッカー" pinnedEmojisForReactionSettingDescription: "リアクション時にピン留め表示する絵文字を設定できます" pinnedEmojisSettingDescription: "絵文字入力時にピン留め表示する絵文字を設定できます" diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 7ed8e51d39..e82632051d 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -193,6 +193,7 @@ import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { shouldCollapsed } from '@/scripts/collapsed.js'; +import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -380,7 +381,21 @@ function react(viaKeyboard = false): void { } } else { blur(); - reactionPicker.show(reactButton.value ?? null, reaction => { + reactionPicker.show(reactButton.value ?? null, async reaction => { + if (reaction.includes(':')) { + const permissions = checkReactionPermissions($i!, props.note, await misskeyApi('emoji', { + name: reaction.replace(/:/g, '').replace(/@\./, ''), + })); + if (!permissions.allowed) { + os.alert({ + type: "info", + title: i18n.ts.reactionDenied, + text: i18n.ts._reactionDeniedReason[permissions.deniedReason], + }); + return; + } + } + sound.playMisskeySfx('reaction'); if (props.mock) { diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index dd956b21ad..bca9ac3af2 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -228,6 +228,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkPagination, { type Paging } from '@/components/MkPagination.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; +import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; const props = defineProps<{ note: Misskey.entities.Note; @@ -385,7 +386,21 @@ function react(viaKeyboard = false): void { } } else { blur(); - reactionPicker.show(reactButton.value ?? null, reaction => { + reactionPicker.show(reactButton.value ?? null, async reaction => { + if (reaction.includes(':')) { + const permissions = checkReactionPermissions($i!, props.note, await misskeyApi('emoji', { + name: reaction.replace(/:/g, '').replace(/@\./, ''), + })); + if (!permissions.allowed) { + os.alert({ + type: "info", + title: i18n.ts.reactionDenied, + text: i18n.ts._reactionDeniedReason[permissions.deniedReason], + }); + return; + } + } + sound.playMisskeySfx('reaction'); misskeyApi('notes/reactions/create', { diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index ffbf62a45c..bd53767624 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -32,6 +32,7 @@ import { claimAchievement } from '@/scripts/achievements.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as sound from '@/scripts/sound.js'; +import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; const props = defineProps<{ reaction: string; @@ -48,12 +49,30 @@ const emit = defineEmits<{ const buttonEl = shallowRef(); -const canToggle = computed(() => !props.reaction.match(/@\w/) && $i); +const canToggle = computed(() => { + return $i && !props.reaction.match(/@\w/); +}); + +const canGetInfo = computed(() => { + return !props.reaction.match(/@\w/); +}); async function toggleReaction() { if (!canToggle.value) return; - // TODO: その絵文字を使う権限があるかどうか確認 + if (props.reaction.includes(':')) { + const permissions = checkReactionPermissions($i!, props.note, await misskeyApi('emoji', { + name: props.reaction.replace(/:/g, '').replace(/@\./, ''), + })); + if (!permissions.allowed) { + os.alert({ + type: "info", + title: i18n.ts.reactionDenied, + text: i18n.ts._reactionDeniedReason[permissions.deniedReason], + }); + return; + } + } const oldReaction = props.note.myReaction; if (oldReaction) { @@ -101,7 +120,7 @@ async function toggleReaction() { } async function menu(ev) { - if (!canToggle.value) return; + if (!canGetInfo.value) return; if (!props.reaction.includes(':')) return; os.popupMenu([{ text: i18n.ts.info, diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts new file mode 100644 index 0000000000..7efab35ebc --- /dev/null +++ b/packages/frontend/src/scripts/check-reaction-permissions.ts @@ -0,0 +1,19 @@ +import * as Misskey from 'misskey-js'; + +export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiDetailed): { + allowed: true; +} | { + allowed: false; + deniedReason: 'localOnly' | 'isSensitive' | 'roleIdsThatCanBeUsedThisEmojiAsReaction'; +} { + if (emoji.localOnly && note.user.host !== me.host) { + return { allowed: false, deniedReason: 'localOnly' } + }; + if (emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote')) { + return { allowed: false, deniedReason: 'isSensitive' } + } + if (!(emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id)))) { + return { allowed: false, deniedReason: 'roleIdsThatCanBeUsedThisEmojiAsReaction' } + } + return { allowed: true }; +}