diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index b6982fffab..936c17d55b 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -87,7 +87,16 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -101,7 +110,16 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ appearNote.channel.name }}
-
+
{{ i18n.ts.more }}
@@ -125,11 +143,11 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -29,7 +29,6 @@ import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
import { useTooltip } from '@/use/use-tooltip.js';
import { $i } from '@/i.js';
import MkReactionEffect from '@/components/MkReactionEffect.vue';
-import { claimAchievement } from '@/utility/achievements.js';
import { i18n } from '@/i18n.js';
import * as sound from '@/utility/sound.js';
import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
@@ -39,10 +38,12 @@ import { DI } from '@/di.js';
import { noteEvents } from '@/use/use-note-capture.js';
const props = defineProps<{
+ noteId: Misskey.entities.Note['id'];
reaction: string;
+ reactionEmojis: Misskey.entities.Note['reactionEmojis'];
+ myReaction: Misskey.entities.Note['myReaction'];
count: number;
isInitial: boolean;
- note: Misskey.entities.Note;
}>();
const mock = inject(DI.mock, false);
@@ -57,14 +58,16 @@ const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./,
const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction));
const canToggle = computed(() => {
- return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value);
+ // TODO
+ //return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value);
+ return !props.reaction.match(/@\w/) && $i && emoji.value;
});
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
async function toggleReaction() {
if (!canToggle.value) return;
- const oldReaction = props.note.myReaction;
+ const oldReaction = props.myReaction;
if (oldReaction) {
const confirm = await os.confirm({
type: 'warning',
@@ -82,19 +85,19 @@ async function toggleReaction() {
}
misskeyApi('notes/reactions/delete', {
- noteId: props.note.id,
+ noteId: props.noteId,
}).then(() => {
- noteEvents.emit(`unreacted:${props.note.id}`, {
+ noteEvents.emit(`unreacted:${props.noteId}`, {
userId: $i!.id,
reaction: props.reaction,
emoji: emoji.value,
});
if (oldReaction !== props.reaction) {
misskeyApi('notes/reactions/create', {
- noteId: props.note.id,
+ noteId: props.noteId,
reaction: props.reaction,
}).then(() => {
- noteEvents.emit(`reacted:${props.note.id}`, {
+ noteEvents.emit(`reacted:${props.noteId}`, {
userId: $i!.id,
reaction: props.reaction,
emoji: emoji.value,
@@ -120,18 +123,19 @@ async function toggleReaction() {
}
misskeyApi('notes/reactions/create', {
- noteId: props.note.id,
+ noteId: props.noteId,
reaction: props.reaction,
}).then(() => {
- noteEvents.emit(`reacted:${props.note.id}`, {
+ noteEvents.emit(`reacted:${props.noteId}`, {
userId: $i!.id,
reaction: props.reaction,
emoji: emoji.value,
});
});
- if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) {
- claimAchievement('reactWithoutRead');
- }
+ // TODO: 上位コンポーネントでやる
+ //if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) {
+ // claimAchievement('reactWithoutRead');
+ //}
}
}
@@ -175,7 +179,7 @@ onMounted(() => {
if (!mock) {
useTooltip(buttonEl, async (showing) => {
const reactions = await misskeyApiGet('notes/reactions', {
- noteId: props.note.id,
+ noteId: props.noteId,
type: props.reaction,
limit: 10,
_cacheKey_: props.count,
diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue
index e8cf6c36db..725978179e 100644
--- a/packages/frontend/src/components/MkReactionsViewer.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.vue
@@ -13,7 +13,17 @@ SPDX-License-Identifier: AGPL-3.0-only
:moveClass="$style.transition_x_move"
tag="div" :class="$style.root"
>
-
+
@@ -27,7 +37,10 @@ import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
const props = withDefaults(defineProps<{
- note: Misskey.entities.Note;
+ noteId: Misskey.entities.Note['id'];
+ reactions: Misskey.entities.Note['reactions'];
+ reactionEmojis: Misskey.entities.Note['reactionEmojis'];
+ myReaction: Misskey.entities.Note['myReaction'];
maxNumber?: number;
}>(), {
maxNumber: Infinity,
@@ -39,33 +52,33 @@ const emit = defineEmits<{
(ev: 'mockUpdateMyReaction', emoji: string, delta: number): void;
}>();
-const initialReactions = new Set(Object.keys(props.note.reactions));
+const initialReactions = new Set(Object.keys(props.reactions));
-const reactions = ref<[string, number][]>([]);
+const _reactions = ref<[string, number][]>([]);
const hasMoreReactions = ref(false);
-if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) {
- reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction];
+if (props.myReaction && !Object.keys(_reactions.value).includes(props.myReaction)) {
+ _reactions.value[props.myReaction] = props.reactions[props.myReaction];
}
function onMockToggleReaction(emoji: string, count: number) {
if (!mock) return;
- const i = reactions.value.findIndex((item) => item[0] === emoji);
+ const i = _reactions.value.findIndex((item) => item[0] === emoji);
if (i < 0) return;
- emit('mockUpdateMyReaction', emoji, (count - reactions.value[i][1]));
+ emit('mockUpdateMyReaction', emoji, (count - _reactions.value[i][1]));
}
-watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
+watch([() => props.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
let newReactions: [string, number][] = [];
hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
- for (let i = 0; i < reactions.value.length; i++) {
- const reaction = reactions.value[i][0];
+ for (let i = 0; i < _reactions.value.length; i++) {
+ const reaction = _reactions.value[i][0];
if (reaction in newSource && newSource[reaction] !== 0) {
- reactions.value[i][1] = newSource[reaction];
- newReactions.push(reactions.value[i]);
+ _reactions.value[i][1] = newSource[reaction];
+ newReactions.push(_reactions.value[i]);
}
}
@@ -79,11 +92,11 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
newReactions = newReactions.slice(0, props.maxNumber);
- if (props.note.myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) {
- newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
+ if (props.myReaction && !newReactions.map(([x]) => x).includes(props.myReaction)) {
+ newReactions.push([props.myReaction, newSource[props.myReaction]]);
}
- reactions.value = newReactions;
+ _reactions.value = newReactions;
}, { immediate: true, deep: true });
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 5800beb3a0..9b28573712 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -321,6 +321,7 @@ refreshEndpointAndChannel();
const paginator = usePagination({
ctx: paginationQuery,
+ useShallowRef: true,
});
onUnmounted(() => {
diff --git a/packages/frontend/src/pages/welcome.timeline.note.vue b/packages/frontend/src/pages/welcome.timeline.note.vue
index 680fe08c14..62a220d2f1 100644
--- a/packages/frontend/src/pages/welcome.timeline.note.vue
+++ b/packages/frontend/src/pages/welcome.timeline.note.vue
@@ -27,7 +27,8 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
+
diff --git a/packages/frontend/src/use/use-note-capture.ts b/packages/frontend/src/use/use-note-capture.ts
index c0d7bbfc65..4d5cb6be0f 100644
--- a/packages/frontend/src/use/use-note-capture.ts
+++ b/packages/frontend/src/use/use-note-capture.ts
@@ -6,7 +6,7 @@
import { onUnmounted } from 'vue';
import * as Misskey from 'misskey-js';
import { EventEmitter } from 'eventemitter3';
-import type { Ref, ShallowRef } from 'vue';
+import type { Ref } from 'vue';
import { useStream } from '@/stream.js';
import { $i } from '@/i.js';
import { store } from '@/store.js';
@@ -28,7 +28,7 @@ const pollingQueue = new Map();
-function pollingEnqueue(note: Misskey.entities.Note) {
+function pollingEnqueue(note: Pick) {
if (pollingQueue.has(note.id)) {
const data = pollingQueue.get(note.id)!;
pollingQueue.set(note.id, {
@@ -44,7 +44,7 @@ function pollingEnqueue(note: Misskey.entities.Note) {
}
}
-function pollingDequeue(note: Misskey.entities.Note) {
+function pollingDequeue(note: Pick) {
const data = pollingQueue.get(note.id);
if (data == null) return;
@@ -85,28 +85,31 @@ window.setInterval(() => {
}, POLLING_INTERVAL);
function pollingSubscribe(props: {
- note: Ref;
+ note: Pick;
+ reactionsRef: Ref;
+ reactionCountRef: Ref;
+ reactionEmojisRef: Ref;
isDeletedRef: Ref;
}) {
- const note = props.note;
+ const { note, reactionsRef, reactionCountRef, reactionEmojisRef } = props;
function onFetched(data: Pick): void {
- note.value.reactions = data.reactions;
- note.value.reactionCount = Object.values(data.reactions).reduce((a, b) => a + b, 0);
- note.value.reactionEmojis = data.reactionEmojis;
+ reactionsRef.value = data.reactions;
+ reactionCountRef.value = Object.values(data.reactions).reduce((a, b) => a + b, 0);
+ reactionEmojisRef.value = data.reactionEmojis;
}
- pollingEnqueue(note.value);
- fetchEvent.on(note.value.id, onFetched);
+ pollingEnqueue(note);
+ fetchEvent.on(note.id, onFetched);
onUnmounted(() => {
- pollingDequeue(note.value);
- fetchEvent.off(note.value.id, onFetched);
+ pollingDequeue(note);
+ fetchEvent.off(note.id, onFetched);
});
}
function realtimeSubscribe(props: {
- note: Ref;
+ note: Pick;
isDeletedRef: Ref;
}): void {
const note = props.note;
@@ -115,7 +118,7 @@ function realtimeSubscribe(props: {
function onStreamNoteUpdated(noteData): void {
const { type, id, body } = noteData;
- if (id !== note.value.id) return;
+ if (id !== note.id) return;
switch (type) {
case 'reacted': {
@@ -152,12 +155,12 @@ function realtimeSubscribe(props: {
}
function capture(withHandler = false): void {
- connection.send('sr', { id: note.value.id });
+ connection.send('sr', { id: note.id });
if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
}
function decapture(withHandler = false): void {
- connection.send('un', { id: note.value.id });
+ connection.send('un', { id: note.id });
if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated);
}
@@ -175,39 +178,42 @@ function realtimeSubscribe(props: {
}
export function useNoteCapture(props: {
- note: Ref;
- parentNote: Ref | null;
+ note: Pick;
+ parentNote: Misskey.entities.Note | null;
+ reactionsRef: Ref;
+ reactionCountRef: Ref;
+ reactionEmojisRef: Ref;
+ myReactionRef: Ref;
+ pollChoicesRef: Ref['choices'] | null>;
isDeletedRef: Ref;
}) {
- const note = props.note;
- const parentNote = props.parentNote;
+ const { note, parentNote, reactionsRef, reactionCountRef, reactionEmojisRef, myReactionRef, pollChoicesRef } = props;
- noteEvents.on(`reacted:${note.value.id}`, onReacted);
- noteEvents.on(`unreacted:${note.value.id}`, onUnreacted);
- noteEvents.on(`pollVoted:${note.value.id}`, onPollVoted);
- noteEvents.on(`deleted:${note.value.id}`, onDeleted);
+ noteEvents.on(`reacted:${note.id}`, onReacted);
+ noteEvents.on(`unreacted:${note.id}`, onUnreacted);
+ noteEvents.on(`pollVoted:${note.id}`, onPollVoted);
+ noteEvents.on(`deleted:${note.id}`, onDeleted);
let latestReactedKey: string | null = null;
let latestUnreactedKey: string | null = null;
let latestPollVotedKey: string | null = null;
function onReacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void {
- console.log('reacted', ctx);
const newReactedKey = `${ctx.userId}:${ctx.reaction}`;
if (newReactedKey === latestReactedKey) return;
latestReactedKey = newReactedKey;
- if (ctx.emoji && !(ctx.emoji.name in note.value.reactionEmojis)) {
- note.value.reactionEmojis[ctx.emoji.name] = ctx.emoji.url;
+ if (ctx.emoji && !(ctx.emoji.name in reactionEmojisRef.value)) {
+ reactionEmojisRef.value[ctx.emoji.name] = ctx.emoji.url;
}
- const currentCount = note.value.reactions[ctx.reaction] || 0;
+ const currentCount = reactionsRef.value[ctx.reaction] || 0;
- note.value.reactions[ctx.reaction] = currentCount + 1;
- note.value.reactionCount += 1;
+ reactionsRef.value[ctx.reaction] = currentCount + 1;
+ reactionCountRef.value += 1;
if ($i && (ctx.userId === $i.id)) {
- note.value.myReaction = ctx.reaction;
+ myReactionRef.value = ctx.reaction;
}
}
@@ -216,14 +222,14 @@ export function useNoteCapture(props: {
if (newUnreactedKey === latestUnreactedKey) return;
latestUnreactedKey = newUnreactedKey;
- const currentCount = note.value.reactions[ctx.reaction] || 0;
+ const currentCount = reactionsRef.value[ctx.reaction] || 0;
- note.value.reactions[ctx.reaction] = Math.max(0, currentCount - 1);
- note.value.reactionCount = Math.max(0, note.value.reactionCount - 1);
- if (note.value.reactions[ctx.reaction] === 0) delete note.value.reactions[ctx.reaction];
+ reactionsRef.value[ctx.reaction] = Math.max(0, currentCount - 1);
+ reactionCountRef.value = Math.max(0, reactionCountRef.value - 1);
+ if (reactionsRef.value[ctx.reaction] === 0) delete reactionsRef.value[ctx.reaction];
if ($i && (ctx.userId === $i.id)) {
- note.value.myReaction = null;
+ myReactionRef.value = null;
}
}
@@ -232,7 +238,7 @@ export function useNoteCapture(props: {
if (newPollVotedKey === latestPollVotedKey) return;
latestPollVotedKey = newPollVotedKey;
- const choices = [...note.value.poll.choices];
+ const choices = [...pollChoicesRef.value];
choices[ctx.choice] = {
...choices[ctx.choice],
votes: choices[ctx.choice].votes + 1,
@@ -241,7 +247,7 @@ export function useNoteCapture(props: {
} : {}),
};
- note.value.poll.choices = choices;
+ pollChoicesRef.value = choices;
}
function onDeleted(): void {
@@ -249,22 +255,22 @@ export function useNoteCapture(props: {
}
onUnmounted(() => {
- noteEvents.off(`reacted:${note.value.id}`, onReacted);
- noteEvents.off(`unreacted:${note.value.id}`, onUnreacted);
- noteEvents.off(`pollVoted:${note.value.id}`, onPollVoted);
- noteEvents.off(`deleted:${note.value.id}`, onDeleted);
+ noteEvents.off(`reacted:${note.id}`, onReacted);
+ noteEvents.off(`unreacted:${note.id}`, onUnreacted);
+ noteEvents.off(`pollVoted:${note.id}`, onPollVoted);
+ noteEvents.off(`deleted:${note.id}`, onDeleted);
});
// 投稿からある程度経過している(=タイムラインを遡って表示した)ノートは、イベントが発生する可能性が低いためそもそも購読しない
// ただし「リノートされたばかりの過去のノート」(= parentNoteが存在し、かつparentNoteの投稿日時が最近)はイベント発生が考えられるため購読する
// TODO: デバイスとサーバーの時計がズレていると不具合の元になるため、ズレを検知して警告を表示するなどのケアが必要かもしれない
if (parentNote == null) {
- if ((Date.now() - new Date(note.value.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
+ if ((Date.now() - new Date(note.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
// リノートで表示されているノートでもないし、投稿からある程度経過しているので購読しない
return;
}
} else {
- if ((Date.now() - new Date(parentNote.value.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
+ if ((Date.now() - new Date(parentNote.createdAt).getTime()) > 1000 * 60 * 5) { // 5min
// リノートで表示されているノートだが、リノートされてからある程度経過しているので購読しない
return;
}
diff --git a/packages/frontend/src/use/use-pagination.ts b/packages/frontend/src/use/use-pagination.ts
index e8d25c3473..12ce2a7f7d 100644
--- a/packages/frontend/src/use/use-pagination.ts
+++ b/packages/frontend/src/use/use-pagination.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { computed, isRef, onMounted, ref, watch } from 'vue';
+import { computed, isRef, onMounted, ref, shallowRef, triggerRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import type { ComputedRef, Ref, ShallowRef } from 'vue';
import { misskeyApi } from '@/utility/misskey-api.js';
@@ -40,9 +40,10 @@ export type PagingCtx(props: {
ctx: PagingCtx;
+ useShallowRef?: boolean;
}) {
- const items = ref([]);
- const queue = ref([]);
+ const items = props.useShallowRef ? shallowRef([]) : ref([]);
+ const queue = props.useShallowRef ? shallowRef([]) : ref([]);
const fetching = ref(true);
const moreFetching = ref(false);
const canFetchMore = ref(false);
@@ -142,8 +143,10 @@ export function usePagination(props: {
}).then(res => {
if (options.toQueue) {
queue.value.unshift(...res.toReversed());
+ if (props.useShallowRef) triggerRef(queue);
} else {
items.value.unshift(...res.toReversed());
+ if (props.useShallowRef) triggerRef(items);
}
});
}
@@ -155,18 +158,22 @@ export function usePagination(props: {
function unshiftItems(newItems: T[]) {
items.value.unshift(...newItems);
+ if (props.useShallowRef) triggerRef(items);
}
function pushItems(oldItems: T[]) {
items.value.push(...oldItems);
+ if (props.useShallowRef) triggerRef(items);
}
function prepend(item: T) {
items.value.unshift(item);
+ if (props.useShallowRef) triggerRef(items);
}
function enqueue(item: T) {
queue.value.unshift(item);
+ if (props.useShallowRef) triggerRef(queue);
}
function releaseQueue() {