wip
This commit is contained in:
parent
2d5c1fca68
commit
8ad49637c5
|
@ -4,8 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div v-if="muted && appearNote" :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)">
|
||||||
|
<MkUserName :user="appearNote.user"/>
|
||||||
|
</MkA>
|
||||||
|
</template>
|
||||||
|
</I18n>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!muted"
|
v-else-if="note && appearNote"
|
||||||
v-show="!isDeleted"
|
v-show="!isDeleted"
|
||||||
ref="el"
|
ref="el"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
|
@ -126,19 +135,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div v-else :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)">
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</MkA>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent } from 'vue';
|
import { computed, inject, onUnmounted, ref, shallowRef, Ref, defineAsyncComponent, watch, onActivated, onDeactivated } 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 MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
|
@ -162,7 +162,7 @@ import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu';
|
import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu';
|
||||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
import { noteManager } from '@/scripts/entity-manager';
|
||||||
import { deepClone } from '@/scripts/clone';
|
import { deepClone } from '@/scripts/clone';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip';
|
import { useTooltip } from '@/scripts/use-tooltip';
|
||||||
import { claimAchievement } from '@/scripts/achievements';
|
import { claimAchievement } from '@/scripts/achievements';
|
||||||
|
@ -173,32 +173,51 @@ import { showMovedDialog } from '@/scripts/show-moved-dialog';
|
||||||
import { shouldCollapsed } from '@/scripts/collapsed';
|
import { shouldCollapsed } from '@/scripts/collapsed';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: misskey.entities.Note;
|
note: { id: string };
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const inChannel = inject('inChannel', null);
|
const inChannel = inject('inChannel', null);
|
||||||
const currentClip = inject<Ref<misskey.entities.Clip> | null>('currentClip', null);
|
const currentClip = inject<Ref<misskey.entities.Clip> | null>('currentClip', null);
|
||||||
|
|
||||||
let note = $ref(deepClone(props.note));
|
const cachedNote = noteManager.get(props.note.id);
|
||||||
|
const overridingNote = shallowRef<Partial<misskey.entities.Note>>({});
|
||||||
// plugin
|
// plugin
|
||||||
if (noteViewInterruptors.length > 0) {
|
watch(cachedNote, async () => {
|
||||||
onMounted(async () => {
|
if (cachedNote.value == null) {
|
||||||
let result = deepClone(note);
|
isDeleted.value = true;
|
||||||
for (const interruptor of noteViewInterruptors) {
|
overridingNote.value = {};
|
||||||
result = await interruptor.handler(result);
|
return;
|
||||||
|
}
|
||||||
|
if (noteViewInterruptors.length > 0) {
|
||||||
|
overridingNote.value = {};
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
note = result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRenote = (
|
let result = deepClone(cachedNote.value);
|
||||||
|
for (const interruptor of noteViewInterruptors) {
|
||||||
|
result = await interruptor.handler(result) as misskey.entities.Note;
|
||||||
|
}
|
||||||
|
overridingNote.value = result;
|
||||||
|
});
|
||||||
|
const note = $computed<misskey.entities.Note | null>(() => {
|
||||||
|
if (cachedNote.value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...cachedNote.value,
|
||||||
|
...overridingNote.value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const isRenote = computed(() => (
|
||||||
|
note != null &&
|
||||||
note.renote != null &&
|
note.renote != null &&
|
||||||
note.text == null &&
|
note.text == null &&
|
||||||
note.fileIds.length === 0 &&
|
note.fileIds?.length === 0 &&
|
||||||
note.poll == null
|
note.poll == null
|
||||||
);
|
));
|
||||||
|
|
||||||
const el = shallowRef<HTMLElement>();
|
const el = shallowRef<HTMLElement>();
|
||||||
const menuButton = shallowRef<HTMLElement>();
|
const menuButton = shallowRef<HTMLElement>();
|
||||||
|
@ -206,19 +225,19 @@ const renoteButton = shallowRef<HTMLElement>();
|
||||||
const renoteTime = shallowRef<HTMLElement>();
|
const renoteTime = shallowRef<HTMLElement>();
|
||||||
const reactButton = shallowRef<HTMLElement>();
|
const reactButton = shallowRef<HTMLElement>();
|
||||||
const clipButton = shallowRef<HTMLElement>();
|
const clipButton = shallowRef<HTMLElement>();
|
||||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
let appearNote = $computed(() => isRenote.value ? note?.renote as misskey.entities.Note : note);
|
||||||
const isMyRenote = $i && ($i.id === note.userId);
|
const isMyRenote = $i && ($i.id === note?.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
const urls = appearNote?.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
||||||
const isLong = shouldCollapsed(appearNote);
|
const isLong = appearNote ? shouldCollapsed(appearNote) : false;
|
||||||
const collapsed = ref(appearNote.cw == null && isLong);
|
const collapsed = ref(appearNote?.cw == null && isLong);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(note === null);
|
||||||
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
const muted = ref(appearNote ? checkWordMute(appearNote, $i, defaultStore.state.mutedWords) : false);
|
||||||
const translation = ref<any>(null);
|
const translation = ref<any>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && !!appearNote?.user.instance);
|
||||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
|
const canRenote = computed(() => (!!appearNote && !!$i) && (['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id));
|
||||||
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
|
let renoteCollapsed = $ref(note && appearNote && defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => reply(true),
|
'r': () => reply(true),
|
||||||
|
@ -231,13 +250,9 @@ const keymap = {
|
||||||
's': () => showContent.value !== showContent.value,
|
's': () => showContent.value !== showContent.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
useNoteCapture({
|
|
||||||
rootEl: el,
|
|
||||||
note: $$(appearNote),
|
|
||||||
isDeletedRef: isDeleted,
|
|
||||||
});
|
|
||||||
|
|
||||||
useTooltip(renoteButton, async (showing) => {
|
useTooltip(renoteButton, async (showing) => {
|
||||||
|
if (!appearNote) return;
|
||||||
|
|
||||||
const renotes = await os.api('notes/renotes', {
|
const renotes = await os.api('notes/renotes', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
limit: 11,
|
limit: 11,
|
||||||
|
@ -267,6 +282,8 @@ function smallerVisibility(a: Visibility | string, b: Visibility | string): Visi
|
||||||
}
|
}
|
||||||
|
|
||||||
function renote(viaKeyboard = false) {
|
function renote(viaKeyboard = false) {
|
||||||
|
if (!appearNote || !canRenote.value) return;
|
||||||
|
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
|
|
||||||
|
@ -277,6 +294,8 @@ function renote(viaKeyboard = false) {
|
||||||
text: i18n.ts.inChannelRenote,
|
text: i18n.ts.inChannelRenote,
|
||||||
icon: 'ti ti-repeat',
|
icon: 'ti ti-repeat',
|
||||||
action: () => {
|
action: () => {
|
||||||
|
if (!appearNote) return;
|
||||||
|
|
||||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||||
if (el) {
|
if (el) {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
|
@ -296,6 +315,7 @@ function renote(viaKeyboard = false) {
|
||||||
text: i18n.ts.inChannelQuote,
|
text: i18n.ts.inChannelQuote,
|
||||||
icon: 'ti ti-quote',
|
icon: 'ti ti-quote',
|
||||||
action: () => {
|
action: () => {
|
||||||
|
if (!appearNote) return;
|
||||||
os.post({
|
os.post({
|
||||||
renote: appearNote,
|
renote: appearNote,
|
||||||
channel: appearNote.channel,
|
channel: appearNote.channel,
|
||||||
|
@ -308,6 +328,8 @@ function renote(viaKeyboard = false) {
|
||||||
text: i18n.ts.renote,
|
text: i18n.ts.renote,
|
||||||
icon: 'ti ti-repeat',
|
icon: 'ti ti-repeat',
|
||||||
action: () => {
|
action: () => {
|
||||||
|
if (!appearNote) return;
|
||||||
|
|
||||||
const el = renoteButton.value as HTMLElement | null | undefined;
|
const el = renoteButton.value as HTMLElement | null | undefined;
|
||||||
if (el) {
|
if (el) {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
|
@ -331,6 +353,7 @@ function renote(viaKeyboard = false) {
|
||||||
text: i18n.ts.quote,
|
text: i18n.ts.quote,
|
||||||
icon: 'ti ti-quote',
|
icon: 'ti ti-quote',
|
||||||
action: () => {
|
action: () => {
|
||||||
|
if (!appearNote) return;
|
||||||
os.post({
|
os.post({
|
||||||
renote: appearNote,
|
renote: appearNote,
|
||||||
});
|
});
|
||||||
|
@ -342,18 +365,19 @@ function renote(viaKeyboard = false) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
async function reply(viaKeyboard = false): void {
|
||||||
|
if (!appearNote) return;
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
os.post({
|
await os.post({
|
||||||
reply: appearNote,
|
reply: appearNote,
|
||||||
channel: appearNote.channel,
|
channel: appearNote.channel,
|
||||||
animation: !viaKeyboard,
|
animation: !viaKeyboard,
|
||||||
}, () => {
|
|
||||||
focus();
|
|
||||||
});
|
});
|
||||||
|
focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function react(viaKeyboard = false): void {
|
function react(viaKeyboard = false): void {
|
||||||
|
if (!appearNote) return;
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
if (appearNote.reactionAcceptance === 'likeOnly') {
|
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||||
|
@ -369,8 +393,10 @@ function react(viaKeyboard = false): void {
|
||||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (!reactButton.value) return;
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value, reaction => {
|
reactionPicker.show(reactButton.value, reaction => {
|
||||||
|
if (!appearNote) return;
|
||||||
os.api('notes/reactions/create', {
|
os.api('notes/reactions/create', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
|
@ -408,17 +434,20 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), ev).then(focus);
|
if (!note) return;
|
||||||
|
os.contextMenu(getNoteMenu({ note: note, translating, translation, isDeleted, currentClip: currentClip?.value }), ev).then(focus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function menu(viaKeyboard = false): void {
|
function menu(viaKeyboard = false): void {
|
||||||
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), menuButton.value, {
|
if (!note) return;
|
||||||
|
os.popupMenu(getNoteMenu({ note: note, translating, translation, isDeleted, currentClip: currentClip?.value }), menuButton.value, {
|
||||||
viaKeyboard,
|
viaKeyboard,
|
||||||
}).then(focus);
|
}).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clip() {
|
async function clip() {
|
||||||
|
if (!note) return;
|
||||||
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,22 +470,23 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
el.value.focus();
|
el.value?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function blur() {
|
function blur() {
|
||||||
el.value.blur();
|
el.value?.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusBefore() {
|
function focusBefore() {
|
||||||
focusPrev(el.value);
|
focusPrev(el.value!);
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusAfter() {
|
function focusAfter() {
|
||||||
focusNext(el.value);
|
focusNext(el.value!);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPromo() {
|
function readPromo() {
|
||||||
|
if (!appearNote) return;
|
||||||
os.api('promo/read', {
|
os.api('promo/read', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
});
|
});
|
||||||
|
@ -464,10 +494,35 @@ function readPromo() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function showReactions(): void {
|
function showReactions(): void {
|
||||||
|
if (!appearNote) return;
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/MkReactedUsersDialog.vue')), {
|
os.popup(defineAsyncComponent(() => import('@/components/MkReactedUsersDialog.vue')), {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
}, {}, 'closed');
|
}, {}, 'closed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unuse = ref<() => void>();
|
||||||
|
unuse.value = noteManager.useNote(props.note.id, true).unuse;
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (unuse.value) {
|
||||||
|
unuse.value();
|
||||||
|
unuse.value = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
if (!unuse.value) {
|
||||||
|
unuse.value = noteManager.useNote(props.note.id, true).unuse;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
if (unuse.value) {
|
||||||
|
unuse.value();
|
||||||
|
unuse.value = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo, tamaina and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
import { Note, UserLite } from "misskey-js/built/entities";
|
import { Note, UserLite } from "misskey-js/built/entities";
|
||||||
import { Ref, ref, ComputedRef, computed } from "vue";
|
import { Ref, ref, ComputedRef, computed } from "vue";
|
||||||
import { api } from "./api";
|
import { api } from "./api";
|
||||||
|
import { useStream } from '@/stream';
|
||||||
|
import { Stream } from "misskey-js";
|
||||||
|
import { $i } from "@/account";
|
||||||
|
|
||||||
export class EntitiyManager<T extends { id: string }> {
|
export class EntitiyManager<T extends { id: string }> {
|
||||||
private entities: Map<T['id'], Ref<T>>;
|
private entities: Map<T['id'], Ref<T>>;
|
||||||
|
@ -27,20 +35,53 @@ export class EntitiyManager<T extends { id: string }> {
|
||||||
export const userLiteManager = new EntitiyManager<UserLite>();
|
export const userLiteManager = new EntitiyManager<UserLite>();
|
||||||
|
|
||||||
type OmittedNote = Omit<Note, 'user' | 'renote' | 'reply'>;
|
type OmittedNote = Omit<Note, 'user' | 'renote' | 'reply'>;
|
||||||
type InternalCachedNote = Ref<OmittedNote>;
|
type CachedNoteSource = Ref<OmittedNote | null>;
|
||||||
|
type CachedNote = ComputedRef<Note | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ノートのキャッシュを管理する
|
||||||
|
* 基本的な使い方:
|
||||||
|
* 1. setでノートのデータをセットする
|
||||||
|
* 2. useNoteでデータを取得+監視
|
||||||
|
*/
|
||||||
export class NoteManager {
|
export class NoteManager {
|
||||||
private notes: Map<Note['id'], InternalCachedNote>;
|
/**
|
||||||
|
* ノートのソースとなるRef
|
||||||
|
* user, renote, replyを持たない
|
||||||
|
* nullは削除済みであることを表す
|
||||||
|
*/
|
||||||
|
private notesSource: Map<Note['id'], CachedNoteSource>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ソースからuser, renote, replyを取得したComputedRef
|
||||||
|
* nullは削除済みであることを表す
|
||||||
|
*/
|
||||||
|
private notesComputed: Map<Note['id'], CachedNote>;
|
||||||
private updatedAt: Map<Note['id'], number>;
|
private updatedAt: Map<Note['id'], number>;
|
||||||
private captureing: Map<Note['id'], number>;
|
private captureing: Map<Note['id'], number>;
|
||||||
|
private connection: Stream | null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.notes = new Map();
|
this.notesSource = new Map();
|
||||||
|
this.notesComputed = new Map();
|
||||||
this.updatedAt = new Map();
|
this.updatedAt = new Map();
|
||||||
this.captureing = new Map();
|
this.captureing = new Map();
|
||||||
|
this.connection = $i ? useStream() : null;
|
||||||
|
this.connection?.on('noteUpdated', this.onStreamNoteUpdated);
|
||||||
|
this.connection?.on('_connected_', () => {
|
||||||
|
// 再接続時に再キャプチャ
|
||||||
|
for (const [id, captureingNumber] of Array.from(this.captureing)) {
|
||||||
|
if (captureingNumber === 0) {
|
||||||
|
this.captureing.delete(id);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public set(_note: Note): ComputedRef<Note> {
|
this.connection?.send('s', { id });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public set(_note: Note): CachedNote {
|
||||||
const note: Note = { ..._note };
|
const note: Note = { ..._note };
|
||||||
userLiteManager.set(note.user);
|
userLiteManager.set(note.user);
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
@ -50,45 +91,199 @@ export class NoteManager {
|
||||||
delete note.renote;
|
delete note.renote;
|
||||||
if (note.reply) this.set(note.reply);
|
if (note.reply) this.set(note.reply);
|
||||||
delete note.reply;
|
delete note.reply;
|
||||||
const cached = this.notes.get(note.id);
|
const cached = this.notesSource.get(note.id);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
cached.value = note;
|
cached.value = note;
|
||||||
} else {
|
} else {
|
||||||
this.notes.set(note.id, ref(note));
|
this.notesSource.set(note.id, ref(note));
|
||||||
}
|
}
|
||||||
this.updatedAt.set(note.id, Date.now());
|
this.updatedAt.set(note.id, Date.now());
|
||||||
return this.get(note.id)!;
|
return this.get(note.id)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(id: string): ComputedRef<Note> | undefined {
|
public get(id: string): CachedNote {
|
||||||
const note: InternalCachedNote | undefined = this.notes.get(id);
|
if (!this.notesComputed.has(id)) {
|
||||||
if (!note) return undefined;
|
const note = this.notesSource.get(id);
|
||||||
|
|
||||||
|
this.notesComputed.set(id, computed<Note | null>(() => {
|
||||||
|
if (!note || !note.value) return null;
|
||||||
|
|
||||||
return computed<Note>(() => {
|
|
||||||
const user = userLiteManager.get(note.value.userId)!;
|
const user = userLiteManager.get(note.value.userId)!;
|
||||||
|
|
||||||
const renote = note.value.renoteId ? this.get(note.value.renoteId) : undefined;
|
const renote = note.value.renoteId ? this.get(note.value.renoteId) : undefined;
|
||||||
|
// renoteが削除されている場合はCASCADE削除されるためnullを返す
|
||||||
|
if (renote && !renote.value) return null;
|
||||||
|
|
||||||
const reply = note.value.replyId ? this.get(note.value.replyId) : undefined;
|
const reply = note.value.replyId ? this.get(note.value.replyId) : undefined;
|
||||||
|
if (reply && !reply.value) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...note.value,
|
...note.value,
|
||||||
user: user.value,
|
user: user.value,
|
||||||
renote: renote?.value,
|
renote: renote?.value ?? undefined,
|
||||||
reply: reply?.value,
|
reply: reply?.value ?? undefined,
|
||||||
};
|
};
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
return this.notesComputed.get(id)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetch(id: string, force = false): Promise<ComputedRef<Note>> {
|
public async fetch(id: string, force = false): Promise<CachedNote> {
|
||||||
if (!force) {
|
if (!force) {
|
||||||
const updatedAt = this.updatedAt.get(id);
|
const updatedAt = this.updatedAt.get(id);
|
||||||
if (updatedAt && Date.now() - updatedAt < 1000 * 30) {
|
if (updatedAt && Date.now() - updatedAt < 1000 * 30) {
|
||||||
const cachedNote = this.get(id);
|
const cachedNote = this.get(id);
|
||||||
if (cachedNote) {
|
if (cachedNote) {
|
||||||
return cachedNote as ComputedRef<Note>;
|
return cachedNote;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const fetchedNote = await api('notes/show', { noteId: id });
|
return api('notes/show', { noteId: id })
|
||||||
return this.set(fetchedNote);
|
.then(fetchedNote => this.set(fetchedNote))
|
||||||
|
.catch(() => {
|
||||||
|
// エラーが発生した場合はとりあえず削除されたものとして扱う
|
||||||
|
const cached = this.notesSource.get(id);
|
||||||
|
if (cached) {
|
||||||
|
cached.value = null;
|
||||||
|
} else {
|
||||||
|
this.notesSource.set(id, ref(null));
|
||||||
|
}
|
||||||
|
// updateAtはしない
|
||||||
|
return this.get(id)!;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onStreamNoteUpdated(noteData: any): void {
|
||||||
|
const { type, id, body } = noteData;
|
||||||
|
|
||||||
|
const note = this.notesSource.get(id);
|
||||||
|
|
||||||
|
if (!note || !note.value) {
|
||||||
|
this.connection?.send('un', { id });
|
||||||
|
this.captureing.delete(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'reacted': {
|
||||||
|
const reaction = body.reaction;
|
||||||
|
|
||||||
|
if (body.emoji && !(body.emoji.name in note.value.reactionEmojis)) {
|
||||||
|
note.value.reactionEmojis[body.emoji.name] = body.emoji.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
|
||||||
|
const currentCount = (note.value.reactions || {})[reaction] || 0;
|
||||||
|
|
||||||
|
note.value.reactions[reaction] = currentCount + 1;
|
||||||
|
|
||||||
|
if ($i && (body.userId === $i.id)) {
|
||||||
|
note.value.myReaction = reaction;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'unreacted': {
|
||||||
|
const reaction = body.reaction;
|
||||||
|
|
||||||
|
// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
|
||||||
|
const currentCount = (note.value.reactions || {})[reaction] || 0;
|
||||||
|
|
||||||
|
note.value.reactions[reaction] = Math.max(0, currentCount - 1);
|
||||||
|
if (note.value.reactions[reaction] === 0) delete note.value.reactions[reaction];
|
||||||
|
|
||||||
|
if ($i && (body.userId === $i.id)) {
|
||||||
|
note.value.myReaction = undefined;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'pollVoted': {
|
||||||
|
const choice = body.choice;
|
||||||
|
|
||||||
|
const choices = [...note.value.poll!.choices];
|
||||||
|
choices[choice] = {
|
||||||
|
...choices[choice],
|
||||||
|
votes: choices[choice].votes + 1,
|
||||||
|
...($i && (body.userId === $i.id) ? {
|
||||||
|
isVoted: true,
|
||||||
|
} : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
note.value.poll!.choices = choices;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'deleted': {
|
||||||
|
note.value = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private capture(id: string, markRead = true): void {
|
||||||
|
if (!this.notesSource.has(id)) return;
|
||||||
|
|
||||||
|
const captureingNumber = this.captureing.get(id);
|
||||||
|
if (typeof captureingNumber === 'number' && captureingNumber > 0) {
|
||||||
|
this.captureing.set(id, captureingNumber + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.connection) {
|
||||||
|
// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
|
||||||
|
this.connection.send(markRead ? 'sr' : 's', { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.captureing.set(id, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private decapture(id: string): void {
|
||||||
|
if (!this.notesSource.has(id)) return;
|
||||||
|
|
||||||
|
const captureingNumber = this.captureing.get(id);
|
||||||
|
if (typeof captureingNumber === 'number' && captureingNumber > 1) {
|
||||||
|
this.captureing.set(id, captureingNumber - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.connection) {
|
||||||
|
this.connection.send('un', { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.captureing.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ノートを取得・監視
|
||||||
|
* キャプチャが要らなくなったら必ずunuseすること
|
||||||
|
* @param id note id
|
||||||
|
* @returns { note, unuse } note: CachedNote | Promise<CachedNote>, unuse: () => void
|
||||||
|
*/
|
||||||
|
public useNote(id: string, shoudFetch: true): { note: Promise<CachedNote>, unuse: () => void };
|
||||||
|
public useNote(id: string, shoudFetch = false) {
|
||||||
|
const note = (!this.notesSource.has(id) || shoudFetch) ? this.fetch(id) : this.get(id)!;
|
||||||
|
let using = false;
|
||||||
|
const CapturePromise = Promise.resolve(note)
|
||||||
|
.then(() => {
|
||||||
|
this.capture(id);
|
||||||
|
using = true;
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
|
||||||
|
const unuse = () => {
|
||||||
|
CapturePromise.then(() => {
|
||||||
|
if (!using) return;
|
||||||
|
this.decapture(id);
|
||||||
|
using = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
note,
|
||||||
|
unuse,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const noteManager = new NoteManager();
|
||||||
|
|
|
@ -93,7 +93,6 @@ export async function getNoteClipMenu(props: {
|
||||||
|
|
||||||
export function getNoteMenu(props: {
|
export function getNoteMenu(props: {
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
menuButton: Ref<HTMLElement>;
|
|
||||||
translation: Ref<any>;
|
translation: Ref<any>;
|
||||||
translating: Ref<boolean>;
|
translating: Ref<boolean>;
|
||||||
isDeleted: Ref<boolean>;
|
isDeleted: Ref<boolean>;
|
||||||
|
|
Loading…
Reference in New Issue