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",
"@vitejs/plugin-vue": "4.2.3",
"@vue/compiler-sfc": "3.3.4",
"@vue/runtime-core": "3.3.4",
"autobind-decorator": "2.4.0",
"autosize": "5.0.2",
"blurhash": "1.1.5",

View File

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

View File

@ -170,7 +170,11 @@ import { useNoteCapture } from "@/scripts/use-note-capture";
import { stream } from "@/stream";
import { NoteUpdatedEvent } from "calckey-js/built/streaming.types";
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";
const props = defineProps<{
@ -254,7 +258,7 @@ function react(viaKeyboard = false): void {
(reaction) => {
os.api("notes/reactions/create", {
noteId: note.id,
reaction: reaction,
reaction: magReactionToLegacy(reaction),
});
},
() => {

View File

@ -34,16 +34,19 @@
class="_button item"
@click="emit('chosen', emoji, $event)"
>
<MkEmoji class="emoji" :emoji="emoji" :normal="true" />
<MagEmoji class="emoji" :emoji="emoji" :normal="true" />
</button>
</div>
</section>
</template>
<script lang="ts" setup>
import { ref, watch, onMounted } from "vue";
import { ref, watch, onMounted, toRaw } from "vue";
import { addSkinTone } from "@/scripts/emojilist";
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<{
emojis: string[];
@ -52,10 +55,19 @@ const props = defineProps<{
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) {
for (let i = 0; i < localEmojis.value.length; i++) {
let rawEmoji: types.ReactionUnicode = toRaw(localEmojis[i]);
if (!magIsUnicodeEmoji(rawEmoji)) continue;
if (
[
emojiComponents.light_skin_tone,
@ -63,16 +75,16 @@ function applyUnicodeSkinTone(custom?: number) {
emojiComponents.medium_skin_tone,
emojiComponents.medium_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<{
(ev: "chosen", v: string, event: MouseEvent): void;
(ev: "chosen", v: types.Reaction, event: MouseEvent): void;
}>();
const shown = ref(!!props.initialShown);
@ -86,7 +98,7 @@ onMounted(() => {
watch(
() => props.emojis,
(newVal) => {
localEmojis.value = [...newVal];
localEmojis.value = newVal.map(resolveEmojis);
}
);
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,11 @@
<template>
<div class="tdflqwzn" :class="{ isMe }">
<XReaction
v-for="r in Array.isArray(note.reactions)
? note.reactions
: Object.entries(note.reactions)"
:key="magReactionPairToLegacy(r)[0]"
:reaction="magReactionPairToLegacy(r)[0]"
:url="typeof r[0]['url'] !== 'undefined' ? r[0]['url'] : undefined"
:count="magReactionPairToLegacy(r)[1]"
:is-initial="initialReactions.has(magReactionPairToLegacy(r)[0])"
v-for="r in reactions"
:key="magReactionToLegacy(r[0])"
:reaction="r[0]"
:count="r[1]"
:is-initial="initialReactions.has(magReactionToLegacy(r[0]))"
:note="note"
/>
</div>
@ -19,14 +16,58 @@ import { computed } from "vue";
import * as misskey from "calckey-js";
import { $i } from "@/account";
import XReaction from "@/components/MkReactionsViewer.reaction.vue";
import { packed } from "magnetar-common";
import { magReactionPairToLegacy } from "@/scripts-mag/mag-util";
import { packed, types } from "magnetar-common";
import {
magConvertReaction,
magReactionEquals,
magReactionToLegacy,
} from "@/scripts-mag/mag-util";
const props = defineProps<{
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);
</script>

View File

@ -1,6 +1,6 @@
<template>
<img
v-if="customEmoji"
v-if="isCustom"
class="mk-emoji custom"
:class="{ normal, noStyle }"
:src="url"
@ -22,56 +22,43 @@
<script lang="ts" setup>
import { computed } from "vue";
import * as Misskey from "calckey-js";
import { getStaticImageUrl } from "@/scripts/get-static-image-url";
import { char2filePath } from "@/scripts/twemoji-base";
import { defaultStore } from "@/store";
import { instance } from "@/instance";
import { packed } from "magnetar-common";
import { magTransProperty } from "@/scripts-mag/mag-util";
import { types } from "magnetar-common";
import {
magIsCustomEmoji,
magIsUnicodeEmoji,
magReactionToLegacy,
} from "@/scripts-mag/mag-util";
const props = defineProps<{
emoji: string;
emoji: types.Reaction;
normal?: boolean;
noStyle?: boolean;
customEmojis?: (
| packed.PackEmojiBase
| Pick<Misskey.entities.CustomEmoji, "name" | "url">
)[];
isReaction?: boolean;
}>();
const isCustom = computed(() => props.emoji.startsWith(":"));
const char = computed(() => (isCustom.value ? null : props.emoji));
const isCustom = computed(() => magIsCustomEmoji(props.emoji));
const char = computed(() =>
magIsUnicodeEmoji(props.emoji) ? props.emoji : null
);
const useOsNativeEmojis = computed(
() => 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(() => {
if (char.value) {
return char2filePath(char.value);
} else if (customEmoji?.value?.url) {
} else if (magIsCustomEmoji(props.emoji)) {
return defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(customEmoji.value.url)
: customEmoji.value.url;
? getStaticImageUrl(props.emoji.url)
: props.emoji.url;
} else {
return null;
}
});
const alt = computed(() =>
customEmoji.value
? `:${magTransProperty(customEmoji.value, "shortcode", "name")}:`
: char.value
);
const alt = computed(() => magReactionToLegacy(props.emoji));
</script>
<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 MkLink from "@/components/MkLink.vue";
import MkMention from "@/components/MkMention.vue";
import MkEmoji from "@/components/global/MkEmoji.vue";
import { concat } from "@/scripts/array";
import MkFormula from "@/components/MkFormula.vue";
import MkCode from "@/components/MkCode.vue";
@ -13,6 +12,12 @@ import MkA from "@/components/global/MkA.vue";
import { host } from "@/config";
import { reducedMotion } from "@/scripts/reduced-motion";
import { defaultStore } from "@/store";
import MagEmoji from "@/components/global/MagEmoji.vue";
import {
magConvertReaction,
magReactionEquals,
magReactionToLegacy,
} from "@/scripts-mag/mag-util";
export default defineComponent({
props: {
@ -537,10 +542,20 @@ export default defineComponent({
case "emojiCode": {
return [
h(MkEmoji, {
h(MagEmoji, {
key: Math.random(),
emoji: `:${token.props.name}:`,
customEmojis: this.customEmojis,
emoji: magConvertReaction(
`:${token.props.name}:`,
(name, host) =>
this.customEmojis.find((e) =>
magReactionEquals(
magConvertReaction(
`:${e.name}:`
),
{ name, host, url: null! }
)
)?.url ?? null
),
normal: this.plain,
}),
];
@ -548,10 +563,9 @@ export default defineComponent({
case "unicodeEmoji": {
return [
h(MkEmoji, {
h(MagEmoji, {
key: Math.random(),
emoji: token.props.emoji,
customEmojis: this.customEmojis,
normal: this.plain,
}),
];

View File

@ -23,7 +23,6 @@ import { compareVersions } from "compare-versions";
import widgets from "@/widgets";
import directives from "@/directives";
import components from "@/components";
import { lang, ui, version } from "@/config";
import { applyTheme } from "@/scripts/theme";
import { isDeviceDarkmode } from "@/scripts/is-device-darkmode";
@ -43,6 +42,68 @@ import { reactionPicker } from "@/scripts/reaction-picker";
import { getUrlWithoutLoginId } from "@/scripts/login-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");
if (accounts) {
set("accounts", JSON.parse(accounts));
@ -208,7 +269,7 @@ function checkForSplash() {
widgets(app);
directives(app);
components(app);
globalComponents(app);
checkForSplash();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import * as Misskey from "calckey-js";
import { packed, types } from "magnetar-common";
import { UnicodeEmojiDef } from "@/scripts/emojilist";
// https://stackoverflow.com/a/50375286 Evil magic
type Dist<U> = U extends any ? (k: U) => void : never;
@ -101,8 +102,16 @@ export function magReactionSelf(
([, , reacted]) => reacted === true
)?.[0];
return typeof found !== "undefined" ? magReactionToLegacy(found) : null;
} else if ((note as Misskey.entities.Note).myReaction !== "undefined") {
return (note as Misskey.entities.Note).myReaction ?? null;
} else if (
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;
@ -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(
reaction: string,
urlHint?: string | null
urlHint?: ((name: string, host: string | null) => string) | string | null
): types.Reaction {
if (reaction.endsWith("@.:")) {
reaction = reaction.replaceAll(/@\.:$/, ":");
}
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 {
name,
host: host || null,
url: urlHint!,
host,
url:
typeof urlHint === "function"
? urlHint(name, host || null)
: urlHint!,
};
} else {
return reaction;
@ -213,30 +254,31 @@ export function magReactionPairToLegacy(
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(
reactions: types.ReactionPair[],
reactionType: types.Reaction
) {
return reactions.findIndex(([r, ,]) => {
if (typeof r !== typeof reactionType) return false;
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;
return magReactionEquals(r, reactionType);
});
}

View File

@ -1,10 +1,11 @@
import { defineAsyncComponent, Ref, ref } from "vue";
import { popup } from "@/os";
import { types } from "magnetar-common";
class ReactionPicker {
private src: Ref<HTMLElement | null> = ref(null);
private manualShowing = ref(false);
private onChosen?: (reaction: string) => void;
private onChosen?: (reaction: types.Reaction) => void;
private onClosed?: () => void;
constructor() {
@ -22,7 +23,7 @@ class ReactionPicker {
manualShowing: this.manualShowing,
},
{
done: (reaction) => {
done: (reaction: types.Reaction) => {
this.onChosen!(reaction);
},
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 { stream } from "@/stream";
import { $i } from "@/account";
@ -28,23 +28,6 @@ export function useNoteCapture(props: {
case "reacted": {
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(
reaction,
body?.emoji?.url
@ -57,7 +40,7 @@ export function useNoteCapture(props: {
const selfReact = ($i && body.userId === $i.id) || false;
if (foundReaction >= 0) {
note.value.reactions[foundReaction] = [
note.value.reactions[foundReaction][0],
toRaw(note.value.reactions[foundReaction][0]),
note.value.reactions[foundReaction][1] + 1,
selfReact,
];
@ -76,12 +59,15 @@ export function useNoteCapture(props: {
reactionType
);
const selfUnReact = ($i && body.userId === $i.id) || false;
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][0],
cnt === 0 ? 0 : cnt - 1,
false,
name,
Math.max(0, cnt - 1),
!selfUnReact && (selfReact ?? false),
];
}

View File

@ -15,6 +15,9 @@ export { PollBase } from "./types/PollBase";
export { PollChoice } from "./types/PollChoice";
export { Reaction } from "./types/Reaction";
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 { ProfileField } from "./types/ProfileField";
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.
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':
specifier: 3.3.4
version: 3.3.4
'@vue/runtime-core':
specifier: 3.3.4
version: 3.3.4
autobind-decorator:
specifier: 2.4.0
version: 2.4.0
@ -1007,7 +1010,7 @@ packages:
hasBin: true
peerDependencies:
'@swc/core': ^1.2.66
chokidar: ^3.5.1
chokidar: ^3.3.1
peerDependenciesMeta:
chokidar:
optional: true
@ -1440,7 +1443,7 @@ packages:
resolution: {integrity: sha512-Km7XAtUIduROw7QPgvcft0lIupeG8a8rdKL8RiSyKvlE7dYY31fEn41HVuQsRFDuROA8tA4K2UVL+WdfFmErBA==}
requiresBuild: true
dependencies:
'@types/node': 20.8.10
'@types/node': 14.18.63
dev: true
optional: true
@ -1982,8 +1985,8 @@ packages:
async-done: 1.3.2
dev: true
/async@3.2.4:
resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
/async@3.2.5:
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
dev: true
/asynckit@0.4.0:
@ -2432,8 +2435,8 @@ packages:
engines: {node: '>=6.0'}
dev: true
/ci-info@3.8.0:
resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==}
/ci-info@3.9.0:
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
engines: {node: '>=8'}
dev: true
@ -3793,7 +3796,7 @@ packages:
/getos@3.2.1:
resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==}
dependencies:
async: 3.2.4
async: 3.2.5
dev: true
/getpass@0.1.7:
@ -4298,7 +4301,7 @@ packages:
resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==}
hasBin: true
dependencies:
ci-info: 3.8.0
ci-info: 3.9.0
dev: true
/is-core-module@2.12.1:
@ -4541,8 +4544,8 @@ packages:
supports-color: 8.1.1
dev: true
/joi@17.9.2:
resolution: {integrity: sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==}
/joi@17.11.0:
resolution: {integrity: sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==}
dependencies:
'@hapi/hoek': 9.3.0
'@hapi/topo': 5.1.0
@ -7509,7 +7512,7 @@ packages:
hasBin: true
dependencies:
axios: 0.25.0(debug@4.3.4)
joi: 17.9.2
joi: 17.11.0
lodash: 4.17.21
minimist: 1.2.8
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
);
#[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)]
#[ts(export)]
#[serde(untagged)]
pub enum Reaction {
Unicode(String),
Shortcode {
name: String,
host: Option<String>,
url: String,
},
Unknown {
raw: String,
},
Unicode(ReactionUnicode),
Shortcode(ReactionShortcode),
Unknown(ReactionUnknown),
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]

View File

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