feat: add an option to disable emoji reactions (#9878)

Closes: #9865
Co-authored-by: naskya <m@naskya.net>
Reviewed-on: https://codeberg.org/calckey/calckey/pulls/9878
Co-authored-by: naskya <naskya@noreply.codeberg.org>
Co-committed-by: naskya <naskya@noreply.codeberg.org>
This commit is contained in:
naskya 2023-04-20 02:53:28 +00:00 committed by Kainoa Kanter
parent 4cb4471fd2
commit 914d8fa9db
13 changed files with 330 additions and 83 deletions

View file

@ -114,6 +114,7 @@ clickToShow: "Click to show"
sensitive: "NSFW"
add: "Add"
reaction: "Reactions"
enableEmojiReactions: "Enable emoji reactions"
reactionSetting: "Reactions to show in the reaction picker"
reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add."
rememberNoteVisibility: "Remember post visibility settings"

View file

@ -109,6 +109,7 @@ clickToShow: "クリックして表示"
sensitive: "閲覧注意"
add: "追加"
reaction: "リアクション"
enableEmojiReactions: "絵文字リアクションを有効にする"
reactionSetting: "ピッカーに表示するリアクション"
reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。"
rememberNoteVisibility: "公開範囲を記憶する"

View file

@ -107,6 +107,7 @@ clickToShow: "点击以显示"
sensitive: "敏感内容"
add: "添加"
reaction: "回应"
enableEmojiReaction: "启用表情符号回应"
reactionSetting: "在选择器中显示的回应"
reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。"
rememberNoteVisibility: "保存上次设置的可见性"

View file

@ -107,6 +107,7 @@ clickToShow: "按一下以顯示"
sensitive: "敏感內容"
add: "新增"
reaction: "情感"
enableEmojiReaction: "啟用表情符號反應"
reactionSetting: "在選擇器中顯示反應"
reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。"
rememberNoteVisibility: "記住貼文可見性"

View file

@ -176,6 +176,7 @@
</div>
<footer ref="el" class="footer" @click.stop>
<XReactionsViewer
v-if="enableEmojiReactions"
ref="reactionsViewer"
:note="appearNote"
/>
@ -195,14 +196,32 @@
:note="appearNote"
:count="appearNote.renoteCount"
/>
<XStarButtonNoEmoji
v-if="!enableEmojiReactions"
class="button"
:note="appearNote"
:count="
Object.values(appearNote.reactions).reduce(
(partialSum, val) => partialSum + val,
0
)
"
:reacted="appearNote.myReaction != null"
/>
<XStarButton
v-if="appearNote.myReaction == null"
v-if="
enableEmojiReactions &&
appearNote.myReaction == null
"
ref="starButton"
class="button"
:note="appearNote"
/>
<button
v-if="appearNote.myReaction == null"
v-if="
enableEmojiReactions &&
appearNote.myReaction == null
"
ref="reactButton"
v-tooltip.noDelay.bottom="i18n.ts.reaction"
class="button _button"
@ -211,7 +230,10 @@
<i class="ph-smiley ph-bold ph-lg"></i>
</button>
<button
v-if="appearNote.myReaction != null"
v-if="
enableEmojiReactions &&
appearNote.myReaction != null
"
ref="reactButton"
class="button _button reacted"
@click="undoReact(appearNote)"
@ -263,6 +285,7 @@ import XPoll from "@/components/MkPoll.vue";
import XRenoteButton from "@/components/MkRenoteButton.vue";
import XReactionsViewer from "@/components/MkReactionsViewer.vue";
import XStarButton from "@/components/MkStarButton.vue";
import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue";
import XQuoteButton from "@/components/MkQuoteButton.vue";
import MkUrlPreview from "@/components/MkUrlPreview.vue";
import MkVisibility from "@/components/MkVisibility.vue";
@ -333,6 +356,7 @@ const translating = ref(false);
const urls = appearNote.text
? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5)
: null;
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
const keymap = {
r: () => reply(true),

View file

@ -179,6 +179,7 @@
</MkA>
</div>
<XReactionsViewer
v-if="enableEmojiReactions"
ref="reactionsViewer"
:note="appearNote"
/>
@ -203,14 +204,32 @@
:note="appearNote"
:count="appearNote.renoteCount"
/>
<XStarButtonNoEmoji
v-if="!enableEmojiReactions"
class="button"
:note="appearNote"
:count="
Object.values(appearNote.reactions).reduce(
(partialSum, val) => partialSum + val,
0
)
"
:reacted="appearNote.myReaction != null"
/>
<XStarButton
v-if="appearNote.myReaction == null"
v-if="
enableEmojiReactions &&
appearNote.myReaction == null
"
ref="starButton"
class="button"
:note="appearNote"
/>
<button
v-if="appearNote.myReaction == null"
v-if="
enableEmojiReactions &&
appearNote.myReaction == null
"
ref="reactButton"
v-tooltip.noDelay.bottom="i18n.ts.reaction"
class="button _button"
@ -219,7 +238,10 @@
<i class="ph-smiley ph-bold ph-lg"></i>
</button>
<button
v-if="appearNote.myReaction != null"
v-if="
enableEmojiReactions &&
appearNote.myReaction != null
"
ref="reactButton"
class="button _button reacted"
@click="undoReact(appearNote)"
@ -283,6 +305,7 @@ import XMediaList from "@/components/MkMediaList.vue";
import XCwButton from "@/components/MkCwButton.vue";
import XPoll from "@/components/MkPoll.vue";
import XStarButton from "@/components/MkStarButton.vue";
import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue";
import XRenoteButton from "@/components/MkRenoteButton.vue";
import XQuoteButton from "@/components/MkQuoteButton.vue";
import MkUrlPreview from "@/components/MkUrlPreview.vue";
@ -316,6 +339,8 @@ const inChannel = inject("inChannel", null);
let note = $ref(deepClone(props.note));
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {

View file

@ -87,6 +87,7 @@
</div>
<footer class="footer" @click.stop>
<XReactionsViewer
v-if="enableEmojiReactions"
ref="reactionsViewer"
:note="appearNote"
/>
@ -106,14 +107,32 @@
:note="appearNote"
:count="appearNote.renoteCount"
/>
<XStarButtonNoEmoji
v-if="!enableEmojiReactions"
class="button"
:note="appearNote"
:count="
Object.values(appearNote.reactions).reduce(
(partialSum, val) => partialSum + val,
0
)
"
:reacted="appearNote.myReaction != null"
/>
<XStarButton
v-if="appearNote.myReaction == null"
v-if="
enableEmojiReactions &&
appearNote.myReaction == null
"
ref="starButton"
class="button"
:note="appearNote"
/>
<button
v-if="appearNote.myReaction == null"
v-if="
enableEmojiReactions &&
appearNote.myReaction == null
"
ref="reactButton"
v-tooltip.noDelay.bottom="i18n.ts.reaction"
class="button _button"
@ -122,7 +141,10 @@
<i class="ph-smiley ph-bold ph-lg"></i>
</button>
<button
v-if="appearNote.myReaction != null"
v-if="
enableEmojiReactions &&
appearNote.myReaction != null
"
ref="reactButton"
class="button _button reacted"
@click="undoReact(appearNote)"
@ -187,6 +209,7 @@ import XNoteHeader from "@/components/MkNoteHeader.vue";
import MkSubNoteContent from "@/components/MkSubNoteContent.vue";
import XReactionsViewer from "@/components/MkReactionsViewer.vue";
import XStarButton from "@/components/MkStarButton.vue";
import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue";
import XRenoteButton from "@/components/MkRenoteButton.vue";
import XQuoteButton from "@/components/MkQuoteButton.vue";
import XCwButton from "@/components/MkCwButton.vue";
@ -199,6 +222,7 @@ import { reactionPicker } from "@/scripts/reaction-picker";
import { i18n } from "@/i18n";
import { deepClone } from "@/scripts/clone";
import { useNoteCapture } from "@/scripts/use-note-capture";
import { defaultStore } from "@/store";
const router = useRouter();
@ -247,6 +271,7 @@ const replies: misskey.entities.Note[] =
item.renoteId === props.note.id
)
.reverse() ?? [];
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
useNoteCapture({
rootEl: el,

View file

@ -65,7 +65,10 @@
></i>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<XReactionIcon
v-else-if="notification.type === 'reaction'"
v-else-if="
notification.type === 'reaction' &&
defaultStore.state.enableEmojiReactions
"
ref="reactionRef"
:reaction="
notification.reaction
@ -78,6 +81,14 @@
:custom-emojis="notification.note.emojis"
:no-style="true"
/>
<XReactionIcon
v-else-if="
notification.type === 'reaction' &&
!defaultStore.state.enableEmojiReactions
"
:reaction="defaultReaction"
:no-style="true"
/>
</div>
</div>
<div class="tail">
@ -272,6 +283,8 @@ import { i18n } from "@/i18n";
import * as os from "@/os";
import { stream } from "@/stream";
import { useTooltip } from "@/scripts/use-tooltip";
import { defaultStore } from "@/store";
import { instance } from "@/instance";
const props = withDefaults(
defineProps<{
@ -288,6 +301,10 @@ const props = withDefaults(
const elRef = ref<HTMLElement>(null);
const reactionRef = ref(null);
const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReaction)
? instance.defaultReaction
: "⭐";
let readObserver: IntersectionObserver | undefined;
let connection;

View file

@ -6,7 +6,7 @@
@close="dialog.close()"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.reactions }}</template>
<template #header>{{ i18n.ts.reaction }}</template>
<MkSpacer :margin-min="20" :margin-max="28">
<div v-if="note" class="_gaps">

View file

@ -0,0 +1,133 @@
<template>
<button
v-tooltip.noDelay.bottom="i18n.ts._gallery.like"
class="_button"
:class="$style.root"
ref="buttonRef"
@click="toggleStar($event)"
>
<span v-if="!reacted">
<i
v-if="instance.defaultReaction === '👍'"
class="ph-thumbs-up ph-bold ph-lg"
></i>
<i
v-else-if="instance.defaultReaction === '❤️'"
class="ph-heart ph-bold ph-lg"
></i>
<i v-else class="ph-star ph-bold ph-lg"></i>
</span>
<span v-else>
<i
v-if="instance.defaultReaction === '👍'"
class="ph-thumbs-up ph-bold ph-lg ph-fill"
:class="$style.yellow"
></i>
<i
v-else-if="instance.defaultReaction === '❤️'"
class="ph-heart ph-bold ph-lg ph-fill"
:class="$style.red"
></i>
<i
v-else
class="ph-star ph-bold ph-lg ph-fill"
:class="$style.yellow"
></i>
</span>
<template v-if="count > 0"
><p :class="$style.count">{{ count }}</p></template
>
</button>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import type { Note } from "calckey-js/built/entities";
import Ripple from "@/components/MkRipple.vue";
import XDetails from "@/components/MkUsersTooltip.vue";
import { pleaseLogin } from "@/scripts/please-login";
import * as os from "@/os";
import { defaultStore } from "@/store";
import { i18n } from "@/i18n";
import { instance } from "@/instance";
import { useTooltip } from "@/scripts/use-tooltip";
const props = defineProps<{
note: Note;
count: number;
reacted: boolean;
}>();
const buttonRef = ref<HTMLElement>();
function toggleStar(ev?: MouseEvent): void {
pleaseLogin();
if (!props.reacted) {
os.api("notes/reactions/create", {
noteId: props.note.id,
reaction: instance.defaultReaction,
});
const el =
ev &&
((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + el.offsetWidth / 2;
const y = rect.top + el.offsetHeight / 2;
os.popup(Ripple, { x, y }, {}, "end");
}
} else {
os.api("notes/reactions/delete", {
noteId: props.note.id,
});
}
}
useTooltip(buttonRef, async (showing) => {
const reactions = await os.apiGet("notes/reactions", {
noteId: props.note.id,
limit: 11,
_cacheKey_: props.count,
});
const users = reactions.map((x) => x.user);
if (users.length < 1) return;
os.popup(
XDetails,
{
showing,
users,
count: props.count,
targetElement: buttonRef.value,
},
{},
"closed"
);
});
</script>
<style lang="scss" module>
.root {
display: inline-block;
height: 32px;
margin: 2px;
padding: 0 6px;
}
.yellow {
color: var(--warn);
}
.red {
color: var(--error);
}
.count {
display: inline;
margin: 0 0 0 8px;
opacity: 0.7;
}
</style>

View file

@ -114,6 +114,7 @@ const defaultStoreSaveKeys: (keyof (typeof defaultStore)["state"])[] = [
"swipeOnDesktop",
"showAdminUpdates",
"enableCustomKaTeXMacro",
"enableEmojiReactions",
];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
"lightTheme",

View file

@ -1,81 +1,92 @@
<template>
<div class="_formRoot">
<FromSlot class="_formBlock">
<template #label>{{ i18n.ts.reactionSettingDescription }}</template>
<div v-panel style="border-radius: 6px">
<XDraggable
v-model="reactions"
class="zoaiodol"
:item-key="(item) => item"
animation="150"
delay="100"
delay-on-touch-only="true"
>
<template #item="{ element }">
<button
class="_button item"
@click="remove(element, $event)"
>
<MkEmoji :emoji="element" :normal="true" />
</button>
</template>
<template #footer>
<button class="_button add" @click="chooseEmoji">
<i class="ph-plus ph-bold ph-lg"></i>
</button>
</template>
</XDraggable>
</div>
<template #caption
>{{ i18n.ts.reactionSettingDescription2 }}
<button class="_textButton" @click="preview">
{{ i18n.ts.preview }}
</button></template
>
</FromSlot>
<FormRadios v-model="reactionPickerSize" class="_formBlock">
<template #label>{{ i18n.ts.size }}</template>
<option :value="1">{{ i18n.ts.small }}</option>
<option :value="2">{{ i18n.ts.medium }}</option>
<option :value="3">{{ i18n.ts.large }}</option>
</FormRadios>
<FormRadios v-model="reactionPickerWidth" class="_formBlock">
<template #label>{{ i18n.ts.numberOfColumn }}</template>
<option :value="1">5</option>
<option :value="2">6</option>
<option :value="3">7</option>
<option :value="4">8</option>
<option :value="5">9</option>
</FormRadios>
<FormRadios v-model="reactionPickerHeight" class="_formBlock">
<template #label>{{ i18n.ts.height }}</template>
<option :value="1">{{ i18n.ts.small }}</option>
<option :value="2">{{ i18n.ts.medium }}</option>
<option :value="3">{{ i18n.ts.large }}</option>
<option :value="4">{{ i18n.ts.large }}+</option>
</FormRadios>
<FormSwitch
v-model="reactionPickerUseDrawerForMobile"
class="_formBlock"
>
{{ i18n.ts.useDrawerReactionPickerForMobile }}
<FormSwitch v-model="enableEmojiReactions" class="_formBlock">
{{ i18n.ts.enableEmojiReactions }}
<template #caption>{{ i18n.ts.needReloadToApply }}</template>
</FormSwitch>
<FormSection>
<div style="display: flex; gap: var(--margin); flex-wrap: wrap">
<FormButton inline @click="preview"
><i class="ph-eye ph-bold ph-lg"></i>
{{ i18n.ts.preview }}</FormButton
<div v-if="enableEmojiReactions">
<FromSlot class="_formBlock">
<template #label>{{
i18n.ts.reactionSettingDescription
}}</template>
<div v-panel style="border-radius: 6px">
<XDraggable
v-model="reactions"
class="zoaiodol"
:item-key="(item) => item"
animation="150"
delay="100"
delay-on-touch-only="true"
>
<template #item="{ element }">
<button
class="_button item"
@click="remove(element, $event)"
>
<MkEmoji :emoji="element" :normal="true" />
</button>
</template>
<template #footer>
<button class="_button add" @click="chooseEmoji">
<i class="ph-plus ph-bold ph-lg"></i>
</button>
</template>
</XDraggable>
</div>
<template #caption
>{{ i18n.ts.reactionSettingDescription2 }}
<button class="_textButton" @click="preview">
{{ i18n.ts.preview }}
</button></template
>
<FormButton inline danger @click="setDefault"
><i class="ph-arrow-counter-clockwise ph-bold ph-lg"></i>
{{ i18n.ts.default }}</FormButton
>
</div>
</FormSection>
</FromSlot>
<FormRadios v-model="reactionPickerSize" class="_formBlock">
<template #label>{{ i18n.ts.size }}</template>
<option :value="1">{{ i18n.ts.small }}</option>
<option :value="2">{{ i18n.ts.medium }}</option>
<option :value="3">{{ i18n.ts.large }}</option>
</FormRadios>
<FormRadios v-model="reactionPickerWidth" class="_formBlock">
<template #label>{{ i18n.ts.numberOfColumn }}</template>
<option :value="1">5</option>
<option :value="2">6</option>
<option :value="3">7</option>
<option :value="4">8</option>
<option :value="5">9</option>
</FormRadios>
<FormRadios v-model="reactionPickerHeight" class="_formBlock">
<template #label>{{ i18n.ts.height }}</template>
<option :value="1">{{ i18n.ts.small }}</option>
<option :value="2">{{ i18n.ts.medium }}</option>
<option :value="3">{{ i18n.ts.large }}</option>
<option :value="4">{{ i18n.ts.large }}+</option>
</FormRadios>
<FormSwitch
v-model="reactionPickerUseDrawerForMobile"
class="_formBlock"
>
{{ i18n.ts.useDrawerReactionPickerForMobile }}
<template #caption>{{ i18n.ts.needReloadToApply }}</template>
</FormSwitch>
<FormSection>
<div style="display: flex; gap: var(--margin); flex-wrap: wrap">
<FormButton inline @click="preview"
><i class="ph-eye ph-bold ph-lg"></i>
{{ i18n.ts.preview }}</FormButton
>
<FormButton inline danger @click="setDefault"
><i
class="ph-arrow-counter-clockwise ph-bold ph-lg"
></i>
{{ i18n.ts.default }}</FormButton
>
</div>
</FormSection>
</div>
</div>
</template>
@ -108,6 +119,9 @@ const reactionPickerHeight = $computed(
const reactionPickerUseDrawerForMobile = $computed(
defaultStore.makeGetterSetter("reactionPickerUseDrawerForMobile")
);
const enableEmojiReactions = $computed(
defaultStore.makeGetterSetter("enableEmojiReactions")
);
function save() {
defaultStore.set("reactions", reactions);

View file

@ -294,6 +294,10 @@ export const defaultStore = markRaw(
where: "device",
default: false,
},
enableEmojiReactions: {
where: "account",
default: true,
},
}),
);