feat(frontend/reactions): リアクションミュート機能を追加しました (MisskeyIO#758)
This commit is contained in:
parent
b585f5bd28
commit
ab566b67ac
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
@ -600,6 +600,14 @@ export interface Locale extends ILocale {
|
|||||||
* ブーストのミュートを解除
|
* ブーストのミュートを解除
|
||||||
*/
|
*/
|
||||||
"renoteUnmute": string;
|
"renoteUnmute": string;
|
||||||
|
/**
|
||||||
|
* リアクションのミュート
|
||||||
|
*/
|
||||||
|
"mutedReactions": string;
|
||||||
|
/**
|
||||||
|
* リモートの絵文字をミュート
|
||||||
|
*/
|
||||||
|
"remoteCustomEmojiMuted": string;
|
||||||
/**
|
/**
|
||||||
* ブロック
|
* ブロック
|
||||||
*/
|
*/
|
||||||
|
@ -146,6 +146,8 @@ mute: "ミュート"
|
|||||||
unmute: "ミュート解除"
|
unmute: "ミュート解除"
|
||||||
renoteMute: "ブーストをミュート"
|
renoteMute: "ブーストをミュート"
|
||||||
renoteUnmute: "ブーストのミュートを解除"
|
renoteUnmute: "ブーストのミュートを解除"
|
||||||
|
mutedReactions: "リアクションのミュート"
|
||||||
|
remoteCustomEmojiMuted: "リモートの絵文字をミュート"
|
||||||
block: "ブロック"
|
block: "ブロック"
|
||||||
unblock: "ブロック解除"
|
unblock: "ブロック解除"
|
||||||
markAsNSFW: "ユーザーのすべてのメディアをNSFWとしてマークする"
|
markAsNSFW: "ユーザーのすべてのメディアをNSFWとしてマークする"
|
||||||
|
@ -32,15 +32,29 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
@chosen="chosen"
|
@chosen="chosen"
|
||||||
@esc="modal?.close()"
|
@esc="modal?.close()"
|
||||||
/>
|
/>
|
||||||
|
<div v-if="manualReactionInput" :class="$style.remoteReactionInputWrapper">
|
||||||
|
<span>{{ i18n.ts.remoteCustomEmojiMuted }}</span>
|
||||||
|
<MkInput v-model="remoteReactionName" placeholder=":emojiname@host:" autocapitalize="off"/>
|
||||||
|
<MkButton :disabled="!(remoteReactionName && remoteReactionName[0] === ':')" @click="chosen(remoteReactionName)">
|
||||||
|
{{ i18n.ts.add }}
|
||||||
|
</MkButton>
|
||||||
|
<div :class="$style.emojiContainer">
|
||||||
|
<MkCustomEmoji v-if="remoteReactionName && remoteReactionName[0] === ':' " :class="$style.emoji" :name="remoteReactionName" :normal="true"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { shallowRef } from 'vue';
|
import { shallowRef, ref } from 'vue';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
manualShowing?: boolean | null;
|
manualShowing?: boolean | null;
|
||||||
@ -50,12 +64,14 @@ const props = withDefaults(defineProps<{
|
|||||||
asReactionPicker?: boolean;
|
asReactionPicker?: boolean;
|
||||||
targetNote?: Misskey.entities.Note;
|
targetNote?: Misskey.entities.Note;
|
||||||
choseAndClose?: boolean;
|
choseAndClose?: boolean;
|
||||||
|
manualReactionInput?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
manualShowing: null,
|
manualShowing: null,
|
||||||
showPinned: true,
|
showPinned: true,
|
||||||
pinnedEmojis: undefined,
|
pinnedEmojis: undefined,
|
||||||
asReactionPicker: false,
|
asReactionPicker: false,
|
||||||
choseAndClose: true,
|
choseAndClose: true,
|
||||||
|
manualReactionInput: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -67,6 +83,8 @@ const emit = defineEmits<{
|
|||||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
const picker = shallowRef<InstanceType<typeof MkEmojiPicker>>();
|
const picker = shallowRef<InstanceType<typeof MkEmojiPicker>>();
|
||||||
|
|
||||||
|
const remoteReactionName = ref('');
|
||||||
|
|
||||||
function chosen(emoji: string) {
|
function chosen(emoji: string) {
|
||||||
emit('done', emoji);
|
emit('done', emoji);
|
||||||
if (props.choseAndClose) {
|
if (props.choseAndClose) {
|
||||||
@ -91,4 +109,16 @@ function opening() {
|
|||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.remoteReactionInputWrapper {
|
||||||
|
margin-top: var(--margin);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--popup);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emojiContainer {
|
||||||
|
height: 48px;
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -22,6 +22,7 @@ import * as Misskey from 'misskey-js';
|
|||||||
import { inject, watch, ref } from 'vue';
|
import { inject, watch, ref } from 'vue';
|
||||||
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
|
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
@ -45,6 +46,13 @@ if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.m
|
|||||||
reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction];
|
reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldDisplayReaction([reaction]: [string, number]): boolean {
|
||||||
|
if (!$i) return true; // 非ログイン状態なら全部のリアクションを見れるように
|
||||||
|
if (reaction === props.note.myReaction) return true; // 自分がつけたリアクションなら表示する
|
||||||
|
if (!defaultStore.state.mutedReactions.includes(reaction.replace('@.', ''))) return true; // ローカルの絵文字には @. というsuffixがつくのでそれを消してから比較してあげる
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function onMockToggleReaction(emoji: string, count: number) {
|
function onMockToggleReaction(emoji: string, count: number) {
|
||||||
if (!mock) return;
|
if (!mock) return;
|
||||||
|
|
||||||
@ -80,7 +88,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
|
|||||||
newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
|
newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
reactions.value = newReactions;
|
reactions.value = newReactions.filter(shouldDisplayReaction);
|
||||||
}, { immediate: true, deep: true });
|
}, { immediate: true, deep: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -105,6 +113,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 4px -2px 0 -2px;
|
margin: 4px -2px 0 -2px;
|
||||||
cursor: auto; /* not clickToOpen-able */
|
cursor: auto; /* not clickToOpen-able */
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -17,6 +17,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #label>{{ i18n.ts.hardWordMute }}</template>
|
<template #label>{{ i18n.ts.hardWordMute }}</template>
|
||||||
|
|
||||||
<XWordMute :muted="$i.hardMutedWords" @save="saveHardMutedWords"/>
|
<XWordMute :muted="$i.hardMutedWords" @save="saveHardMutedWords"/>
|
||||||
|
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder>
|
||||||
|
<template #icon><i class="ti ti-message-off"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.mutedReactions }}</template>
|
||||||
|
|
||||||
|
<div class="_gaps">
|
||||||
|
<div v-panel style="border-radius: var(--radius); padding: var(--margin);">
|
||||||
|
<button v-for="emoji in mutedReactions" class="_button" :class="$style.emojisItem" @click="removeReaction(emoji, $event)">
|
||||||
|
<MkCustomEmoji v-if="emoji && emoji[0] === ':'" :name="emoji"/>
|
||||||
|
<MkEmoji v-else :emoji="emoji ? emoji : 'null'"/>
|
||||||
|
</button>
|
||||||
|
<button class="_button" @click="chooseReaction">
|
||||||
|
<i class="ti ti-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder>
|
<MkFolder>
|
||||||
@ -126,7 +144,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed, watch, Ref } from 'vue';
|
||||||
import XInstanceMute from './mute-block.instance-mute.vue';
|
import XInstanceMute from './mute-block.instance-mute.vue';
|
||||||
import XWordMute from './mute-block.word-mute.vue';
|
import XWordMute from './mute-block.word-mute.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
@ -138,6 +156,9 @@ import * as os from '@/os.js';
|
|||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
import { signinRequired } from '@/account.js';
|
import { signinRequired } from '@/account.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
|
||||||
|
import MkEmoji from '@/components/global/MkEmoji.vue';
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
|
||||||
@ -160,6 +181,38 @@ const expandedRenoteMuteItems = ref([]);
|
|||||||
const expandedMuteItems = ref([]);
|
const expandedMuteItems = ref([]);
|
||||||
const expandedBlockItems = ref([]);
|
const expandedBlockItems = ref([]);
|
||||||
|
|
||||||
|
const mutedReactions = ref<string[]>(defaultStore.state.mutedReactions);
|
||||||
|
|
||||||
|
watch(mutedReactions, () => {
|
||||||
|
defaultStore.set('mutedReactions', mutedReactions.value);
|
||||||
|
}, {
|
||||||
|
deep: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const chooseReaction = (ev: MouseEvent) => pickEmoji(mutedReactions, ev);
|
||||||
|
const removeReaction = (reaction: string, ev: MouseEvent) => remove(mutedReactions, reaction, ev);
|
||||||
|
|
||||||
|
function remove(itemsRef: Ref<string[]>, reaction: string, ev: MouseEvent) {
|
||||||
|
os.popupMenu([{
|
||||||
|
text: i18n.ts.remove,
|
||||||
|
action: () => {
|
||||||
|
itemsRef.value = itemsRef.value.filter(x => x !== reaction);
|
||||||
|
},
|
||||||
|
}], ev.currentTarget ?? ev.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickEmoji(itemsRef: Ref<string[]>, ev: MouseEvent) {
|
||||||
|
os.pickEmoji(ev.currentTarget ?? ev.target, {
|
||||||
|
showPinned: false,
|
||||||
|
manualReactionInput: true,
|
||||||
|
}).then(it => {
|
||||||
|
const emoji = it;
|
||||||
|
if (!itemsRef.value.includes(emoji)) {
|
||||||
|
itemsRef.value.push(emoji);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function unrenoteMute(user, ev) {
|
async function unrenoteMute(user, ev) {
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
text: i18n.ts.renoteUnmute,
|
text: i18n.ts.renoteUnmute,
|
||||||
@ -273,4 +326,9 @@ definePageMetadata(() => ({
|
|||||||
transform: rotateX(180deg);
|
transform: rotateX(180deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emojisItem{
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -118,6 +118,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
|
|||||||
'sound_note',
|
'sound_note',
|
||||||
'sound_noteMy',
|
'sound_noteMy',
|
||||||
'sound_notification',
|
'sound_notification',
|
||||||
|
'mutedReactions',
|
||||||
];
|
];
|
||||||
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
|
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
|
||||||
'lightTheme',
|
'lightTheme',
|
||||||
|
@ -577,6 +577,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||||||
where: 'device',
|
where: 'device',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
mutedReactions: {
|
||||||
|
where: 'account',
|
||||||
|
default: [] as string[],
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// TODO: 他のタブと永続化されたstateを同期
|
// TODO: 他のタブと永続化されたstateを同期
|
||||||
|
Loading…
Reference in New Issue
Block a user