feat(frontend): 絵文字をミュート可能にする機能 (#15966)
* wip ( 絵文字ミュートの基礎実装, PoC )
* refactor: 絵文字のmute/unmute処理の共通化
* SPDX
* リアクションからも絵文字ミュート可能に
* emojiMute/emojiUnmute
* replace resource of emojiMute
* add vitest preferstate for mutedEmojis
* add vitest to preferReactive
* 混入削除
* Fix typo (mutedEmojis -> mutingEmojis)
* reactiveやめる
* add時の判定ミスを修正
* Add CHANGELOG
* Revert "reactiveやめる"
This reverts commit 442742c371
.
* Update Changelog
This commit is contained in:
parent
b18d6b4cef
commit
5bc52b6743
|
@ -14,6 +14,8 @@
|
|||
- 何らの理由によりWebsocket接続が行えない環境でも快適に利用可能です
|
||||
- 従来のWebsocket接続を行うモードはリアルタイムモードとして再定義されました
|
||||
- チャットなど、一部の機能は引き続き設定に関わらずWebsocket接続が行われます
|
||||
- Feat: 絵文字をミュート可能にする機能
|
||||
- 絵文字(ユニコードの絵文字・カスタム絵文字)毎にミュートし、不可視化することができるようになりました
|
||||
- Enhance: メモリ使用量を軽減しました
|
||||
- Enhance: 画像の高品質なプレースホルダを無効化してパフォーマンスを向上させるオプションを追加
|
||||
- Enhance: 招待されているが参加していないルームを開いたときに、招待を承認するかどうか尋ねるように
|
||||
|
|
|
@ -5425,6 +5425,22 @@ export interface Locale extends ILocale {
|
|||
* オフにする
|
||||
*/
|
||||
"turnItOff": string;
|
||||
/**
|
||||
* 絵文字ミュート
|
||||
*/
|
||||
"emojiMute": string;
|
||||
/**
|
||||
* 絵文字ミュート解除
|
||||
*/
|
||||
"emojiUnmute": string;
|
||||
/**
|
||||
* {x}をミュート
|
||||
*/
|
||||
"muteX": ParameterizedString<"x">;
|
||||
/**
|
||||
* {x}のミュートを解除
|
||||
*/
|
||||
"unmuteX": ParameterizedString<"x">;
|
||||
"_chat": {
|
||||
/**
|
||||
* まだメッセージはありません
|
||||
|
|
|
@ -1351,6 +1351,10 @@ advice: "アドバイス"
|
|||
realtimeMode: "リアルタイムモード"
|
||||
turnItOn: "オンにする"
|
||||
turnItOff: "オフにする"
|
||||
emojiMute: "絵文字ミュート"
|
||||
emojiUnmute: "絵文字ミュート解除"
|
||||
muteX: "{x}をミュート"
|
||||
unmuteX: "{x}のミュートを解除"
|
||||
|
||||
_chat:
|
||||
noMessagesYet: "まだメッセージはありません"
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -22,6 +22,7 @@ import { computed, inject, onMounted, useTemplateRef, watch } from 'vue';
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { getUnicodeEmoji } from '@@/js/emojilist.js';
|
||||
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
|
||||
import type { MenuItem } from '@/types/menu';
|
||||
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -36,6 +37,7 @@ import { customEmojisMap } from '@/custom-emojis.js';
|
|||
import { prefer } from '@/preferences.js';
|
||||
import { DI } from '@/di.js';
|
||||
import { noteEvents } from '@/composables/use-note-capture.js';
|
||||
import { mute as muteEmoji, unmute as unmuteEmoji, checkMuted as isEmojiMuted } from '@/utility/emoji-mute.js';
|
||||
|
||||
const props = defineProps<{
|
||||
noteId: Misskey.entities.Note['id'];
|
||||
|
@ -63,6 +65,7 @@ const canToggle = computed(() => {
|
|||
return !props.reaction.match(/@\w/) && $i && emoji.value;
|
||||
});
|
||||
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
|
||||
const isLocalCustomEmoji = props.reaction[0] === ':' && props.reaction.includes('@.');
|
||||
|
||||
async function toggleReaction() {
|
||||
if (!canToggle.value) return;
|
||||
|
@ -139,9 +142,10 @@ async function toggleReaction() {
|
|||
}
|
||||
|
||||
async function menu(ev) {
|
||||
if (!canGetInfo.value) return;
|
||||
let menuItems: MenuItem[] = [];
|
||||
|
||||
os.popupMenu([{
|
||||
if (canGetInfo.value) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.info,
|
||||
icon: 'ti ti-info-circle',
|
||||
action: async () => {
|
||||
|
@ -153,7 +157,40 @@ async function menu(ev) {
|
|||
closed: () => dispose(),
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
});
|
||||
}
|
||||
|
||||
if (isEmojiMuted(props.reaction).value) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.emojiUnmute,
|
||||
icon: 'ti ti-mood-smile',
|
||||
action: () => {
|
||||
os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.tsx.unmuteX({ x: isLocalCustomEmoji ? `:${emojiName.value}:` : props.reaction }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
unmuteEmoji(props.reaction);
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
menuItems.push({
|
||||
text: i18n.ts.emojiMute,
|
||||
icon: 'ti ti-mood-off',
|
||||
action: () => {
|
||||
os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.tsx.muteX({ x: isLocalCustomEmoji ? `:${emojiName.value}:` : props.reaction }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
muteEmoji(props.reaction);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function anime() {
|
||||
|
|
|
@ -5,7 +5,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<img
|
||||
v-if="errored && fallbackToImage"
|
||||
v-if="shouldMute"
|
||||
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
|
||||
src="/client-assets/unknown.png"
|
||||
:title="alt"
|
||||
draggable="false"
|
||||
style="-webkit-user-drag: none;"
|
||||
@click="onClick"
|
||||
/>
|
||||
<img
|
||||
v-else-if="errored && fallbackToImage"
|
||||
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
|
||||
src="/client-assets/dummy.png"
|
||||
:title="alt"
|
||||
|
@ -40,6 +49,7 @@ import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialo
|
|||
import { $i } from '@/i.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { DI } from '@/di.js';
|
||||
import { makeEmojiMuteKey, mute as muteEmoji, unmute as unmuteEmoji, checkMuted as checkEmojiMuted } from '@/utility/emoji-mute';
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
|
@ -51,12 +61,16 @@ const props = defineProps<{
|
|||
menu?: boolean;
|
||||
menuReaction?: boolean;
|
||||
fallbackToImage?: boolean;
|
||||
ignoreMuted?: boolean;
|
||||
}>();
|
||||
|
||||
const react = inject(DI.mfmEmojiReactCallback);
|
||||
|
||||
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', ''));
|
||||
const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
|
||||
const emojiCodeToMute = makeEmojiMuteKey(props);
|
||||
const isMuted = checkEmojiMuted(emojiCodeToMute);
|
||||
const shouldMute = computed(() => !props.ignoreMuted && isMuted.value);
|
||||
|
||||
const rawUrl = computed(() => {
|
||||
if (props.url) {
|
||||
|
@ -95,13 +109,17 @@ function onClick(ev: MouseEvent) {
|
|||
menuItems.push({
|
||||
type: 'label',
|
||||
text: `:${props.name}:`,
|
||||
}, {
|
||||
});
|
||||
|
||||
if (isLocal.value) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.copy,
|
||||
icon: 'ti ti-copy',
|
||||
action: () => {
|
||||
copyToClipboard(`:${props.name}:`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (props.menuReaction && react) {
|
||||
menuItems.push({
|
||||
|
@ -113,7 +131,10 @@ function onClick(ev: MouseEvent) {
|
|||
});
|
||||
}
|
||||
|
||||
if (isLocal.value) {
|
||||
menuItems.push({
|
||||
type: 'divider',
|
||||
}, {
|
||||
text: i18n.ts.info,
|
||||
icon: 'ti ti-info-circle',
|
||||
action: async () => {
|
||||
|
@ -126,8 +147,27 @@ function onClick(ev: MouseEvent) {
|
|||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if ($i?.isModerator ?? $i?.isAdmin) {
|
||||
if (isMuted.value) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.emojiUnmute,
|
||||
icon: 'ti ti-mood-smile',
|
||||
action: async () => {
|
||||
await unmute();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
menuItems.push({
|
||||
text: i18n.ts.emojiMute,
|
||||
icon: 'ti ti-mood-off',
|
||||
action: async () => {
|
||||
await mute();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (($i?.isModerator ?? $i?.isAdmin) && isLocal.value) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.edit,
|
||||
icon: 'ti ti-pencil',
|
||||
|
@ -152,6 +192,36 @@ async function edit(name: string) {
|
|||
});
|
||||
}
|
||||
|
||||
function mute() {
|
||||
const titleEmojiName = isLocal.value
|
||||
? `:${customEmojiName.value}:`
|
||||
: emojiCodeToMute;
|
||||
os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.tsx.muteX({ x: titleEmojiName }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
muteEmoji(emojiCodeToMute);
|
||||
});
|
||||
}
|
||||
|
||||
function unmute() {
|
||||
const titleEmojiName = isLocal.value
|
||||
? `:${customEmojiName.value}:`
|
||||
: emojiCodeToMute;
|
||||
os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.tsx.unmuteX({ x: titleEmojiName }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
unmuteEmoji(emojiCodeToMute);
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -4,7 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
|
||||
<img v-if="shouldMute" :class="$style.root" src="/client-assets/unknown.png" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
|
||||
<img v-else-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
|
||||
<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ colorizedNativeEmoji }}</span>
|
||||
</template>
|
||||
|
||||
|
@ -18,11 +19,13 @@ import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { DI } from '@/di.js';
|
||||
import { mute as muteEmoji, unmute as unmuteEmoji, checkMuted as checkMutedEmoji } from '@/utility/emoji-mute.js';
|
||||
|
||||
const props = defineProps<{
|
||||
emoji: string;
|
||||
menu?: boolean;
|
||||
menuReaction?: boolean;
|
||||
ignoreMuted?: boolean;
|
||||
}>();
|
||||
|
||||
const react = inject(DI.mfmEmojiReactCallback, null);
|
||||
|
@ -32,12 +35,38 @@ const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : cha
|
|||
const useOsNativeEmojis = computed(() => prefer.s.emojiStyle === 'native');
|
||||
const url = computed(() => char2path(props.emoji));
|
||||
const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji));
|
||||
const isMuted = checkMutedEmoji(props.emoji);
|
||||
const shouldMute = computed(() => isMuted.value && !props.ignoreMuted);
|
||||
|
||||
// Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter
|
||||
function computeTitle(event: PointerEvent): void {
|
||||
(event.target as HTMLElement).title = getEmojiName(props.emoji);
|
||||
}
|
||||
|
||||
function mute() {
|
||||
os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.tsx.muteX({ x: props.emoji }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
muteEmoji(props.emoji);
|
||||
});
|
||||
}
|
||||
|
||||
function unmute() {
|
||||
os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.tsx.unmuteX({ x: props.emoji }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
unmuteEmoji(props.emoji);
|
||||
});
|
||||
}
|
||||
|
||||
function onClick(ev: MouseEvent) {
|
||||
if (props.menu) {
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
@ -63,6 +92,22 @@ function onClick(ev: MouseEvent) {
|
|||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
type: 'divider',
|
||||
}, isMuted.value ? {
|
||||
text: i18n.ts.emojiUnmute,
|
||||
icon: 'ti ti-mood-smile',
|
||||
action: () => {
|
||||
unmute();
|
||||
},
|
||||
} : {
|
||||
text: i18n.ts.emojiMute,
|
||||
icon: 'ti ti-mood-off',
|
||||
action: () => {
|
||||
mute();
|
||||
},
|
||||
});
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -435,6 +435,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
normal: props.plain,
|
||||
host: props.author.host,
|
||||
useOriginalSize: scale >= 2.5,
|
||||
menu: props.enableEmojiMenu,
|
||||
menuReaction: false,
|
||||
})];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.emojis">
|
||||
<div v-for="emoji in emojis" :key="`emojiMute-${emoji}`" :class="$style.emoji" @click="onEmojiClick($event, emoji)">
|
||||
<MkCustomEmoji
|
||||
v-if="emoji.startsWith(':')"
|
||||
:name="customEmojiName(emoji)"
|
||||
:host="customEmojiHost(emoji)"
|
||||
:normal="true"
|
||||
:menu="false"
|
||||
:menuReaction="false"
|
||||
:ignoreMuted="true"
|
||||
/>
|
||||
<MkEmoji
|
||||
v-else
|
||||
:emoji="emoji"
|
||||
:menu="false"
|
||||
:menuReaction="false"
|
||||
:ignoreMuted="true"
|
||||
></MkEmoji>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkButton primary inline @click="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MenuItem } from '@/types/menu';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import {
|
||||
mute as muteEmoji,
|
||||
unmute as unmuteEmoji,
|
||||
extractCustomEmojiName as customEmojiName,
|
||||
extractCustomEmojiHost as customEmojiHost,
|
||||
} from '@/utility/emoji-mute.js';
|
||||
|
||||
const emojis = prefer.model('mutingEmojis');
|
||||
|
||||
function getHTMLElement(ev: MouseEvent): HTMLElement {
|
||||
const target = ev.currentTarget ?? ev.target;
|
||||
return target as HTMLElement;
|
||||
}
|
||||
|
||||
function add(ev: MouseEvent) {
|
||||
os.pickEmoji(getHTMLElement(ev), { showPinned: false }).then((emoji) => {
|
||||
if (emoji) {
|
||||
muteEmoji(emoji);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onEmojiClick(ev: MouseEvent, emoji: string) {
|
||||
const menuItems : MenuItem[] = [{
|
||||
type: 'label',
|
||||
text: emoji,
|
||||
}, {
|
||||
text: i18n.ts.emojiUnmute,
|
||||
icon: 'ti ti-mood-off',
|
||||
action: () => unmute(emoji),
|
||||
}];
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function unmute(emoji: string) {
|
||||
os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.tsx.unmuteX({ x: emoji }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
unmuteEmoji(emoji);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style module>
|
||||
.emojis {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji {
|
||||
display: inline-flex;
|
||||
height: 42px;
|
||||
padding: 0 6px;
|
||||
font-size: 1.5em;
|
||||
border-radius: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--MI_THEME-buttonBg);
|
||||
}
|
||||
</style>
|
|
@ -49,6 +49,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkFolder>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker
|
||||
:label="i18n.ts.emojiMute"
|
||||
:keywords="['emoji', 'mute', 'hide']"
|
||||
>
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-mood-off"></i></template>
|
||||
<template #label>{{ i18n.ts.emojiMute }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<XEmojiMute/>
|
||||
</div>
|
||||
</mkfolder>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker
|
||||
:label="i18n.ts.instanceMute"
|
||||
:keywords="['note', 'server', 'instance', 'host', 'federation', 'mute', 'hide']"
|
||||
|
@ -163,6 +177,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import XEmojiMute from './mute-block.emoji-mute.vue';
|
||||
import XInstanceMute from './mute-block.instance-mute.vue';
|
||||
import XWordMute from './mute-block.word-mute.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
|
|
|
@ -345,6 +345,9 @@ export const PREF_DEF = {
|
|||
plugins: {
|
||||
default: [] as Plugin[],
|
||||
},
|
||||
mutingEmojis: {
|
||||
default: [] as string[],
|
||||
},
|
||||
|
||||
'sound.masterVolume': {
|
||||
default: 0.3,
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { computed } from 'vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
// custom絵文字の情報からキーを作成する
|
||||
export function makeEmojiMuteKey(props: { name: string; host?: string | null }) {
|
||||
return props.name.startsWith(':') ? props.name : `:${props.name}${props.host ? `@${props.host}` : ''}:`;
|
||||
}
|
||||
|
||||
export function extractCustomEmojiName (name:string) {
|
||||
return (name[0] === ':' ? name.substring(1, name.length - 1) : name).replace('@.', '').split('@')[0];
|
||||
}
|
||||
|
||||
export function extractCustomEmojiHost (name:string) {
|
||||
// nameは:emojiName@host:の形式
|
||||
// 取り出したい部分はhostなので、@以降を取り出す
|
||||
const index = name.indexOf('@');
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
const host = name.substring(index + 1, name.length - 1);
|
||||
if (host === '' || host === '.') {
|
||||
return null;
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
export function mute(emoji: string) {
|
||||
const isCustomEmoji = emoji.startsWith(':') && emoji.endsWith(':');
|
||||
const emojiMuteKey = isCustomEmoji ?
|
||||
makeEmojiMuteKey({ name: extractCustomEmojiName(emoji), host: extractCustomEmojiHost(emoji) }) :
|
||||
emoji;
|
||||
const mutedEmojis = prefer.r.mutingEmojis.value;
|
||||
if (!mutedEmojis.includes(emojiMuteKey)) {
|
||||
return prefer.commit('mutingEmojis', [...mutedEmojis, emojiMuteKey]);
|
||||
}
|
||||
throw new Error('Emoji is already muted', { cause: `${emojiMuteKey} is Already Muted` });
|
||||
}
|
||||
|
||||
export function unmute(emoji:string) {
|
||||
const isCustomEmoji = emoji.startsWith(':') && emoji.endsWith(':');
|
||||
const emojiMuteKey = isCustomEmoji ?
|
||||
makeEmojiMuteKey({ name: extractCustomEmojiName(emoji), host: extractCustomEmojiHost(emoji) }) :
|
||||
emoji;
|
||||
const mutedEmojis = prefer.r.mutingEmojis.value;
|
||||
console.log('unmute', emoji, emojiMuteKey);
|
||||
console.log('mutedEmojis', mutedEmojis);
|
||||
prefer.commit('mutingEmojis', mutedEmojis.filter((e) => e !== emojiMuteKey));
|
||||
}
|
||||
|
||||
export function checkMuted(emoji: string) {
|
||||
const isCustomEmoji = emoji.startsWith(':') && emoji.endsWith(':');
|
||||
const emojiMuteKey = isCustomEmoji ?
|
||||
makeEmojiMuteKey({ name: extractCustomEmojiName(emoji), host: extractCustomEmojiHost(emoji) }) :
|
||||
emoji;
|
||||
return computed(() => prefer.r.mutingEmojis.value.includes(emojiMuteKey));
|
||||
}
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
import { vi } from 'vitest';
|
||||
import createFetchMock from 'vitest-fetch-mock';
|
||||
import type { Ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const fetchMocker = createFetchMock(vi);
|
||||
fetchMocker.enableMocks();
|
||||
|
@ -27,13 +29,24 @@ export const preferState: Record<string, unknown> = {
|
|||
code: false,
|
||||
},
|
||||
|
||||
mutingEmojis: [],
|
||||
};
|
||||
|
||||
export let preferReactive: Record<string, Ref<unknown>> = {};
|
||||
|
||||
for (const key in preferState) {
|
||||
if (preferState[key] !== undefined) {
|
||||
preferReactive[key] = ref(preferState[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: store somehow becomes undefined in vitest?
|
||||
vi.mock('@/preferences.js', () => {
|
||||
|
||||
return {
|
||||
prefer: {
|
||||
s: preferState,
|
||||
r: preferReactive,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue