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 8588979db9
commit 0a173a3c1c
13 changed files with 330 additions and 83 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@
@close="dialog.close()" @close="dialog.close()"
@closed="emit('closed')" @closed="emit('closed')"
> >
<template #header>{{ i18n.ts.reactions }}</template> <template #header>{{ i18n.ts.reaction }}</template>
<MkSpacer :margin-min="20" :margin-max="28"> <MkSpacer :margin-min="20" :margin-max="28">
<div v-if="note" class="_gaps"> <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", "swipeOnDesktop",
"showAdminUpdates", "showAdminUpdates",
"enableCustomKaTeXMacro", "enableCustomKaTeXMacro",
"enableEmojiReactions",
]; ];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [ const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
"lightTheme", "lightTheme",

View File

@ -1,7 +1,15 @@
<template> <template>
<div class="_formRoot"> <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"> <FromSlot class="_formBlock">
<template #label>{{ i18n.ts.reactionSettingDescription }}</template> <template #label>{{
i18n.ts.reactionSettingDescription
}}</template>
<div v-panel style="border-radius: 6px"> <div v-panel style="border-radius: 6px">
<XDraggable <XDraggable
v-model="reactions" v-model="reactions"
@ -71,12 +79,15 @@
{{ i18n.ts.preview }}</FormButton {{ i18n.ts.preview }}</FormButton
> >
<FormButton inline danger @click="setDefault" <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 {{ i18n.ts.default }}</FormButton
> >
</div> </div>
</FormSection> </FormSection>
</div> </div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -108,6 +119,9 @@ const reactionPickerHeight = $computed(
const reactionPickerUseDrawerForMobile = $computed( const reactionPickerUseDrawerForMobile = $computed(
defaultStore.makeGetterSetter("reactionPickerUseDrawerForMobile") defaultStore.makeGetterSetter("reactionPickerUseDrawerForMobile")
); );
const enableEmojiReactions = $computed(
defaultStore.makeGetterSetter("enableEmojiReactions")
);
function save() { function save() {
defaultStore.set("reactions", reactions); defaultStore.set("reactions", reactions);

View File

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