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:
parent
8588979db9
commit
0a173a3c1c
|
@ -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"
|
||||
|
|
|
@ -109,6 +109,7 @@ clickToShow: "クリックして表示"
|
|||
sensitive: "閲覧注意"
|
||||
add: "追加"
|
||||
reaction: "リアクション"
|
||||
enableEmojiReactions: "絵文字リアクションを有効にする"
|
||||
reactionSetting: "ピッカーに表示するリアクション"
|
||||
reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。"
|
||||
rememberNoteVisibility: "公開範囲を記憶する"
|
||||
|
|
|
@ -107,6 +107,7 @@ clickToShow: "点击以显示"
|
|||
sensitive: "敏感内容"
|
||||
add: "添加"
|
||||
reaction: "回应"
|
||||
enableEmojiReaction: "启用表情符号回应"
|
||||
reactionSetting: "在选择器中显示的回应"
|
||||
reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。"
|
||||
rememberNoteVisibility: "保存上次设置的可见性"
|
||||
|
|
|
@ -107,6 +107,7 @@ clickToShow: "按一下以顯示"
|
|||
sensitive: "敏感內容"
|
||||
add: "新增"
|
||||
reaction: "情感"
|
||||
enableEmojiReaction: "啟用表情符號反應"
|
||||
reactionSetting: "在選擇器中顯示反應"
|
||||
reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。"
|
||||
rememberNoteVisibility: "記住貼文可見性"
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
|
@ -114,6 +114,7 @@ const defaultStoreSaveKeys: (keyof (typeof defaultStore)["state"])[] = [
|
|||
"swipeOnDesktop",
|
||||
"showAdminUpdates",
|
||||
"enableCustomKaTeXMacro",
|
||||
"enableEmojiReactions",
|
||||
];
|
||||
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
|
||||
"lightTheme",
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="enableEmojiReactions" class="_formBlock">
|
||||
{{ i18n.ts.enableEmojiReactions }}
|
||||
<template #caption>{{ i18n.ts.needReloadToApply }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<div v-if="enableEmojiReactions">
|
||||
<FromSlot class="_formBlock">
|
||||
<template #label>{{ i18n.ts.reactionSettingDescription }}</template>
|
||||
<template #label>{{
|
||||
i18n.ts.reactionSettingDescription
|
||||
}}</template>
|
||||
<div v-panel style="border-radius: 6px">
|
||||
<XDraggable
|
||||
v-model="reactions"
|
||||
|
@ -71,12 +79,15 @@
|
|||
{{ i18n.ts.preview }}</FormButton
|
||||
>
|
||||
<FormButton inline danger @click="setDefault"
|
||||
><i class="ph-arrow-counter-clockwise ph-bold ph-lg"></i>
|
||||
><i
|
||||
class="ph-arrow-counter-clockwise ph-bold ph-lg"
|
||||
></i>
|
||||
{{ i18n.ts.default }}</FormButton
|
||||
>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -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);
|
||||
|
|
|
@ -294,6 +294,10 @@ export const defaultStore = markRaw(
|
|||
where: "device",
|
||||
default: false,
|
||||
},
|
||||
enableEmojiReactions: {
|
||||
where: "account",
|
||||
default: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in New Issue