Emoji reaction handling fixes
ci/woodpecker/push/ociImagePush Pipeline was successful Details

This commit is contained in:
Natty 2023-11-07 03:11:22 +01:00
parent 4cb7431681
commit 0bb0c775dc
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
35 changed files with 548 additions and 357 deletions

View File

@ -27,6 +27,7 @@
"@types/uuid": "8.3.4", "@types/uuid": "8.3.4",
"@vitejs/plugin-vue": "4.2.3", "@vitejs/plugin-vue": "4.2.3",
"@vue/compiler-sfc": "3.3.4", "@vue/compiler-sfc": "3.3.4",
"@vue/runtime-core": "3.3.4",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autosize": "5.0.2", "autosize": "5.0.2",
"blurhash": "1.1.5", "blurhash": "1.1.5",

View File

@ -260,6 +260,8 @@ import {
magHasReacted, magHasReacted,
magIsRenote, magIsRenote,
magReactionCount, magReactionCount,
magReactionSelf,
magReactionToLegacy,
} from "@/scripts-mag/mag-util"; } from "@/scripts-mag/mag-util";
const router = useRouter(); const router = useRouter();
@ -353,7 +355,7 @@ function react(viaKeyboard = false): void {
(reaction) => { (reaction) => {
os.api("notes/reactions/create", { os.api("notes/reactions/create", {
noteId: appearNote.id, noteId: appearNote.id,
reaction: reaction, reaction: magReactionToLegacy(reaction),
}); });
}, },
() => { () => {
@ -362,8 +364,8 @@ function react(viaKeyboard = false): void {
); );
} }
function undoReact(note): void { function undoReact(note: packed.PackNoteMaybeFull): void {
const oldReaction = note.myReaction; const oldReaction = magReactionSelf(note);
if (!oldReaction) return; if (!oldReaction) return;
os.api("notes/reactions/delete", { os.api("notes/reactions/delete", {
noteId: note.id, noteId: note.id,

View File

@ -170,7 +170,11 @@ import { useNoteCapture } from "@/scripts/use-note-capture";
import { stream } from "@/stream"; import { stream } from "@/stream";
import { NoteUpdatedEvent } from "calckey-js/built/streaming.types"; import { NoteUpdatedEvent } from "calckey-js/built/streaming.types";
import { packed } from "magnetar-common"; import { packed } from "magnetar-common";
import { magIsRenote, magReactionCount } from "@/scripts-mag/mag-util"; import {
magIsRenote,
magReactionCount,
magReactionToLegacy,
} from "@/scripts-mag/mag-util";
import MagNote from "@/components/MagNote.vue"; import MagNote from "@/components/MagNote.vue";
const props = defineProps<{ const props = defineProps<{
@ -254,7 +258,7 @@ function react(viaKeyboard = false): void {
(reaction) => { (reaction) => {
os.api("notes/reactions/create", { os.api("notes/reactions/create", {
noteId: note.id, noteId: note.id,
reaction: reaction, reaction: magReactionToLegacy(reaction),
}); });
}, },
() => { () => {

View File

@ -34,16 +34,19 @@
class="_button item" class="_button item"
@click="emit('chosen', emoji, $event)" @click="emit('chosen', emoji, $event)"
> >
<MkEmoji class="emoji" :emoji="emoji" :normal="true" /> <MagEmoji class="emoji" :emoji="emoji" :normal="true" />
</button> </button>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch, onMounted } from "vue"; import { ref, watch, onMounted, toRaw } from "vue";
import { addSkinTone } from "@/scripts/emojilist"; import { addSkinTone } from "@/scripts/emojilist";
import emojiComponents from "unicode-emoji-json/data-emoji-components.json"; import emojiComponents from "unicode-emoji-json/data-emoji-components.json";
import { types } from "magnetar-common";
import { magConvertReaction, magIsUnicodeEmoji } from "@/scripts-mag/mag-util";
import { instance } from "@/instance";
const props = defineProps<{ const props = defineProps<{
emojis: string[]; emojis: string[];
@ -52,10 +55,19 @@ const props = defineProps<{
skinTones?: string[]; skinTones?: string[];
}>(); }>();
const localEmojis = ref([...props.emojis]); const resolveEmojis = (e: string) =>
magConvertReaction(e, (name) => {
return instance.emojis.find((e) => e.name === name)?.url!;
});
const localEmojis = ref<types.Reaction[]>(props.emojis.map(resolveEmojis));
function applyUnicodeSkinTone(custom?: number) { function applyUnicodeSkinTone(custom?: number) {
for (let i = 0; i < localEmojis.value.length; i++) { for (let i = 0; i < localEmojis.value.length; i++) {
let rawEmoji: types.ReactionUnicode = toRaw(localEmojis[i]);
if (!magIsUnicodeEmoji(rawEmoji)) continue;
if ( if (
[ [
emojiComponents.light_skin_tone, emojiComponents.light_skin_tone,
@ -63,16 +75,16 @@ function applyUnicodeSkinTone(custom?: number) {
emojiComponents.medium_skin_tone, emojiComponents.medium_skin_tone,
emojiComponents.medium_dark_skin_tone, emojiComponents.medium_dark_skin_tone,
emojiComponents.dark_skin_tone, emojiComponents.dark_skin_tone,
].some((v) => localEmojis.value[i].endsWith(v)) ].some((v) => rawEmoji.endsWith(v))
) { ) {
localEmojis.value[i] = localEmojis.value[i].slice(0, -1); rawEmoji = rawEmoji.slice(0, -1);
} }
localEmojis.value[i] = addSkinTone(localEmojis.value[i], custom); localEmojis.value[i] = addSkinTone(rawEmoji, custom);
} }
} }
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "chosen", v: string, event: MouseEvent): void; (ev: "chosen", v: types.Reaction, event: MouseEvent): void;
}>(); }>();
const shown = ref(!!props.initialShown); const shown = ref(!!props.initialShown);
@ -86,7 +98,7 @@ onMounted(() => {
watch( watch(
() => props.emojis, () => props.emojis,
(newVal) => { (newVal) => {
localEmojis.value = [...newVal]; localEmojis.value = newVal.map(resolveEmojis);
} }
); );
</script> </script>

View File

@ -26,9 +26,9 @@
class="_button item" class="_button item"
:title="emoji.name" :title="emoji.name"
tabindex="0" tabindex="0"
@click="chosen(emoji, $event)" @click="chosen(magCustomEmoji(emoji), $event)"
> >
<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>--> <!--<MagEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
<img <img
class="emoji" class="emoji"
:src=" :src="
@ -46,9 +46,9 @@
class="_button item" class="_button item"
:title="emoji.slug" :title="emoji.slug"
tabindex="0" tabindex="0"
@click="chosen(emoji, $event)" @click="chosen(magUnicodeEmoji(emoji), $event)"
> >
<MkEmoji class="emoji" :emoji="emoji.emoji" /> <MagEmoji class="emoji" :emoji="emoji.emoji" />
</button> </button>
</div> </div>
</section> </section>
@ -57,13 +57,13 @@
<section v-if="showPinned"> <section v-if="showPinned">
<div class="body"> <div class="body">
<button <button
v-for="emoji in pinned" v-for="emoji in resolvedPinned"
:key="emoji" :key="emoji"
class="_button item" class="_button item"
tabindex="0" tabindex="0"
@click="chosen(emoji, $event)" @click="chosen(emoji, $event)"
> >
<MkEmoji <MagEmoji
class="emoji" class="emoji"
:emoji="emoji" :emoji="emoji"
:normal="true" :normal="true"
@ -79,12 +79,12 @@
</header> </header>
<div class="body"> <div class="body">
<button <button
v-for="emoji in recentlyUsedEmojis" v-for="emoji in resolvedRecent"
:key="emoji" :key="emoji"
class="_button item" class="_button item"
@click="chosen(emoji, $event)" @click="chosen(emoji, $event)"
> >
<MkEmoji <MagEmoji
class="emoji" class="emoji"
:emoji="emoji" :emoji="emoji"
:normal="true" :normal="true"
@ -181,6 +181,13 @@ import { emojiCategories, instance } from "@/instance";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { FocusTrap } from "focus-trap-vue"; import { FocusTrap } from "focus-trap-vue";
import { types } from "magnetar-common";
import {
magConvertReaction,
magCustomEmoji,
magReactionToLegacy,
magUnicodeEmoji,
} from "@/scripts-mag/mag-util";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -195,7 +202,7 @@ const props = withDefaults(
); );
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "chosen", v: string): void; (ev: "chosen", v: types.Reaction): void;
}>(); }>();
const search = ref<HTMLInputElement>(); const search = ref<HTMLInputElement>();
@ -226,6 +233,16 @@ const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]); const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
const tab = ref<"index" | "custom" | "unicode" | "tags">("index"); const tab = ref<"index" | "custom" | "unicode" | "tags">("index");
const resolveEmojis = (e: string) =>
magConvertReaction(e, (name) => {
return customEmojis.find((e) => e.name === name)?.url!;
});
const resolvedPinned = computed(() => pinned.value.map(resolveEmojis));
const resolvedRecent = computed(() =>
recentlyUsedEmojis.value.map(resolveEmojis)
);
watch(q, () => { watch(q, () => {
if (emojis.value) emojis.value.scrollTop = 0; if (emojis.value) emojis.value.scrollTop = 0;
@ -400,13 +417,7 @@ function reset() {
q.value = ""; q.value = "";
} }
function getKey( function chosen(emoji: types.Reaction, ev?: MouseEvent) {
emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef
): string {
return typeof emoji === "string" ? emoji : emoji.emoji || `:${emoji.name}:`;
}
function chosen(emoji: any, ev?: MouseEvent) {
const el = const el =
ev && ev &&
((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined); ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
@ -417,8 +428,9 @@ function chosen(emoji: any, ev?: MouseEvent) {
os.popup(Ripple, { x, y }, {}, "end"); os.popup(Ripple, { x, y }, {}, "end");
} }
const key = getKey(emoji); emit("chosen", emoji);
emit("chosen", key);
const key = magReactionToLegacy(emoji);
// 使 // 使
if (!pinned.value.includes(key)) { if (!pinned.value.includes(key)) {
@ -443,22 +455,22 @@ function done(query?: any): boolean | void {
const q2 = query.replaceAll(":", ""); const q2 = query.replaceAll(":", "");
const exactMatchCustom = customEmojis.find((emoji) => emoji.name === q2); const exactMatchCustom = customEmojis.find((emoji) => emoji.name === q2);
if (exactMatchCustom) { if (exactMatchCustom) {
chosen(exactMatchCustom); chosen(magCustomEmoji(exactMatchCustom));
return true; return true;
} }
const exactMatchUnicode = emojilist.find( const exactMatchUnicode = emojilist.find(
(emoji) => emoji.emoji === q2 || emoji.slug === q2 (emoji) => emoji.emoji === q2 || emoji.slug === q2
); );
if (exactMatchUnicode) { if (exactMatchUnicode) {
chosen(exactMatchUnicode); chosen(magUnicodeEmoji(exactMatchUnicode));
return true; return true;
} }
if (searchResultCustom.value.length > 0) { if (searchResultCustom.value.length > 0) {
chosen(searchResultCustom.value[0]); chosen(magCustomEmoji(searchResultCustom.value[0]));
return true; return true;
} }
if (searchResultUnicode.value.length > 0) { if (searchResultUnicode.value.length > 0) {
chosen(searchResultUnicode.value[0]); chosen(magUnicodeEmoji(searchResultUnicode.value[0]));
return true; return true;
} }
} }

View File

@ -35,6 +35,7 @@ import { ref } from "vue";
import MkModal from "@/components/MkModal.vue"; import MkModal from "@/components/MkModal.vue";
import MkEmojiPicker from "@/components/MkEmojiPicker.vue"; import MkEmojiPicker from "@/components/MkEmojiPicker.vue";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { types } from "magnetar-common";
withDefaults( withDefaults(
defineProps<{ defineProps<{
@ -51,7 +52,7 @@ withDefaults(
); );
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "done", v: any): void; (ev: "done", v: types.Reaction): void;
(ev: "close"): void; (ev: "close"): void;
(ev: "closed"): void; (ev: "closed"): void;
}>(); }>();
@ -59,7 +60,7 @@ const emit = defineEmits<{
const modal = ref<InstanceType<typeof MkModal>>(); const modal = ref<InstanceType<typeof MkModal>>();
const picker = ref<InstanceType<typeof MkEmojiPicker>>(); const picker = ref<InstanceType<typeof MkEmojiPicker>>();
function chosen(emoji: any) { function chosen(emoji: types.Reaction) {
emit("done", emoji); emit("done", emoji);
modal.value?.close(); modal.value?.close();
} }

View File

@ -264,6 +264,7 @@ import { getNoteMenu } from "@/scripts/get-note-menu";
import { useNoteCapture } from "@/scripts/use-note-capture"; import { useNoteCapture } from "@/scripts/use-note-capture";
import { notePage } from "@/filters/note"; import { notePage } from "@/filters/note";
import { getNoteSummary } from "@/scripts/get-note-summary"; import { getNoteSummary } from "@/scripts/get-note-summary";
import { magReactionToLegacy } from "@/scripts-mag/mag-util";
const router = useRouter(); const router = useRouter();
@ -360,7 +361,7 @@ function react(viaKeyboard = false): void {
(reaction) => { (reaction) => {
os.api("notes/reactions/create", { os.api("notes/reactions/create", {
noteId: appearNote.id, noteId: appearNote.id,
reaction: reaction, reaction: magReactionToLegacy(reaction),
}); });
}, },
() => { () => {

View File

@ -222,6 +222,7 @@ import {
magHasReacted, magHasReacted,
magIsRenote, magIsRenote,
magReactionCount, magReactionCount,
magReactionToLegacy,
magTransMap, magTransMap,
magTransProperty, magTransProperty,
} from "@/scripts-mag/mag-util"; } from "@/scripts-mag/mag-util";
@ -312,7 +313,7 @@ function react(viaKeyboard = false): void {
(reaction) => { (reaction) => {
os.api("notes/reactions/create", { os.api("notes/reactions/create", {
noteId: appearNote.id, noteId: appearNote.id,
reaction: reaction, reaction: magReactionToLegacy(reaction),
}); });
}, },
() => { () => {

View File

@ -64,27 +64,23 @@
class="ph-microphone-stage ph-bold" class="ph-microphone-stage ph-bold"
></i> ></i>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> <!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<XReactionIcon <MagEmoji
v-else-if=" v-else-if="
showEmojiReactions && notification.type === 'reaction' showEmojiReactions && notification.type === 'reaction'
" "
ref="reactionRef" ref="reactionRef"
:reaction=" :emoji="normalizeNotifReaction(notification)"
notification.reaction :is-reaction="true"
? notification.reaction.replace( :normal="true"
/^:(\w+):$/,
':$1@.:'
)
: notification.reaction
"
:custom-emojis="notification.note.emojis"
:no-style="true" :no-style="true"
/> />
<XReactionIcon <MagEmoji
v-else-if=" v-else-if="
!showEmojiReactions && notification.type === 'reaction' !showEmojiReactions && notification.type === 'reaction'
" "
:reaction="defaultReaction" :emoji="defaultReaction"
:is-reaction="true"
:normal="true"
:no-style="true" :no-style="true"
/> />
</div> </div>
@ -273,7 +269,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, ref, watch } from "vue"; import { onMounted, onUnmounted, ref, watch } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import XReactionIcon from "@/components/MkReactionIcon.vue";
import MkFollowButton from "@/components/MkFollowButton.vue"; import MkFollowButton from "@/components/MkFollowButton.vue";
import XReactionTooltip from "@/components/MkReactionTooltip.vue"; import XReactionTooltip from "@/components/MkReactionTooltip.vue";
import { getNoteSummary } from "@/scripts/get-note-summary"; import { getNoteSummary } from "@/scripts/get-note-summary";
@ -286,6 +281,8 @@ import { useTooltip } from "@/scripts/use-tooltip";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { instance } from "@/instance"; import { instance } from "@/instance";
import MkFollowApproveButton from "@/components/MkFollowApproveButton.vue"; import MkFollowApproveButton from "@/components/MkFollowApproveButton.vue";
import { magConvertReaction, magIsCustomEmoji } from "@/scripts-mag/mag-util";
import { types } from "magnetar-common";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -310,6 +307,27 @@ const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReact
? instance.defaultReaction ? instance.defaultReaction
: "⭐"; : "⭐";
function normalizeNotifReaction(
notification: misskey.entities.Notification & { type: "reaction" }
): types.Reaction {
return notification.reaction
? magConvertReaction(
notification.reaction,
(name, host) =>
notification.note.emojis.find((e) => {
const parsed = magConvertReaction(`:${e.name}:`, e.url);
if (!magIsCustomEmoji(parsed)) return false;
return (
parsed.name === name &&
(parsed.host ?? null) === (host ?? null)
);
})?.url!
)
: notification.reaction;
}
let readObserver: IntersectionObserver | undefined; let readObserver: IntersectionObserver | undefined;
let connection; let connection;
@ -357,14 +375,13 @@ const rejectGroupInvitation = () => {
}; };
useTooltip(reactionRef, (showing) => { useTooltip(reactionRef, (showing) => {
if (props.notification.type !== "reaction") return;
os.popup( os.popup(
XReactionTooltip, XReactionTooltip,
{ {
showing, showing,
reaction: props.notification.reaction reaction: normalizeNotifReaction(props.notification),
? props.notification.reaction.replace(/^:(\w+):$/, ":$1@.:")
: props.notification.reaction,
emojis: props.notification.note.emojis,
targetElement: reactionRef.value.$el, targetElement: reactionRef.value.$el,
}, },
{}, {},

View File

@ -3,23 +3,25 @@
<div :class="$style.tabs"> <div :class="$style.tabs">
<button <button
v-for="reaction in reactions" v-for="reaction in reactions"
:key="reaction" :key="reaction[0]"
:class="[$style.tab, { [$style.tabActive]: tab === reaction }]" :class="[
$style.tab,
{
[$style.tabActive]:
tab === magReactionToLegacy(reaction[0]),
},
]"
class="_button" class="_button"
@click="tab = reaction" @click="tab = magReactionToLegacy(reaction[0])"
> >
<MkReactionIcon <MagEmoji
ref="reactionRef" ref="reactionRef"
:reaction=" :emoji="reaction[0]"
reaction :is-reaction="true"
? reaction.replace(/^:(\w+):$/, ':$1@.:') :normal="true"
: reaction
"
:custom-emojis="note.emojis"
/> />
<span style="margin-left: 4px">{{
note.reactions[reaction] <span style="margin-left: 4px">{{ reaction[1] }}</span>
}}</span>
</button> </button>
</div> </div>
<MkUserCardMini <MkUserCardMini
@ -37,19 +39,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, watch } from "vue"; import { onMounted, watch } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import MkReactionIcon from "@/components/MkReactionIcon.vue";
import MkUserCardMini from "@/components/MkUserCardMini.vue"; import MkUserCardMini from "@/components/MkUserCardMini.vue";
import { i18n } from "@/i18n";
import * as os from "@/os"; import * as os from "@/os";
import { endpoints, types, packed } from "magnetar-common";
import { magReactionToLegacy } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
noteId: misskey.entities.Note["id"]; noteId: string;
}>(); }>();
let note = $ref<misskey.entities.Note>();
let tab = $ref<string>(); let tab = $ref<string>();
let reactions = $ref<string[]>(); let note = $ref<packed.PackNoteBase>();
let users = $ref(); let reactions = $ref<types.ReactionPair[]>();
let users = $ref<misskey.entities.UserLite[]>();
watch($$(tab), async () => { watch($$(tab), async () => {
const res = await os.api("notes/reactions", { const res = await os.api("notes/reactions", {
@ -62,11 +64,9 @@ watch($$(tab), async () => {
}); });
onMounted(() => { onMounted(() => {
os.api("notes/show", { os.magApi(endpoints.GetNoteById, {}, { id: props.noteId }).then((res) => {
noteId: props.noteId, reactions = res.reactions;
}).then((res) => { tab = magReactionToLegacy(reactions.map((r) => r[0])[0]);
reactions = Object.keys(res.reactions);
tab = reactions[0];
note = res; note = res;
}); });
}); });

View File

@ -1,7 +1,6 @@
<template> <template>
<MkEmoji <MagEmoji
:emoji="reaction" :emoji="reaction"
:custom-emojis="customEmojis || []"
:is-reaction="true" :is-reaction="true"
:normal="true" :normal="true"
:no-style="noStyle" :no-style="noStyle"
@ -9,11 +8,10 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as Misskey from "calckey-js"; import { types } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
reaction: string; reaction: types.Reaction;
customEmojis?: Pick<Misskey.entities.CustomEmoji, "name" | "url">[];
noStyle?: boolean; noStyle?: boolean;
}>(); }>();
</script> </script>

View File

@ -6,13 +6,17 @@
@closed="emit('closed')" @closed="emit('closed')"
> >
<div class="beeadbfb"> <div class="beeadbfb">
<XReactionIcon <MagEmoji
:reaction="reaction"
:custom-emojis="emojis"
class="icon" class="icon"
:class="{
unicodeReaction: magIsUnicodeEmoji(reaction),
}"
:emoji="reaction"
:is-reaction="true"
:normal="true"
:no-style="true" :no-style="true"
/> />
<div class="name">{{ reaction.replace("@.", "") }}</div> <div class="name">{{ magReactionToLegacy(reaction) }}</div>
</div> </div>
</MkTooltip> </MkTooltip>
</template> </template>
@ -20,11 +24,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import {} from "vue"; import {} from "vue";
import MkTooltip from "./MkTooltip.vue"; import MkTooltip from "./MkTooltip.vue";
import XReactionIcon from "@/components/MkReactionIcon.vue"; import { magIsUnicodeEmoji, magReactionToLegacy } from "@/scripts-mag/mag-util";
import { types } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
reaction: string; reaction: types.Reaction;
emojis: any[]; // TODO
targetElement: HTMLElement; targetElement: HTMLElement;
}>(); }>();
@ -40,8 +44,11 @@ const emit = defineEmits<{
> .icon { > .icon {
display: block; display: block;
width: 60px; width: 60px;
font-size: 60px; // unicodewidth
margin: 0 auto; margin: 0 auto;
&.unicodeReaction {
font-size: 60px;
}
} }
> .name { > .name {

View File

@ -7,13 +7,17 @@
> >
<div class="bqxuuuey"> <div class="bqxuuuey">
<div class="reaction"> <div class="reaction">
<XReactionIcon <MagEmoji
:reaction="reaction"
:custom-emojis="emojis"
class="icon" class="icon"
:class="{
unicodeReaction: magIsUnicodeEmoji(reaction),
}"
:emoji="reaction"
:is-reaction="true"
:normal="true"
:no-style="true" :no-style="true"
/> />
<div class="name">{{ reaction.replace("@.", "") }}</div> <div class="name">{{ magReactionToLegacy(reaction) }}</div>
</div> </div>
<div class="users"> <div class="users">
<div v-for="u in users" :key="u.id" class="user"> <div v-for="u in users" :key="u.id" class="user">
@ -31,13 +35,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import {} from "vue"; import {} from "vue";
import MkTooltip from "./MkTooltip.vue"; import MkTooltip from "./MkTooltip.vue";
import XReactionIcon from "@/components/MkReactionIcon.vue"; import { magIsUnicodeEmoji, magReactionToLegacy } from "@/scripts-mag/mag-util";
import { types } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
reaction: string; reaction: types.Reaction;
users: any[]; // TODO users: any[]; // TODO
count: number; count: number;
emojis: any[]; // TODO
targetElement: HTMLElement; targetElement: HTMLElement;
}>(); }>();
@ -57,8 +61,11 @@ const emit = defineEmits<{
> .icon { > .icon {
display: block; display: block;
width: 60px; width: 60px;
font-size: 60px; // unicodewidth
margin: 0 auto; margin: 0 auto;
&.unicodeReaction {
font-size: 60px;
}
} }
> .name { > .name {

View File

@ -5,24 +5,17 @@
v-ripple="canToggle" v-ripple="canToggle"
class="hkzvhatu _button" class="hkzvhatu _button"
:class="{ :class="{
reacted: magReactionSelf(note) === reaction, reacted: magReactionSelf(note) === magReactionToLegacy(reaction),
canToggle, canToggle,
newlyAdded: !isInitial, newlyAdded: !isInitial,
}" }"
@click="toggleReaction()" @click="toggleReaction()"
> >
<XReactionIcon <MagEmoji
class="icon" class="icon"
:reaction="reaction" :emoji="reaction"
:custom-emojis="[ :is-reaction="true"
...note.emojis, :normal="true"
typeof url !== 'undefined'
? {
name: reaction.substring(1, reaction.length - 1),
url: url,
}
: undefined,
]"
/> />
<span class="count">{{ count }}</span> <span class="count">{{ count }}</span>
</button> </button>
@ -32,44 +25,45 @@
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import XDetails from "@/components/MkReactionsViewer.details.vue"; import XDetails from "@/components/MkReactionsViewer.details.vue";
import XReactionIcon from "@/components/MkReactionIcon.vue";
import * as os from "@/os"; import * as os from "@/os";
import { useTooltip } from "@/scripts/use-tooltip"; import { useTooltip } from "@/scripts/use-tooltip";
import { $i } from "@/account"; import { $i } from "@/account";
import { packed } from "magnetar-common"; import { packed, types } from "magnetar-common";
import { magReactionSelf } from "@/scripts-mag/mag-util"; import { magReactionSelf, magReactionToLegacy } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
reaction: string; reaction: types.Reaction;
count: number; count: number;
url?: string;
isInitial: boolean; isInitial: boolean;
note: packed.PackNoteMaybeFull | misskey.entities.Note; note: packed.PackNoteMaybeFull | misskey.entities.Note;
}>(); }>();
const buttonRef = ref<HTMLElement>(); const buttonRef = ref<HTMLElement>();
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i); const canToggle = computed(
() => !magReactionToLegacy(props.reaction).match(/@\w/) && $i
);
const toggleReaction = () => { const toggleReaction = () => {
if (!canToggle.value) return; if (!canToggle.value) return;
const oldReaction = magReactionSelf(props.note); const oldReaction = magReactionSelf(props.note);
const newReaction = magReactionToLegacy(props.reaction);
if (oldReaction) { if (oldReaction) {
os.api("notes/reactions/delete", { os.api("notes/reactions/delete", {
noteId: props.note.id, noteId: props.note.id,
}).then(() => { }).then(() => {
if (oldReaction !== props.reaction) { if (oldReaction !== newReaction) {
os.api("notes/reactions/create", { os.api("notes/reactions/create", {
noteId: props.note.id, noteId: props.note.id,
reaction: props.reaction, reaction: magReactionToLegacy(props.reaction),
}); });
} }
}); });
} else { } else {
os.api("notes/reactions/create", { os.api("notes/reactions/create", {
noteId: props.note.id, noteId: props.note.id,
reaction: props.reaction, reaction: magReactionToLegacy(props.reaction),
}); });
} }
}; };
@ -79,7 +73,7 @@ useTooltip(
async (showing) => { async (showing) => {
const reactions = await os.apiGet("notes/reactions", { const reactions = await os.apiGet("notes/reactions", {
noteId: props.note.id, noteId: props.note.id,
type: props.reaction, type: magReactionToLegacy(props.reaction),
limit: 11, limit: 11,
_cacheKey_: props.count, _cacheKey_: props.count,
}); });
@ -91,7 +85,6 @@ useTooltip(
{ {
showing, showing,
reaction: props.reaction, reaction: props.reaction,
emojis: props.note.emojis,
users, users,
count: props.count, count: props.count,
targetElement: buttonRef.value, targetElement: buttonRef.value,

View File

@ -1,14 +1,11 @@
<template> <template>
<div class="tdflqwzn" :class="{ isMe }"> <div class="tdflqwzn" :class="{ isMe }">
<XReaction <XReaction
v-for="r in Array.isArray(note.reactions) v-for="r in reactions"
? note.reactions :key="magReactionToLegacy(r[0])"
: Object.entries(note.reactions)" :reaction="r[0]"
:key="magReactionPairToLegacy(r)[0]" :count="r[1]"
:reaction="magReactionPairToLegacy(r)[0]" :is-initial="initialReactions.has(magReactionToLegacy(r[0]))"
:url="typeof r[0]['url'] !== 'undefined' ? r[0]['url'] : undefined"
:count="magReactionPairToLegacy(r)[1]"
:is-initial="initialReactions.has(magReactionPairToLegacy(r)[0])"
:note="note" :note="note"
/> />
</div> </div>
@ -19,14 +16,58 @@ import { computed } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import { $i } from "@/account"; import { $i } from "@/account";
import XReaction from "@/components/MkReactionsViewer.reaction.vue"; import XReaction from "@/components/MkReactionsViewer.reaction.vue";
import { packed } from "magnetar-common"; import { packed, types } from "magnetar-common";
import { magReactionPairToLegacy } from "@/scripts-mag/mag-util"; import {
magConvertReaction,
magReactionEquals,
magReactionToLegacy,
} from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
note: packed.PackNoteMaybeFull | misskey.entities.Note; note: packed.PackNoteMaybeFull | misskey.entities.Note;
}>(); }>();
const initialReactions = new Set(Object.keys(props.note.reactions)); const reactions = computed(() => {
const myReactionMk = (props.note as misskey.entities.Note).myReaction
? magConvertReaction((props.note as misskey.entities.Note).myReaction!)
: null;
return Array.isArray(props.note.reactions)
? props.note.reactions
: Object.entries(props.note.reactions)
.filter(([e]) => e)
.map(([e, cnt]) => {
const parsed = magConvertReaction(
e,
(name, host) =>
(
props.note
.emojis as misskey.entities.Note["emojis"]
).find((e) =>
magReactionEquals(
magConvertReaction(`:${e.name}:`),
{ name, host, url: null! }
)
)?.url ?? null!
);
return [
parsed,
cnt,
myReactionMk
? magReactionEquals(myReactionMk, parsed)
: undefined,
] as types.ReactionPair;
});
});
const initialReactions = new Set(
Array.isArray(props.note.reactions)
? props.note.reactions.map((v) => magReactionToLegacy(v[0]))
: Object.keys(props.note.reactions)
.map((v) => magConvertReaction(v))
.map(magReactionToLegacy)
);
const isMe = computed(() => $i && $i.id === props.note.user.id); const isMe = computed(() => $i && $i.id === props.note.user.id);
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<img <img
v-if="customEmoji" v-if="isCustom"
class="mk-emoji custom" class="mk-emoji custom"
:class="{ normal, noStyle }" :class="{ normal, noStyle }"
:src="url" :src="url"
@ -22,56 +22,43 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed } from "vue";
import * as Misskey from "calckey-js";
import { getStaticImageUrl } from "@/scripts/get-static-image-url"; import { getStaticImageUrl } from "@/scripts/get-static-image-url";
import { char2filePath } from "@/scripts/twemoji-base"; import { char2filePath } from "@/scripts/twemoji-base";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { instance } from "@/instance"; import { types } from "magnetar-common";
import { packed } from "magnetar-common"; import {
import { magTransProperty } from "@/scripts-mag/mag-util"; magIsCustomEmoji,
magIsUnicodeEmoji,
magReactionToLegacy,
} from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
emoji: string; emoji: types.Reaction;
normal?: boolean; normal?: boolean;
noStyle?: boolean; noStyle?: boolean;
customEmojis?: (
| packed.PackEmojiBase
| Pick<Misskey.entities.CustomEmoji, "name" | "url">
)[];
isReaction?: boolean; isReaction?: boolean;
}>(); }>();
const isCustom = computed(() => props.emoji.startsWith(":")); const isCustom = computed(() => magIsCustomEmoji(props.emoji));
const char = computed(() => (isCustom.value ? null : props.emoji));
const char = computed(() =>
magIsUnicodeEmoji(props.emoji) ? props.emoji : null
);
const useOsNativeEmojis = computed( const useOsNativeEmojis = computed(
() => defaultStore.state.useOsNativeEmojis && !props.isReaction () => defaultStore.state.useOsNativeEmojis && !props.isReaction
); );
const ce = computed(() => props.customEmojis ?? instance.emojis ?? []);
const customEmoji = computed(() =>
isCustom.value
? ce.value.find(
(x) =>
magTransProperty(x, "shortcode", "name") ===
props.emoji.substring(1, props.emoji.length - 1)
)
: null
);
const url = computed(() => { const url = computed(() => {
if (char.value) { if (char.value) {
return char2filePath(char.value); return char2filePath(char.value);
} else if (customEmoji?.value?.url) { } else if (magIsCustomEmoji(props.emoji)) {
return defaultStore.state.disableShowingAnimatedImages return defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(customEmoji.value.url) ? getStaticImageUrl(props.emoji.url)
: customEmoji.value.url; : props.emoji.url;
} else { } else {
return null; return null;
} }
}); });
const alt = computed(() => const alt = computed(() => magReactionToLegacy(props.emoji));
customEmoji.value
? `:${magTransProperty(customEmoji.value, "shortcode", "name")}:`
: char.value
);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,61 +0,0 @@
import { App } from "vue";
import Mfm from "./global/MkMisskeyFlavoredMarkdown.vue";
import MkA from "./global/MkA.vue";
import MkAcct from "./global/MkAcct.vue";
import MkAvatar from "./global/MkAvatar.vue";
import MkEmoji from "./global/MkEmoji.vue";
import MkUserName from "./global/MkUserName.vue";
import MkEllipsis from "./global/MkEllipsis.vue";
import MkTime from "./global/MkTime.vue";
import MkUrl from "./global/MkUrl.vue";
import I18n from "./global/i18n";
import RouterView from "./global/RouterView.vue";
import MkLoading from "./global/MkLoading.vue";
import MkError from "./global/MkError.vue";
import MkAd from "./global/MkAd.vue";
import MkPageHeader from "./global/MkPageHeader.vue";
import MkSpacer from "./global/MkSpacer.vue";
import MkStickyContainer from "./global/MkStickyContainer.vue";
export default function (app: App) {
app.component("I18n", I18n);
app.component("RouterView", RouterView);
app.component("Mfm", Mfm);
app.component("MkA", MkA);
app.component("MkAcct", MkAcct);
app.component("MkAvatar", MkAvatar);
app.component("MkEmoji", MkEmoji);
app.component("MkUserName", MkUserName);
app.component("MkEllipsis", MkEllipsis);
app.component("MkTime", MkTime);
app.component("MkUrl", MkUrl);
app.component("MkLoading", MkLoading);
app.component("MkError", MkError);
app.component("MkAd", MkAd);
app.component("MkPageHeader", MkPageHeader);
app.component("MkSpacer", MkSpacer);
app.component("MkStickyContainer", MkStickyContainer);
}
declare module "@vue/runtime-core" {
export interface GlobalComponents {
I18n: typeof I18n;
RouterView: typeof RouterView;
Mfm: typeof Mfm;
MkA: typeof MkA;
MkAcct: typeof MkAcct;
MkAvatar: typeof MkAvatar;
MkEmoji: typeof MkEmoji;
MkUserName: typeof MkUserName;
MkEllipsis: typeof MkEllipsis;
MkTime: typeof MkTime;
MkUrl: typeof MkUrl;
MkLoading: typeof MkLoading;
MkError: typeof MkError;
MkAd: typeof MkAd;
MkPageHeader: typeof MkPageHeader;
MkSpacer: typeof MkSpacer;
MkStickyContainer: typeof MkStickyContainer;
}
}

View File

@ -4,7 +4,6 @@ import type { VNode } from "vue";
import MkUrl from "@/components/global/MkUrl.vue"; import MkUrl from "@/components/global/MkUrl.vue";
import MkLink from "@/components/MkLink.vue"; import MkLink from "@/components/MkLink.vue";
import MkMention from "@/components/MkMention.vue"; import MkMention from "@/components/MkMention.vue";
import MkEmoji from "@/components/global/MkEmoji.vue";
import { concat } from "@/scripts/array"; import { concat } from "@/scripts/array";
import MkFormula from "@/components/MkFormula.vue"; import MkFormula from "@/components/MkFormula.vue";
import MkCode from "@/components/MkCode.vue"; import MkCode from "@/components/MkCode.vue";
@ -13,6 +12,12 @@ import MkA from "@/components/global/MkA.vue";
import { host } from "@/config"; import { host } from "@/config";
import { reducedMotion } from "@/scripts/reduced-motion"; import { reducedMotion } from "@/scripts/reduced-motion";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import MagEmoji from "@/components/global/MagEmoji.vue";
import {
magConvertReaction,
magReactionEquals,
magReactionToLegacy,
} from "@/scripts-mag/mag-util";
export default defineComponent({ export default defineComponent({
props: { props: {
@ -537,10 +542,20 @@ export default defineComponent({
case "emojiCode": { case "emojiCode": {
return [ return [
h(MkEmoji, { h(MagEmoji, {
key: Math.random(), key: Math.random(),
emoji: `:${token.props.name}:`, emoji: magConvertReaction(
customEmojis: this.customEmojis, `:${token.props.name}:`,
(name, host) =>
this.customEmojis.find((e) =>
magReactionEquals(
magConvertReaction(
`:${e.name}:`
),
{ name, host, url: null! }
)
)?.url ?? null
),
normal: this.plain, normal: this.plain,
}), }),
]; ];
@ -548,10 +563,9 @@ export default defineComponent({
case "unicodeEmoji": { case "unicodeEmoji": {
return [ return [
h(MkEmoji, { h(MagEmoji, {
key: Math.random(), key: Math.random(),
emoji: token.props.emoji, emoji: token.props.emoji,
customEmojis: this.customEmojis,
normal: this.plain, normal: this.plain,
}), }),
]; ];

View File

@ -23,7 +23,6 @@ import { compareVersions } from "compare-versions";
import widgets from "@/widgets"; import widgets from "@/widgets";
import directives from "@/directives"; import directives from "@/directives";
import components from "@/components";
import { lang, ui, version } from "@/config"; import { lang, ui, version } from "@/config";
import { applyTheme } from "@/scripts/theme"; import { applyTheme } from "@/scripts/theme";
import { isDeviceDarkmode } from "@/scripts/is-device-darkmode"; import { isDeviceDarkmode } from "@/scripts/is-device-darkmode";
@ -43,6 +42,68 @@ import { reactionPicker } from "@/scripts/reaction-picker";
import { getUrlWithoutLoginId } from "@/scripts/login-id"; import { getUrlWithoutLoginId } from "@/scripts/login-id";
import { getAccountFromId } from "@/scripts/get-account-from-id"; import { getAccountFromId } from "@/scripts/get-account-from-id";
import { App } from "vue";
import Mfm from "./components/global/MkMisskeyFlavoredMarkdown.vue";
import MkA from "./components/global/MkA.vue";
import MkAcct from "./components/global/MkAcct.vue";
import MkAvatar from "./components/global/MkAvatar.vue";
import MagEmoji from "./components/global/MagEmoji.vue";
import MkUserName from "./components/global/MkUserName.vue";
import MkEllipsis from "./components/global/MkEllipsis.vue";
import MkTime from "./components/global/MkTime.vue";
import MkUrl from "./components/global/MkUrl.vue";
import I18n from "./components/global/i18n";
import RouterView from "./components/global/RouterView.vue";
import MkLoading from "./components/global/MkLoading.vue";
import MkError from "./components/global/MkError.vue";
import MkAd from "./components/global/MkAd.vue";
import MkPageHeader from "./components/global/MkPageHeader.vue";
import MkSpacer from "./components/global/MkSpacer.vue";
import MkStickyContainer from "./components/global/MkStickyContainer.vue";
function globalComponents(app: App) {
app.component("I18n", I18n);
app.component("RouterView", RouterView);
app.component("Mfm", Mfm);
app.component("MkA", MkA);
app.component("MkAcct", MkAcct);
app.component("MkAvatar", MkAvatar);
app.component("MagEmoji", MagEmoji);
app.component("MkUserName", MkUserName);
app.component("MkEllipsis", MkEllipsis);
app.component("MkTime", MkTime);
app.component("MkUrl", MkUrl);
app.component("MkLoading", MkLoading);
app.component("MkError", MkError);
app.component("MkAd", MkAd);
app.component("MkPageHeader", MkPageHeader);
app.component("MkSpacer", MkSpacer);
app.component("MkStickyContainer", MkStickyContainer);
}
declare module "@vue/runtime-core" {
export interface GlobalComponents {
I18n: typeof I18n;
RouterView: typeof RouterView;
Mfm: typeof Mfm;
MkA: typeof MkA;
MkAcct: typeof MkAcct;
MkAvatar: typeof MkAvatar;
MagEmoji: typeof MagEmoji;
MkUserName: typeof MkUserName;
MkEllipsis: typeof MkEllipsis;
MkTime: typeof MkTime;
MkUrl: typeof MkUrl;
MkLoading: typeof MkLoading;
MkError: typeof MkError;
MkAd: typeof MkAd;
MkPageHeader: typeof MkPageHeader;
MkSpacer: typeof MkSpacer;
MkStickyContainer: typeof MkStickyContainer;
}
}
const accounts = localStorage.getItem("accounts"); const accounts = localStorage.getItem("accounts");
if (accounts) { if (accounts) {
set("accounts", JSON.parse(accounts)); set("accounts", JSON.parse(accounts));
@ -208,7 +269,7 @@ function checkForSplash() {
widgets(app); widgets(app);
directives(app); directives(app);
components(app); globalComponents(app);
checkForSplash(); checkForSplash();

View File

@ -19,6 +19,8 @@ import {
MagApiClient, MagApiClient,
Method, Method,
} from "magnetar-common"; } from "magnetar-common";
import { magReactionToLegacy } from "@/scripts-mag/mag-util";
import { types } from "magnetar-common";
export const pendingApiRequestsCount = ref(0); export const pendingApiRequestsCount = ref(0);
@ -814,8 +816,8 @@ export async function pickEmoji(src: HTMLElement | null, opts) {
...opts, ...opts,
}, },
{ {
done: (emoji) => { done: (emoji: types.Reaction) => {
resolve(emoji); resolve(magReactionToLegacy(emoji));
}, },
}, },
"closed" "closed"
@ -910,11 +912,11 @@ export async function openEmojiPicker(
...opts, ...opts,
}, },
{ {
chosen: (emoji) => { chosen: (emoji: types.Reaction) => {
insertTextAtCursor(activeTextarea, emoji); insertTextAtCursor(activeTextarea, magReactionToLegacy(emoji));
}, },
done: (emoji) => { done: (emoji: types.Reaction) => {
insertTextAtCursor(activeTextarea, emoji); insertTextAtCursor(activeTextarea, magReactionToLegacy(emoji));
}, },
closed: () => { closed: () => {
openingEmojiPicker!.dispose(); openingEmojiPicker!.dispose();

View File

@ -32,7 +32,7 @@
:class="{ :class="{
_physics_circle_: !emoji.emoji.startsWith(':'), _physics_circle_: !emoji.emoji.startsWith(':'),
}" }"
><MkEmoji ><MagEmoji
class="emoji" class="emoji"
:emoji="emoji.emoji" :emoji="emoji.emoji"
:custom-emojis="$instance.emojis" :custom-emojis="$instance.emojis"

View File

@ -130,13 +130,22 @@
i18n.ts.defaultReaction i18n.ts.defaultReaction
}}</template> }}</template>
<option value="⭐"> <option value="⭐">
<MkEmoji emoji="⭐" style="height: 1.7em" /> <MagEmoji
emoji="⭐"
style="height: 1.7em"
/>
</option> </option>
<option value="👍"> <option value="👍">
<MkEmoji emoji="👍" style="height: 1.7em" /> <MagEmoji
emoji="👍"
style="height: 1.7em"
/>
</option> </option>
<option value="❤️"> <option value="❤️">
<MkEmoji emoji="❤️" style="height: 1.7em" /> <MagEmoji
emoji="❤️"
style="height: 1.7em"
/>
</option> </option>
<option value="custom"> <option value="custom">
<FormInput <FormInput

View File

@ -11,9 +11,9 @@
}}</template> }}</template>
<div v-panel style="border-radius: 6px"> <div v-panel style="border-radius: 6px">
<XDraggable <XDraggable
v-model="reactions" v-model="reactionsResolved"
class="zoaiodol" class="zoaiodol"
:item-key="(item) => item" :item-key="(item) => magReactionToLegacy(item)"
animation="150" animation="150"
delay="100" delay="100"
delay-on-touch-only="true" delay-on-touch-only="true"
@ -23,7 +23,7 @@
class="_button item" class="_button item"
@click="remove(element, $event)" @click="remove(element, $event)"
> >
<MkEmoji <MagEmoji
:emoji="element" :emoji="element"
style="height: 1.7em" style="height: 1.7em"
class="emoji" class="emoji"
@ -48,22 +48,22 @@
<FormRadios v-model="reactionPickerSkinTone" class="_formBlock"> <FormRadios v-model="reactionPickerSkinTone" class="_formBlock">
<template #label>{{ i18n.ts.reactionPickerSkinTone }}</template> <template #label>{{ i18n.ts.reactionPickerSkinTone }}</template>
<option :value="1"> <option :value="1">
<MkEmoji style="height: 1.7em" emoji="✌️" /> <MagEmoji style="height: 1.7em" emoji="✌️" />
</option> </option>
<option :value="6"> <option :value="6">
<MkEmoji style="height: 1.7em" emoji="✌🏿" /> <MagEmoji style="height: 1.7em" emoji="✌🏿" />
</option> </option>
<option :value="5"> <option :value="5">
<MkEmoji style="height: 1.7em" emoji="✌🏾" /> <MagEmoji style="height: 1.7em" emoji="✌🏾" />
</option> </option>
<option :value="4"> <option :value="4">
<MkEmoji style="height: 1.7em" emoji="✌🏽" /> <MagEmoji style="height: 1.7em" emoji="✌🏽" />
</option> </option>
<option :value="3"> <option :value="3">
<MkEmoji style="height: 1.7em" emoji="✌🏼" /> <MagEmoji style="height: 1.7em" emoji="✌🏼" />
</option> </option>
<option :value="2"> <option :value="2">
<MkEmoji style="height: 1.7em" emoji="✌🏻" /> <MagEmoji style="height: 1.7em" emoji="✌🏻" />
</option> </option>
</FormRadios> </FormRadios>
<FormRadios v-model="reactionPickerSize" class="_formBlock"> <FormRadios v-model="reactionPickerSize" class="_formBlock">
@ -123,7 +123,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, watch } from "vue"; import { computed, defineAsyncComponent, watch } from "vue";
import XDraggable from "vuedraggable"; import XDraggable from "vuedraggable";
import FormRadios from "@/components/form/radios.vue"; import FormRadios from "@/components/form/radios.vue";
import FromSlot from "@/components/form/slot.vue"; import FromSlot from "@/components/form/slot.vue";
@ -135,6 +135,13 @@ import { defaultStore } from "@/store";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
import { unisonReload } from "@/scripts/unison-reload"; import { unisonReload } from "@/scripts/unison-reload";
import MagEmoji from "@/components/global/MagEmoji.vue";
import {
magConvertReaction,
magReactionToLegacy,
} from "@/scripts-mag/mag-util";
import { instance } from "@/instance";
import { types } from "magnetar-common";
async function reloadAsk() { async function reloadAsk() {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
@ -146,8 +153,15 @@ async function reloadAsk() {
unisonReload(); unisonReload();
} }
const resolveEmojis = (e: string) =>
magConvertReaction(e, (name) => {
return instance.emojis.find((e) => e.name === name)?.url!;
});
let reactions = $ref(structuredClone(defaultStore.state.reactions)); let reactions = $ref(structuredClone(defaultStore.state.reactions));
let reactionsResolved = computed(() => reactions.map(resolveEmojis));
const reactionPickerSkinTone = $computed( const reactionPickerSkinTone = $computed(
defaultStore.makeGetterSetter("reactionPickerSkinTone") defaultStore.makeGetterSetter("reactionPickerSkinTone")
); );
@ -174,13 +188,15 @@ function save() {
defaultStore.set("reactions", reactions); defaultStore.set("reactions", reactions);
} }
function remove(reaction, ev: MouseEvent) { function remove(reaction: types.Reaction, ev: MouseEvent) {
os.popupMenu( os.popupMenu(
[ [
{ {
text: i18n.ts.remove, text: i18n.ts.remove,
action: () => { action: () => {
reactions = reactions.filter((x) => x !== reaction); reactions = reactions.filter(
(x) => x !== magReactionToLegacy(reaction)
);
}, },
}, },
], ],

View File

@ -7,17 +7,17 @@
<div class="shape2"></div> <div class="shape2"></div>
<img src="/client-assets/misskey.svg" class="misskey" /> <img src="/client-assets/misskey.svg" class="misskey" />
<div class="emojis"> <div class="emojis">
<MkEmoji :normal="true" :no-style="true" emoji="⭐" /> <MagEmoji :normal="true" :no-style="true" emoji="⭐" />
<MkEmoji :normal="true" :no-style="true" emoji="❤️" /> <MagEmoji :normal="true" :no-style="true" emoji="❤️" />
<MkEmoji :normal="true" :no-style="true" emoji="😆" /> <MagEmoji :normal="true" :no-style="true" emoji="😆" />
<MkEmoji :normal="true" :no-style="true" emoji="🤔" /> <MagEmoji :normal="true" :no-style="true" emoji="🤔" />
<MkEmoji :normal="true" :no-style="true" emoji="😮" /> <MagEmoji :normal="true" :no-style="true" emoji="😮" />
<MkEmoji :normal="true" :no-style="true" emoji="🎉" /> <MagEmoji :normal="true" :no-style="true" emoji="🎉" />
<MkEmoji :normal="true" :no-style="true" emoji="💢" /> <MagEmoji :normal="true" :no-style="true" emoji="💢" />
<MkEmoji :normal="true" :no-style="true" emoji="😥" /> <MagEmoji :normal="true" :no-style="true" emoji="😥" />
<MkEmoji :normal="true" :no-style="true" emoji="😇" /> <MagEmoji :normal="true" :no-style="true" emoji="😇" />
<MkEmoji :normal="true" :no-style="true" emoji="🦊" /> <MagEmoji :normal="true" :no-style="true" emoji="🦊" />
<MkEmoji :normal="true" :no-style="true" emoji="🦋" /> <MagEmoji :normal="true" :no-style="true" emoji="🦋" />
</div> </div>
<div class="main"> <div class="main">
<img <img

View File

@ -1,5 +1,6 @@
import * as Misskey from "calckey-js"; import * as Misskey from "calckey-js";
import { packed, types } from "magnetar-common"; import { packed, types } from "magnetar-common";
import { UnicodeEmojiDef } from "@/scripts/emojilist";
// https://stackoverflow.com/a/50375286 Evil magic // https://stackoverflow.com/a/50375286 Evil magic
type Dist<U> = U extends any ? (k: U) => void : never; type Dist<U> = U extends any ? (k: U) => void : never;
@ -101,8 +102,16 @@ export function magReactionSelf(
([, , reacted]) => reacted === true ([, , reacted]) => reacted === true
)?.[0]; )?.[0];
return typeof found !== "undefined" ? magReactionToLegacy(found) : null; return typeof found !== "undefined" ? magReactionToLegacy(found) : null;
} else if ((note as Misskey.entities.Note).myReaction !== "undefined") { } else if (
return (note as Misskey.entities.Note).myReaction ?? null; typeof (note as Misskey.entities.Note).myReaction !== "undefined"
) {
return (note as Misskey.entities.Note).myReaction
? magReactionToLegacy(
magConvertReaction(
(note as Misskey.entities.Note).myReaction!
)
)
: null;
} }
return null; return null;
@ -164,22 +173,54 @@ export function magLegacyVisibility(
} }
} }
export function magCustomEmoji(
emoji: Misskey.entities.CustomEmoji
): types.ReactionShortcode {
return {
name: emoji.name,
host: null,
url: emoji.url,
};
}
export function magUnicodeEmoji(emoji: UnicodeEmojiDef): types.ReactionUnicode {
return emoji.emoji;
}
export function magIsCustomEmoji(
emoji: types.Reaction
): emoji is types.ReactionShortcode {
return (
typeof emoji === "object" &&
emoji !== null &&
typeof emoji["name"] !== "undefined"
);
}
export function magIsUnicodeEmoji(
emoji: types.Reaction
): emoji is types.ReactionUnicode {
return typeof emoji === "string";
}
export function magConvertReaction( export function magConvertReaction(
reaction: string, reaction: string,
urlHint?: string | null urlHint?: ((name: string, host: string | null) => string) | string | null
): types.Reaction { ): types.Reaction {
if (reaction.endsWith("@.:")) {
reaction = reaction.replaceAll(/@\.:$/, ":");
}
if (reaction.match(/^:.+:$/)) { if (reaction.match(/^:.+:$/)) {
reaction = reaction.replaceAll(/(^:) | (:$)/, ""); reaction = reaction.replaceAll(":", "");
const [name, maybeHost] = reaction.split("@");
const host = (maybeHost || ".") === "." ? null : maybeHost;
const [name, host] = reaction.split("@");
return { return {
name, name,
host: host || null, host,
url: urlHint!, url:
typeof urlHint === "function"
? urlHint(name, host || null)
: urlHint!,
}; };
} else { } else {
return reaction; return reaction;
@ -213,30 +254,31 @@ export function magReactionPairToLegacy(
return [legacy, reaction[1]]; return [legacy, reaction[1]];
} }
export function magReactionEquals(a: types.Reaction, b: types.Reaction) {
if (typeof a !== typeof b) return false;
if (magIsUnicodeEmoji(a)) {
return a === b;
} else if (magIsCustomEmoji(a)) {
const { name, host } = b as {
name: string;
host: string | null;
};
const { name: rName, host: rHost } = a;
return name === rName && (host ?? null) === (rHost ?? null);
} else if ("raw" in a && "raw" in (b as { raw: string })) {
return a.raw === (b as { raw: string }).raw;
}
return false;
}
export function magReactionIndex( export function magReactionIndex(
reactions: types.ReactionPair[], reactions: types.ReactionPair[],
reactionType: types.Reaction reactionType: types.Reaction
) { ) {
return reactions.findIndex(([r, ,]) => { return reactions.findIndex(([r, ,]) => {
if (typeof r !== typeof reactionType) return false; return magReactionEquals(r, reactionType);
if (typeof r === "string") {
return r === reactionType;
} else if (
"name" in r &&
"name" in (reactionType as { name: string; host: string | null })
) {
const { name, host } = reactionType as {
name: string;
host: string | null;
};
const { name: rName, host: rHost } = r;
return name === rName && (host ?? null) === (rHost ?? null);
} else if ("raw" in r && "raw" in (reactionType as { raw: string })) {
return r.raw === (reactionType as { raw: string }).raw;
}
return false;
}); });
} }

View File

@ -1,10 +1,11 @@
import { defineAsyncComponent, Ref, ref } from "vue"; import { defineAsyncComponent, Ref, ref } from "vue";
import { popup } from "@/os"; import { popup } from "@/os";
import { types } from "magnetar-common";
class ReactionPicker { class ReactionPicker {
private src: Ref<HTMLElement | null> = ref(null); private src: Ref<HTMLElement | null> = ref(null);
private manualShowing = ref(false); private manualShowing = ref(false);
private onChosen?: (reaction: string) => void; private onChosen?: (reaction: types.Reaction) => void;
private onClosed?: () => void; private onClosed?: () => void;
constructor() { constructor() {
@ -22,7 +23,7 @@ class ReactionPicker {
manualShowing: this.manualShowing, manualShowing: this.manualShowing,
}, },
{ {
done: (reaction) => { done: (reaction: types.Reaction) => {
this.onChosen!(reaction); this.onChosen!(reaction);
}, },
close: () => { close: () => {

View File

@ -1,4 +1,4 @@
import { onUnmounted, Ref } from "vue"; import { onUnmounted, Ref, toRaw } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import { stream } from "@/stream"; import { stream } from "@/stream";
import { $i } from "@/account"; import { $i } from "@/account";
@ -28,23 +28,6 @@ export function useNoteCapture(props: {
case "reacted": { case "reacted": {
const reaction = body.reaction as string; const reaction = body.reaction as string;
if (body.emoji) {
const emojis = note.value.emojis || [];
if (!emojis.includes(body.emoji)) {
note.value.emojis = [
...emojis,
{
id: body.emoji.id,
shortcode: body.emoji.name,
url: body.emoji.url,
width: body.emoji.width ?? null,
height: body.emoji.height ?? null,
category: null,
},
];
}
}
const reactionType = magConvertReaction( const reactionType = magConvertReaction(
reaction, reaction,
body?.emoji?.url body?.emoji?.url
@ -57,7 +40,7 @@ export function useNoteCapture(props: {
const selfReact = ($i && body.userId === $i.id) || false; const selfReact = ($i && body.userId === $i.id) || false;
if (foundReaction >= 0) { if (foundReaction >= 0) {
note.value.reactions[foundReaction] = [ note.value.reactions[foundReaction] = [
note.value.reactions[foundReaction][0], toRaw(note.value.reactions[foundReaction][0]),
note.value.reactions[foundReaction][1] + 1, note.value.reactions[foundReaction][1] + 1,
selfReact, selfReact,
]; ];
@ -76,12 +59,15 @@ export function useNoteCapture(props: {
reactionType reactionType
); );
const selfUnReact = ($i && body.userId === $i.id) || false;
if (foundReaction >= 0) { if (foundReaction >= 0) {
const cnt = note.value.reactions[foundReaction][1]; const [name, cnt, selfReact] =
note.value.reactions[foundReaction];
note.value.reactions[foundReaction] = [ note.value.reactions[foundReaction] = [
note.value.reactions[foundReaction][0], name,
cnt === 0 ? 0 : cnt - 1, Math.max(0, cnt - 1),
false, !selfUnReact && (selfReact ?? false),
]; ];
} }

View File

@ -15,6 +15,9 @@ export { PollBase } from "./types/PollBase";
export { PollChoice } from "./types/PollChoice"; export { PollChoice } from "./types/PollChoice";
export { Reaction } from "./types/Reaction"; export { Reaction } from "./types/Reaction";
export { ReactionPair } from "./types/ReactionPair"; export { ReactionPair } from "./types/ReactionPair";
export { ReactionShortcode } from "./types/ReactionShortcode";
export { ReactionUnicode } from "./types/ReactionUnicode";
export { ReactionUnknown } from "./types/ReactionUnknown";
export { AvatarDecoration } from "./types/AvatarDecoration"; export { AvatarDecoration } from "./types/AvatarDecoration";
export { ProfileField } from "./types/ProfileField"; export { ProfileField } from "./types/ProfileField";
export { SecurityKeyBase } from "./types/SecurityKeyBase"; export { SecurityKeyBase } from "./types/SecurityKeyBase";

View File

@ -1,3 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ReactionShortcode } from "./ReactionShortcode";
import type { ReactionUnicode } from "./ReactionUnicode";
import type { ReactionUnknown } from "./ReactionUnknown";
export type Reaction = string | { name: string, host: string | null, url: string, } | { raw: string, }; export type Reaction = ReactionUnicode | ReactionShortcode | ReactionUnknown;

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ReactionShortcode { name: string, host: string | null, url: string, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ReactionUnicode = string;

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ReactionUnknown { raw: string, }

View File

@ -164,6 +164,9 @@ importers:
'@vue/compiler-sfc': '@vue/compiler-sfc':
specifier: 3.3.4 specifier: 3.3.4
version: 3.3.4 version: 3.3.4
'@vue/runtime-core':
specifier: 3.3.4
version: 3.3.4
autobind-decorator: autobind-decorator:
specifier: 2.4.0 specifier: 2.4.0
version: 2.4.0 version: 2.4.0
@ -1007,7 +1010,7 @@ packages:
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@swc/core': ^1.2.66 '@swc/core': ^1.2.66
chokidar: ^3.5.1 chokidar: ^3.3.1
peerDependenciesMeta: peerDependenciesMeta:
chokidar: chokidar:
optional: true optional: true
@ -1440,7 +1443,7 @@ packages:
resolution: {integrity: sha512-Km7XAtUIduROw7QPgvcft0lIupeG8a8rdKL8RiSyKvlE7dYY31fEn41HVuQsRFDuROA8tA4K2UVL+WdfFmErBA==} resolution: {integrity: sha512-Km7XAtUIduROw7QPgvcft0lIupeG8a8rdKL8RiSyKvlE7dYY31fEn41HVuQsRFDuROA8tA4K2UVL+WdfFmErBA==}
requiresBuild: true requiresBuild: true
dependencies: dependencies:
'@types/node': 20.8.10 '@types/node': 14.18.63
dev: true dev: true
optional: true optional: true
@ -1982,8 +1985,8 @@ packages:
async-done: 1.3.2 async-done: 1.3.2
dev: true dev: true
/async@3.2.4: /async@3.2.5:
resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
dev: true dev: true
/asynckit@0.4.0: /asynckit@0.4.0:
@ -2432,8 +2435,8 @@ packages:
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
dev: true dev: true
/ci-info@3.8.0: /ci-info@3.9.0:
resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
@ -3793,7 +3796,7 @@ packages:
/getos@3.2.1: /getos@3.2.1:
resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==} resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==}
dependencies: dependencies:
async: 3.2.4 async: 3.2.5
dev: true dev: true
/getpass@0.1.7: /getpass@0.1.7:
@ -4298,7 +4301,7 @@ packages:
resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==}
hasBin: true hasBin: true
dependencies: dependencies:
ci-info: 3.8.0 ci-info: 3.9.0
dev: true dev: true
/is-core-module@2.12.1: /is-core-module@2.12.1:
@ -4541,8 +4544,8 @@ packages:
supports-color: 8.1.1 supports-color: 8.1.1
dev: true dev: true
/joi@17.9.2: /joi@17.11.0:
resolution: {integrity: sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==} resolution: {integrity: sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==}
dependencies: dependencies:
'@hapi/hoek': 9.3.0 '@hapi/hoek': 9.3.0
'@hapi/topo': 5.1.0 '@hapi/topo': 5.1.0
@ -7509,7 +7512,7 @@ packages:
hasBin: true hasBin: true
dependencies: dependencies:
axios: 0.25.0(debug@4.3.4) axios: 0.25.0(debug@4.3.4)
joi: 17.9.2 joi: 17.11.0
lodash: 4.17.21 lodash: 4.17.21
minimist: 1.2.8 minimist: 1.2.8
rxjs: 7.8.1 rxjs: 7.8.1

View File

@ -106,19 +106,32 @@ pack!(
Required<Id> as id & Required<NoteBase> as note & Optional<NoteSelfContextExt> as user_context & Optional<NoteAttachmentExt> as attachment & Optional<NoteDetailExt> as detail Required<Id> as id & Required<NoteBase> as note & Optional<NoteSelfContextExt> as user_context & Optional<NoteAttachmentExt> as attachment & Optional<NoteDetailExt> as detail
); );
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
#[repr(transparent)]
pub struct ReactionUnicode(pub String);
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct ReactionShortcode {
pub name: String,
pub host: Option<String>,
pub url: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct ReactionUnknown {
pub raw: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)] #[ts(export)]
#[serde(untagged)] #[serde(untagged)]
pub enum Reaction { pub enum Reaction {
Unicode(String), Unicode(ReactionUnicode),
Shortcode { Shortcode(ReactionShortcode),
name: String, Unknown(ReactionUnknown),
host: Option<String>,
url: String,
},
Unknown {
raw: String,
},
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]

View File

@ -25,6 +25,7 @@ use magnetar_sdk::types::emoji::EmojiContext;
use magnetar_sdk::types::note::{ use magnetar_sdk::types::note::{
NoteAttachmentExt, NoteBase, NoteDetailExt, NoteSelfContextExt, PackNoteBase, NoteAttachmentExt, NoteBase, NoteDetailExt, NoteSelfContextExt, PackNoteBase,
PackNoteMaybeAttachments, PackNoteMaybeFull, PackPollBase, PollBase, Reaction, ReactionPair, PackNoteMaybeAttachments, PackNoteMaybeFull, PackPollBase, PollBase, Reaction, ReactionPair,
ReactionShortcode, ReactionUnicode, ReactionUnknown,
}; };
use magnetar_sdk::types::{Id, MmXml}; use magnetar_sdk::types::{Id, MmXml};
use magnetar_sdk::{mmm, Optional, Packed, Required}; use magnetar_sdk::{mmm, Optional, Packed, Required};
@ -243,6 +244,7 @@ impl NoteModel {
.map(|(code, count, self_reacted)| { .map(|(code, count, self_reacted)| {
Ok((code, usize::deserialize(count)?, self_reacted)) Ok((code, usize::deserialize(count)?, self_reacted))
}) })
.filter(|v| !v.as_ref().is_ok_and(|(_, count, _)| *count == 0))
.collect::<Result<Vec<_>, serde_json::Error>>()?; .collect::<Result<Vec<_>, serde_json::Error>>()?;
// Pick out all successfully-parsed shortcode emojis // Pick out all successfully-parsed shortcode emojis
let reactions_to_resolve = reactions_raw let reactions_to_resolve = reactions_raw
@ -274,25 +276,29 @@ impl NoteModel {
.into_iter() .into_iter()
.map(|(raw, count, self_reaction)| { .map(|(raw, count, self_reaction)| {
let reaction = raw.either( let reaction = raw.either(
|raw| Reaction::Unknown { raw }, |raw| Reaction::Unknown(ReactionUnknown { raw }),
|raw| match raw { |raw| match raw {
RawReaction::Unicode(text) => Reaction::Unicode(text), RawReaction::Unicode(text) => Reaction::Unicode(ReactionUnicode(text)),
RawReaction::Shortcode { shortcode, host } => reactions_fetched RawReaction::Shortcode { shortcode, host } => reactions_fetched
.iter() .iter()
.find(|e| e.host == host && e.name == shortcode) .find(|e| e.host == host && e.name == shortcode)
.map_or_else( .map_or_else(
|| Reaction::Unknown { || {
Reaction::Unknown(ReactionUnknown {
raw: format!( raw: format!(
":{shortcode}{}:", ":{shortcode}{}:",
host.as_deref() host.as_deref()
.map(|h| format!("@{h}")) .map(|h| format!("@{h}"))
.unwrap_or_default() .unwrap_or_default()
), ),
})
}, },
|e| Reaction::Shortcode { |e| {
Reaction::Shortcode(ReactionShortcode {
name: shortcode.clone(), name: shortcode.clone(),
host: host.clone(), host: host.clone(),
url: e.public_url.clone(), url: e.public_url.clone(),
})
}, },
), ),
}, },