Enhance(frontend): フロント側でもリアクション権限のチェックをするように (#13134)
* フロント側でもリアクション権限のチェックをするように * update CHANGELOG.md * lint fixes * remove unrelated diffs * deny -> reject denyは「(信用しないことを理由に)拒否する」という意味らしい * allow -> accept * EmojiSimpleにlocalOnlyを含めるように * リアクション権限のない絵文字は打てないように(ダイアログを出すのではなく) * regenerate type definitions * lint fix * remove unused locales * remove unnecessary async
This commit is contained in:
parent
edb39a089d
commit
74245df382
17 changed files with 53 additions and 16 deletions
|
@ -50,6 +50,10 @@
|
||||||
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
|
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
|
||||||
- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
|
- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
|
||||||
- Enhance: コードのシンタックスハイライトにテーマを適用できるように
|
- Enhance: コードのシンタックスハイライトにテーマを適用できるように
|
||||||
|
- Enhance: リアクション権限がない場合、ハートにフォールバックするのではなく、権限がないことをダイアログで表示するように
|
||||||
|
- リモートのユーザーにローカルのみのカスタム絵文字をリアクションしようとした場合
|
||||||
|
- センシティブなリアクションを認めていないユーザーにセンシティブなカスタム絵文字をリアクションしようとした場合
|
||||||
|
- ロールが必要な絵文字をリアクションしようとした場合
|
||||||
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||||
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
||||||
|
|
|
@ -31,6 +31,7 @@ export class EmojiEntityService {
|
||||||
category: emoji.category,
|
category: emoji.category,
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url: emoji.publicUrl || emoji.originalUrl,
|
url: emoji.publicUrl || emoji.originalUrl,
|
||||||
|
localOnly: emoji.localOnly ? true : undefined,
|
||||||
isSensitive: emoji.isSensitive ? true : undefined,
|
isSensitive: emoji.isSensitive ? true : undefined,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
|
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,6 +27,10 @@ export const packedEmojiSimpleSchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
localOnly: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
isSensitive: {
|
isSensitive: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
|
|
|
@ -118,6 +118,7 @@ import { i18n } from '@/i18n.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
|
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
|
@ -126,6 +127,7 @@ const props = withDefaults(defineProps<{
|
||||||
asDrawer?: boolean;
|
asDrawer?: boolean;
|
||||||
asWindow?: boolean;
|
asWindow?: boolean;
|
||||||
asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう
|
asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう
|
||||||
|
targetNote?: Misskey.entities.Note;
|
||||||
}>(), {
|
}>(), {
|
||||||
showPinned: true,
|
showPinned: true,
|
||||||
});
|
});
|
||||||
|
@ -340,7 +342,7 @@ watch(q, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
|
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
|
||||||
return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction?.includes(r.id)))) ?? false;
|
return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
|
|
|
@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:showPinned="showPinned"
|
:showPinned="showPinned"
|
||||||
:pinnedEmojis="pinnedEmojis"
|
:pinnedEmojis="pinnedEmojis"
|
||||||
:asReactionPicker="asReactionPicker"
|
:asReactionPicker="asReactionPicker"
|
||||||
|
:targetNote="targetNote"
|
||||||
:asDrawer="type === 'drawer'"
|
:asDrawer="type === 'drawer'"
|
||||||
:max-height="maxHeight"
|
:max-height="maxHeight"
|
||||||
@chosen="chosen"
|
@chosen="chosen"
|
||||||
|
@ -32,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import { shallowRef } from 'vue';
|
import { shallowRef } from 'vue';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
||||||
|
@ -43,6 +45,7 @@ const props = withDefaults(defineProps<{
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
pinnedEmojis?: string[],
|
pinnedEmojis?: string[],
|
||||||
asReactionPicker?: boolean;
|
asReactionPicker?: boolean;
|
||||||
|
targetNote?: Misskey.entities.Note;
|
||||||
choseAndClose?: boolean;
|
choseAndClose?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
manualShowing: null,
|
manualShowing: null,
|
||||||
|
|
|
@ -13,12 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:front="true"
|
:front="true"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" asWindow :class="$style.picker" @chosen="chosen"/>
|
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" :targetNote="targetNote" asWindow :class="$style.picker" @chosen="chosen"/>
|
||||||
</MkWindow>
|
</MkWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import MkWindow from '@/components/MkWindow.vue';
|
import MkWindow from '@/components/MkWindow.vue';
|
||||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ withDefaults(defineProps<{
|
||||||
src?: HTMLElement;
|
src?: HTMLElement;
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
asReactionPicker?: boolean;
|
asReactionPicker?: boolean;
|
||||||
|
targetNote?: Misskey.entities.Note
|
||||||
}>(), {
|
}>(), {
|
||||||
showPinned: true,
|
showPinned: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -385,7 +385,7 @@ function react(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value ?? null, reaction => {
|
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
|
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
|
|
|
@ -385,7 +385,7 @@ function react(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value ?? null, reaction => {
|
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
|
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
|
|
|
@ -32,6 +32,8 @@ import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
|
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
|
||||||
|
import { customEmojis } from '@/custom-emojis.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reaction: string;
|
reaction: string;
|
||||||
|
@ -48,13 +50,19 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const buttonEl = shallowRef<HTMLElement>();
|
const buttonEl = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
const isCustomEmoji = computed(() => props.reaction.includes(':'));
|
||||||
|
const emoji = computed(() => isCustomEmoji.value ? customEmojis.value.find(emoji => emoji.name === props.reaction.replace(/:/g, '').replace(/@\./, '')) : null);
|
||||||
|
|
||||||
|
const canToggle = computed(() => {
|
||||||
|
return !props.reaction.match(/@\w/) && $i
|
||||||
|
&& (emoji.value && checkReactionPermissions($i, props.note, emoji.value))
|
||||||
|
|| !isCustomEmoji.value;
|
||||||
|
});
|
||||||
|
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
|
||||||
|
|
||||||
async function toggleReaction() {
|
async function toggleReaction() {
|
||||||
if (!canToggle.value) return;
|
if (!canToggle.value) return;
|
||||||
|
|
||||||
// TODO: その絵文字を使う権限があるかどうか確認
|
|
||||||
|
|
||||||
const oldReaction = props.note.myReaction;
|
const oldReaction = props.note.myReaction;
|
||||||
if (oldReaction) {
|
if (oldReaction) {
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
|
@ -101,8 +109,8 @@ async function toggleReaction() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function menu(ev) {
|
async function menu(ev) {
|
||||||
if (!canToggle.value) return;
|
if (!canGetInfo.value) return;
|
||||||
if (!props.reaction.includes(':')) return;
|
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
text: i18n.ts.info,
|
text: i18n.ts.info,
|
||||||
icon: 'ti ti-info-circle',
|
icon: 'ti ti-info-circle',
|
||||||
|
|
|
@ -157,7 +157,7 @@ const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev);
|
||||||
const setDefaultEmoji = () => setDefault(pinnedEmojis);
|
const setDefaultEmoji = () => setDefault(pinnedEmojis);
|
||||||
|
|
||||||
function previewReaction(ev: MouseEvent) {
|
function previewReaction(ev: MouseEvent) {
|
||||||
reactionPicker.show(getHTMLElement(ev));
|
reactionPicker.show(getHTMLElement(ev), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function previewEmoji(ev: MouseEvent) {
|
function previewEmoji(ev: MouseEvent) {
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
|
export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple): boolean {
|
||||||
|
const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [];
|
||||||
|
return !(emoji.localOnly && note.user.host !== me.host)
|
||||||
|
&& !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote'))
|
||||||
|
&& (roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id)));
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import { defineAsyncComponent, Ref, ref } from 'vue';
|
import { defineAsyncComponent, Ref, ref } from 'vue';
|
||||||
import { popup } from '@/os.js';
|
import { popup } from '@/os.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
@ -10,6 +11,7 @@ import { defaultStore } from '@/store.js';
|
||||||
class ReactionPicker {
|
class ReactionPicker {
|
||||||
private src: Ref<HTMLElement | null> = ref(null);
|
private src: Ref<HTMLElement | null> = ref(null);
|
||||||
private manualShowing = ref(false);
|
private manualShowing = ref(false);
|
||||||
|
private targetNote: Ref<Misskey.entities.Note | null> = ref(null);
|
||||||
private onChosen?: (reaction: string) => void;
|
private onChosen?: (reaction: string) => void;
|
||||||
private onClosed?: () => void;
|
private onClosed?: () => void;
|
||||||
|
|
||||||
|
@ -23,6 +25,7 @@ class ReactionPicker {
|
||||||
src: this.src,
|
src: this.src,
|
||||||
pinnedEmojis: reactionsRef,
|
pinnedEmojis: reactionsRef,
|
||||||
asReactionPicker: true,
|
asReactionPicker: true,
|
||||||
|
targetNote: this.targetNote,
|
||||||
manualShowing: this.manualShowing,
|
manualShowing: this.manualShowing,
|
||||||
}, {
|
}, {
|
||||||
done: reaction => {
|
done: reaction => {
|
||||||
|
@ -38,8 +41,9 @@ class ReactionPicker {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public show(src: HTMLElement | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
|
public show(src: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
|
||||||
this.src.value = src;
|
this.src.value = src;
|
||||||
|
this.targetNote.value = targetNote;
|
||||||
this.manualShowing.value = true;
|
this.manualShowing.value = true;
|
||||||
this.onChosen = onChosen;
|
this.onChosen = onChosen;
|
||||||
this.onClosed = onClosed;
|
this.onClosed = onClosed;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.8
|
* version: 2024.2.0-beta.8
|
||||||
* generatedAt: 2024-02-04T11:51:13.598Z
|
* generatedAt: 2024-02-04T16:51:09.469Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SwitchCaseResponseType } from '../api.js';
|
import type { SwitchCaseResponseType } from '../api.js';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.8
|
* version: 2024.2.0-beta.8
|
||||||
* generatedAt: 2024-02-04T11:51:13.595Z
|
* generatedAt: 2024-02-04T16:51:09.467Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.8
|
* version: 2024.2.0-beta.8
|
||||||
* generatedAt: 2024-02-04T11:51:13.593Z
|
* generatedAt: 2024-02-04T16:51:09.466Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { operations } from './types.js';
|
import { operations } from './types.js';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.8
|
* version: 2024.2.0-beta.8
|
||||||
* generatedAt: 2024-02-04T11:51:13.592Z
|
* generatedAt: 2024-02-04T16:51:09.465Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { components } from './types.js';
|
import { components } from './types.js';
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* version: 2024.2.0-beta.8
|
* version: 2024.2.0-beta.8
|
||||||
* generatedAt: 2024-02-04T11:51:13.473Z
|
* generatedAt: 2024-02-04T16:51:09.378Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4423,6 +4423,7 @@ export type components = {
|
||||||
name: string;
|
name: string;
|
||||||
category: string | null;
|
category: string | null;
|
||||||
url: string;
|
url: string;
|
||||||
|
localOnly?: boolean;
|
||||||
isSensitive?: boolean;
|
isSensitive?: boolean;
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction?: string[];
|
roleIdsThatCanBeUsedThisEmojiAsReaction?: string[];
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue