This commit is contained in:
syuilo 2025-05-03 17:34:59 +09:00
parent 607196e863
commit d01a49d19a
8 changed files with 59 additions and 41 deletions

View File

@ -6,11 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
v-if="!hardMuted && muted === false"
v-show="!isDeleted"
ref="rootEl"
v-hotkey="keymap"
:class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]"
:tabindex="isDeleted ? '-1' : '0'"
tabindex="0"
>
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
@ -241,6 +240,7 @@ import { getAppearNote } from '@/utility/get-appear-note.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
import { DI } from '@/di.js';
import { globalEvents } from '@/events.js';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@ -273,10 +273,6 @@ if (noteViewInterruptors.length > 0) {
for (const interruptor of noteViewInterruptors) {
try {
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
if (result === null) {
isDeleted.value = true;
return;
}
} catch (err) {
console.error(err);
}
@ -308,7 +304,6 @@ const parsed = computed(() => appearNote.text ? mfm.parse(appearNote.text) : nul
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.renote?.url !== url && appearNote.renote?.uri !== url) : null);
const isLong = shouldCollapsed(appearNote, urls.value ?? []);
const collapsed = ref(appearNote.cw == null && isLong);
const isDeleted = ref(false);
const muted = ref(checkMute(appearNote, $i?.mutedWords));
const hardMuted = ref(props.withHardMute && checkMute(appearNote, $i?.hardMutedWords, true));
const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord);
@ -595,7 +590,7 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, isDeleted, currentClip: currentClip?.value });
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, currentClip: currentClip?.value });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
@ -605,7 +600,7 @@ function showMenu(): void {
return;
}
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, isDeleted, currentClip: currentClip?.value });
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, currentClip: currentClip?.value });
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
}
@ -614,7 +609,7 @@ async function clip(): Promise<void> {
return;
}
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
os.popupMenu(await getNoteClipMenu({ note: note, currentClip: currentClip?.value }), clipButton.value).then(focus);
}
function showRenoteMenu(): void {
@ -630,8 +625,9 @@ function showRenoteMenu(): void {
action: () => {
misskeyApi('notes/delete', {
noteId: note.id,
}).then(() => {
globalEvents.emit('noteDeleted', note.id);
});
isDeleted.value = true;
},
};
}
@ -682,7 +678,6 @@ function readPromo() {
misskeyApi('promo/read', {
noteId: appearNote.id,
});
isDeleted.value = true;
}
function emitUpdReaction(emoji: string, delta: number) {

View File

@ -5,12 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
v-if="!muted"
v-show="!isDeleted"
v-if="!muted && !isDeleted"
ref="rootEl"
v-hotkey="keymap"
:class="$style.root"
:tabindex="isDeleted ? '-1' : '0'"
tabindex="0"
>
<div v-if="appearNote.reply && appearNote.reply.replyId">
<div v-if="!conversationLoaded" style="padding: 16px">
@ -217,7 +216,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
<div v-else-if="muted" class="_panel" :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
@ -274,6 +273,7 @@ import { getAppearNote } from '@/utility/get-appear-note.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
import { DI } from '@/di.js';
import { globalEvents, useGlobalEvent } from '@/events.js';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@ -294,10 +294,6 @@ if (noteViewInterruptors.length > 0) {
for (const interruptor of noteViewInterruptors) {
try {
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
if (result === null) {
isDeleted.value = true;
return;
}
} catch (err) {
console.error(err);
}
@ -336,6 +332,12 @@ const conversation = ref<Misskey.entities.Note[]>([]);
const replies = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i?.id);
useGlobalEvent('noteDeleted', (noteId) => {
if (noteId === note.id || noteId === appearNote.id) {
isDeleted.value = true;
}
});
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup',
url: `https://${host}/notes/${appearNote.id}`,
@ -544,18 +546,18 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, isDeleted });
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
function showMenu(): void {
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, isDeleted });
const { menu, cleanup } = getNoteMenu({ note: note, translating, translation });
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
}
async function clip(): Promise<void> {
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus);
os.popupMenu(await getNoteClipMenu({ note: note }), clipButton.value).then(focus);
}
function showRenoteMenu(): void {
@ -568,8 +570,9 @@ function showRenoteMenu(): void {
action: () => {
misskeyApi('notes/delete', {
noteId: note.id,
}).then(() => {
globalEvents.emit('noteDeleted', note.id);
});
isDeleted.value = true;
},
}], renoteTime.value);
}

View File

