Compare commits

..

No commits in common. "02b37b7adf3d8302816c464dac058b7de53e974b" and "fe1b2b00f5fb162ae03d9f304f5e5a54a09d4ff2" have entirely different histories.

5 changed files with 56 additions and 67 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2025.5.1-beta.2", "version": "2025.5.1-beta.1",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -193,7 +193,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, ref, useTemplateRef, provide } from 'vue'; import { computed, inject, onMounted, ref, useTemplateRef, watch, provide, shallowRef, reactive } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js'; import { isLink } from '@@/js/is-link.js';
@ -283,10 +283,12 @@ if (noteViewInterruptors.length > 0) {
const isRenote = Misskey.note.isPureRenote(note); const isRenote = Misskey.note.isPureRenote(note);
const appearNote = getAppearNote(note); const appearNote = getAppearNote(note);
const { $note: $appearNote, subscribe: subscribeManuallyToNoteCapture } = useNoteCapture({ const $appearNote = reactive({
note: appearNote, reactions: appearNote.reactions,
parentNote: note, reactionCount: appearNote.reactionCount,
mock: props.mock, reactionEmojis: appearNote.reactionEmojis,
myReaction: appearNote.myReaction,
pollChoices: appearNote.poll?.choices,
}); });
const rootEl = useTemplateRef('rootEl'); const rootEl = useTemplateRef('rootEl');
@ -408,6 +410,17 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
}); });
}); });
let subscribeManuallyToNoteCapture: () => void = () => { };
if (!props.mock) {
const { subscribe } = useNoteCapture({
note: appearNote,
parentNote: note,
$note: $appearNote,
});
subscribeManuallyToNoteCapture = subscribe;
}
if (!props.mock) { if (!props.mock) {
useTooltip(renoteButton, async (showing) => { useTooltip(renoteButton, async (showing) => {
const renotes = await misskeyApi('notes/renotes', { const renotes = await misskeyApi('notes/renotes', {

View File

@ -228,7 +228,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, provide, ref, useTemplateRef } from 'vue'; import { computed, inject, onMounted, provide, reactive, ref, useTemplateRef } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js'; import { isLink } from '@@/js/is-link.js';
@ -304,9 +304,12 @@ if (noteViewInterruptors.length > 0) {
const isRenote = Misskey.note.isPureRenote(note); const isRenote = Misskey.note.isPureRenote(note);
const appearNote = getAppearNote(note); const appearNote = getAppearNote(note);
const { $note: $appearNote, subscribe: subscribeManuallyToNoteCapture } = useNoteCapture({ const $appearNote = reactive({
note: appearNote, reactions: appearNote.reactions,
parentNote: note, reactionCount: appearNote.reactionCount,
reactionEmojis: appearNote.reactionEmojis,
myReaction: appearNote.myReaction,
pollChoices: appearNote.poll?.choices,
}); });
const rootEl = useTemplateRef('rootEl'); const rootEl = useTemplateRef('rootEl');
@ -394,6 +397,12 @@ const reactionsPagination = computed(() => ({
}, },
})); }));
const { subscribe: subscribeManuallyToNoteCapture } = useNoteCapture({
note: appearNote,
parentNote: note,
$note: $appearNote,
});
useTooltip(renoteButton, async (showing) => { useTooltip(renoteButton, async (showing) => {
const renotes = await misskeyApi('notes/renotes', { const renotes = await misskeyApi('notes/renotes', {
noteId: appearNote.id, noteId: appearNote.id,

View File

@ -3,10 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { onUnmounted, reactive } from 'vue'; import { onUnmounted } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import type { Reactive } from 'vue'; import type { Reactive, Ref } from 'vue';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
@ -179,80 +179,60 @@ function realtimeSubscribe(props: {
}); });
} }
export type ReactiveNoteData = { type ReactiveNoteData = Reactive<{
reactions: Misskey.entities.Note['reactions']; reactions: Misskey.entities.Note['reactions'];
reactionCount: Misskey.entities.Note['reactionCount']; reactionCount: Misskey.entities.Note['reactionCount'];
reactionEmojis: Misskey.entities.Note['reactionEmojis']; reactionEmojis: Misskey.entities.Note['reactionEmojis'];
myReaction: Misskey.entities.Note['myReaction']; myReaction: Misskey.entities.Note['myReaction'];
pollChoices: NonNullable<Misskey.entities.Note['poll']>['choices']; pollChoices: NonNullable<Misskey.entities.Note['poll']>['choices'];
}; }>;
export function useNoteCapture(props: { export function useNoteCapture(props: {
note: Misskey.entities.Note; note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>;
parentNote: Misskey.entities.Note | null; parentNote: Misskey.entities.Note | null;
mock?: boolean; $note: ReactiveNoteData;
}): { }): {
$note: Reactive<ReactiveNoteData>;
subscribe: () => void; subscribe: () => void;
} { } {
const { note, parentNote, mock } = props; const { note, parentNote, $note } = props;
const $note = reactive<ReactiveNoteData>({
reactions: Object.entries(note.reactions).reduce((acc, [name, count]) => {
// Normalize reactions
const normalizedName = name.replace(/^:(\w+):$/, ':$1@.:');
if (acc[normalizedName] == null) {
acc[normalizedName] = count;
} else {
acc[normalizedName] += count;
}
return acc;
}, {} as Misskey.entities.Note['reactions']),
reactionCount: note.reactionCount,
reactionEmojis: note.reactionEmojis,
myReaction: note.myReaction,
pollChoices: note.poll?.choices ?? [],
});
noteEvents.on(`reacted:${note.id}`, onReacted); noteEvents.on(`reacted:${note.id}`, onReacted);
noteEvents.on(`unreacted:${note.id}`, onUnreacted); noteEvents.on(`unreacted:${note.id}`, onUnreacted);
noteEvents.on(`pollVoted:${note.id}`, onPollVoted); noteEvents.on(`pollVoted:${note.id}`, onPollVoted);
// 操作がダブっていないかどうかを簡易的に記録するためのMap let latestReactedKey: string | null = null;
const reactionUserMap = new Map<Misskey.entities.User['id'], string>(); let latestUnreactedKey: string | null = null;
let latestPollVotedKey: string | null = null; let latestPollVotedKey: string | null = null;
function onReacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void { function onReacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void {
const normalizedName = ctx.reaction.replace(/^:(\w+):$/, ':$1@.:'); const newReactedKey = `${ctx.userId}:${ctx.reaction}`;
if (newReactedKey === latestReactedKey) return;
if (reactionUserMap.has(ctx.userId) && reactionUserMap.get(ctx.userId) === normalizedName) return; latestReactedKey = newReactedKey;
reactionUserMap.set(ctx.userId, normalizedName);
if (ctx.emoji && !(ctx.emoji.name in $note.reactionEmojis)) { if (ctx.emoji && !(ctx.emoji.name in $note.reactionEmojis)) {
$note.reactionEmojis[ctx.emoji.name] = ctx.emoji.url; $note.reactionEmojis[ctx.emoji.name] = ctx.emoji.url;
} }
const currentCount = $note.reactions[normalizedName] || 0; const currentCount = $note.reactions[ctx.reaction] || 0;
$note.reactions[normalizedName] = currentCount + 1; $note.reactions[ctx.reaction] = currentCount + 1;
$note.reactionCount += 1; $note.reactionCount += 1;
if ($i && (ctx.userId === $i.id)) { if ($i && (ctx.userId === $i.id)) {
$note.myReaction = normalizedName; $note.myReaction = ctx.reaction;
} }
} }
function onUnreacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void { function onUnreacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void {
const normalizedName = ctx.reaction.replace(/^:(\w+):$/, ':$1@.:'); const newUnreactedKey = `${ctx.userId}:${ctx.reaction}`;
if (newUnreactedKey === latestUnreactedKey) return;
latestUnreactedKey = newUnreactedKey;
if (!reactionUserMap.has(ctx.userId)) return; const currentCount = $note.reactions[ctx.reaction] || 0;
reactionUserMap.delete(ctx.userId);
const currentCount = $note.reactions[normalizedName] || 0; $note.reactions[ctx.reaction] = Math.max(0, currentCount - 1);
$note.reactions[normalizedName] = Math.max(0, currentCount - 1);
$note.reactionCount = Math.max(0, $note.reactionCount - 1); $note.reactionCount = Math.max(0, $note.reactionCount - 1);
if ($note.reactions[normalizedName] === 0) delete $note.reactions[normalizedName]; if ($note.reactions[ctx.reaction] === 0) delete $note.reactions[ctx.reaction];
if ($i && (ctx.userId === $i.id)) { if ($i && (ctx.userId === $i.id)) {
$note.myReaction = null; $note.myReaction = null;
@ -277,20 +257,10 @@ export function useNoteCapture(props: {
} }
function subscribe() { function subscribe() {
if (mock) {
// モックモードでは購読しない
return;
}
if ($i && store.s.realtimeMode) { if ($i && store.s.realtimeMode) {
realtimeSubscribe({ realtimeSubscribe(props);
note,
});
} else { } else {
pollingSubscribe({ pollingSubscribe(props);
note,
$note,
});
} }
} }
@ -307,7 +277,6 @@ export function useNoteCapture(props: {
if ((Date.now() - new Date(note.createdAt).getTime()) > 1000 * 60 * 5) { // 5min if ((Date.now() - new Date(note.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
// リノートで表示されているノートでもないし、投稿からある程度経過しているので自動で購読しない // リノートで表示されているノートでもないし、投稿からある程度経過しているので自動で購読しない
return { return {
$note,
subscribe: () => { subscribe: () => {
subscribe(); subscribe();
}, },
@ -317,7 +286,6 @@ export function useNoteCapture(props: {
if ((Date.now() - new Date(parentNote.createdAt).getTime()) > 1000 * 60 * 5) { // 5min if ((Date.now() - new Date(parentNote.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
// リノートで表示されているノートだが、リノートされてからある程度経過しているので自動で購読しない // リノートで表示されているノートだが、リノートされてからある程度経過しているので自動で購読しない
return { return {
$note,
subscribe: () => { subscribe: () => {
subscribe(); subscribe();
}, },
@ -328,7 +296,6 @@ export function useNoteCapture(props: {
subscribe(); subscribe();
return { return {
$note,
subscribe: () => { subscribe: () => {
// すでに購読しているので何もしない // すでに購読しているので何もしない
}, },

View File

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "misskey-js", "name": "misskey-js",
"version": "2025.5.1-beta.2", "version": "2025.5.1-beta.1",
"description": "Misskey SDK for JavaScript", "description": "Misskey SDK for JavaScript",
"license": "MIT", "license": "MIT",
"main": "./built/index.js", "main": "./built/index.js",