@ -35,6 +35,7 @@ import MkNote from '@/components/MkNote.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js';
import { globalEvents, useGlobalEvent } from '@/events.js';
const props = withDefaults(defineProps<{
pagination: PagingCtx;
@ -47,6 +48,10 @@ const props = withDefaults(defineProps<{
const pagingComponent = useTemplateRef('pagingComponent');
useGlobalEvent('noteDeleted', (noteId) => {
pagingComponent.value?.paginator.removeItem(noteId);
});
function reload() {
return pagingComponent.value?.paginator.reload();
}

View File

@ -71,7 +71,7 @@ import MkNote from '@/components/MkNote.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js';
import { globalEvents } from '@/events.js';
import { globalEvents, useGlobalEvent } from '@/events.js';
const props = withDefaults(defineProps<{
src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
@ -156,13 +156,17 @@ if (!store.s.realtimeMode) {
afterMounted: true,
});
globalEvents.on('notePosted', (note: Misskey.entities.Note) => {
useGlobalEvent('notePosted', (note) => {
paginator.fetchNewer({
toQueue: !isTop(),
});
});
}
useGlobalEvent('noteDeleted', (noteId) => {
paginator.removeItem(noteId);
});
function releaseQueue() {
paginator.releaseQueue();
scrollToTop(rootEl.value);

View File

@ -5,10 +5,24 @@
import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js';
import { onBeforeUnmount } from 'vue';
export const globalEvents = new EventEmitter<{
type Events = {
themeChanging: () => void;
themeChanged: () => void;
clientNotification: (notification: Misskey.entities.Notification) => void;
notePosted: (note: Misskey.entities.Note) => void;
}>();
noteDeleted: (noteId: Misskey.entities.Note['id']) => void;
};
export const globalEvents = new EventEmitter<Events>();
export function useGlobalEvent<T extends keyof Events>(
event: T,
callback: Events[T],
): void {
globalEvents.on(event, callback);
onBeforeUnmount(() => {
globalEvents.off(event, callback);
});
}

View File

@ -12,12 +12,12 @@ import { $i } from '@/i.js';
import { store } from '@/store.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { prefer } from '@/preferences.js';
import { globalEvents } from '@/events.js';
export const noteEvents = new EventEmitter<{
[ev: `reacted:${string}`]: (ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }) => void;
[ev: `unreacted:${string}`]: (ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }) => void;
[ev: `pollVoted:${string}`]: (ctx: { userId: Misskey.entities.User['id']; choice: string; }) => void;
[ev: `deleted:${string}`]: () => void;
}>();
const fetchEvent = new EventEmitter<{
@ -113,7 +113,6 @@ function pollingSubscribe(props: {
function realtimeSubscribe(props: {
note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>;
isDeletedRef: Ref<boolean>;
}): void {
const note = props.note;
const connection = useStream();
@ -151,7 +150,7 @@ function realtimeSubscribe(props: {
}
case 'deleted': {
noteEvents.emit(`deleted:${id}`);
globalEvents.emit('noteDeleted', id);
break;
}
}
@ -198,7 +197,6 @@ export function useNoteCapture(props: {
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;
@ -256,15 +254,10 @@ export function useNoteCapture(props: {
$note.pollChoices = choices;
}
function onDeleted(): void {
$note.isDeleted = true;
}
onUnmounted(() => {
noteEvents.off(`reacted:${note.id}`, onReacted);
noteEvents.off(`unreacted:${note.id}`, onUnreacted);
noteEvents.off(`pollVoted:${note.id}`, onPollVoted);
noteEvents.off(`deleted:${note.id}`, onDeleted);
});
// 投稿からある程度経過している(=タイムラインを遡って表示した)ノートは、イベントが発生する可能性が低いためそもそも購読しない

View File

@ -197,6 +197,8 @@ export function usePagination<T extends MisskeyEntity>(props: {
}
function removeItem(id: string) {
// TODO: queueからも消す
const index = items.value.findIndex(x => x.id === id);
if (index !== -1) {
items.value.splice(index, 1);
@ -205,6 +207,8 @@ export function usePagination<T extends MisskeyEntity>(props: {
}
function updateItem(id: string, updator: (item: T) => T) {
// TODO: queueのも更新
const index = items.value.findIndex(x => x.id === id);
if (index !== -1) {
const item = items.value[index]!;

View File

@ -29,7 +29,6 @@ import { globalEvents } from '@/events.js';
export async function getNoteClipMenu(props: {
note: Misskey.entities.Note;
isDeleted: Ref<boolean>;
currentClip?: Misskey.entities.Clip;
}) {
function getClipName(clip: Misskey.entities.Clip) {
@ -69,7 +68,6 @@ export async function getNoteClipMenu(props: {
}
}));
});
if (props.currentClip?.id === clip.id) props.isDeleted.value = true;
}
} else if (err.id === 'f0dba960-ff73-4615-8df4-d6ac5d9dc118') {
os.alert({
@ -179,7 +177,6 @@ export function getNoteMenu(props: {
note: Misskey.entities.Note;
translation: Ref<Misskey.entities.NotesTranslateResponse | null>;
translating: Ref<boolean>;
isDeleted: Ref<boolean>;
currentClip?: Misskey.entities.Clip;
}) {
const appearNote = getAppearNote(props.note);
@ -195,6 +192,8 @@ export function getNoteMenu(props: {
misskeyApi('notes/delete', {
noteId: appearNote.id,
}).then(() => {
globalEvents.emit('noteDeleted', appearNote.id);
});
if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
@ -212,6 +211,8 @@ export function getNoteMenu(props: {
misskeyApi('notes/delete', {
noteId: appearNote.id,
}).then(() => {
globalEvents.emit('noteDeleted', appearNote.id);
});
os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
@ -252,7 +253,6 @@ export function getNoteMenu(props: {
async function unclip(): Promise<void> {
if (!props.currentClip) return;
os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id });
props.isDeleted.value = true;
}
async function promote(): Promise<void> {