This commit is contained in:
tamaina 2023-07-30 04:05:29 +00:00
parent b05f2bb834
commit 0c75d9a9f9
10 changed files with 241 additions and 192 deletions

View File

@ -138,8 +138,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, inject, onUnmounted, ref, shallowRef, Ref, defineAsyncComponent, watch, onActivated, onDeactivated } from 'vue';
import * as mfm from 'mfm-js';
import { inject, onUnmounted, ref, shallowRef, Ref, defineAsyncComponent, onActivated, onDeactivated, onMounted } from 'vue';
import * as misskey from 'misskey-js';
import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
@ -156,69 +155,37 @@ import { focusPrev, focusNext } from '@/scripts/focus';
import { checkWordMute } from '@/scripts/check-word-mute';
import { userPage } from '@/filters/user';
import * as os from '@/os';
import { defaultStore, noteViewInterruptors } from '@/store';
import { defaultStore } from '@/store';
import { reactionPicker } from '@/scripts/reaction-picker';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu';
import { noteManager } from '@/scripts/entity-manager';
import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip';
import { claimAchievement } from '@/scripts/achievements';
import { getNoteSummary } from '@/scripts/get-note-summary';
import { MenuItem } from '@/types/menu';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog';
import { shouldCollapsed } from '@/scripts/collapsed';
const props = defineProps<{
note: { id: string };
pinned?: boolean;
setNote?: boolean;
}>();
const inChannel = inject('inChannel', null);
const currentClip = inject<Ref<misskey.entities.Clip> | null>('currentClip', null);
const cachedNote = noteManager.get(props.note.id);
const overridingNote = shallowRef<Partial<misskey.entities.Note>>({});
// plugin
watch(cachedNote, async () => {
if (cachedNote.value == null) {
isDeleted.value = true;
overridingNote.value = {};
return;
}
isDeleted.value = false;
if (noteViewInterruptors.length > 0) {
overridingNote.value = {};
return;
}
if (props.setNote) {
noteManager.set(props.note as any);
}
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.text == null &&
note.fileIds?.length === 0 &&
note.poll == null
));
const {
note, interruptorUnwatch, executeInterruptor,
isRenote, isMyRenote, appearNote,
urls, isLong, canRenote,
} = noteManager.getNoteViewBase(props.note.id);
const el = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
@ -226,26 +193,20 @@ const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote.value ? note?.renote as misskey.entities.Note : note);
const isMyRenote = $i && ($i.id === note?.userId);
const showContent = ref(false);
const urls = appearNote?.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
const isLong = appearNote ? shouldCollapsed(appearNote) : false;
const collapsed = ref(appearNote?.cw == null && isLong);
const collapsed = ref(appearNote.value?.cw == null && isLong);
const isDeleted = ref(note === null);
const muted = ref(appearNote ? checkWordMute(appearNote, $i, defaultStore.state.mutedWords) : false);
const muted = ref(appearNote.value ? checkWordMute(appearNote.value, $i, defaultStore.state.mutedWords) : false);
const translation = ref<any>(null);
const translating = ref(false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && !!appearNote?.user.instance);
const canRenote = computed(() => (!!appearNote && !!$i) && (['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id));
let renoteCollapsed = $ref(
note &&
appearNote &&
note.value &&
appearNote.value &&
defaultStore.state.collapseRenotes &&
isRenote.value &&
(
($i && ($i.id === note.userId || $i.id === appearNote.userId)) ||
(appearNote.myReaction != null)
($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) ||
(appearNote.value.myReaction != null)
)
);
@ -261,10 +222,10 @@ const keymap = {
};
useTooltip(renoteButton, async (showing) => {
if (!appearNote) return;
if (!appearNote.value) return;
const renotes = await os.api('notes/renotes', {
noteId: appearNote.id,
noteId: appearNote.value.id,
limit: 11,
});
@ -275,7 +236,7 @@ useTooltip(renoteButton, async (showing) => {
os.popup(MkUsersTooltip, {
showing,
users,
count: appearNote.renoteCount,
count: appearNote.value.renoteCount,
targetElement: renoteButton.value,
}, {}, 'closed');
});
@ -292,19 +253,19 @@ function smallerVisibility(a: Visibility | string, b: Visibility | string): Visi
}
function renote(viaKeyboard = false) {
if (!appearNote || !canRenote.value) return;
if (!appearNote.value || !canRenote.value) return;
pleaseLogin();
showMovedDialog();
let items = [] as MenuItem[];
if (appearNote.channel) {
if (appearNote.value.channel) {
items = items.concat([{
text: i18n.ts.inChannelRenote,
icon: 'ti ti-repeat',
action: () => {
if (!appearNote) return;
if (!appearNote.value) return;
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
@ -315,8 +276,8 @@ function renote(viaKeyboard = false) {
}
os.api('notes/create', {
renoteId: appearNote.id,
channelId: appearNote.channelId,
renoteId: appearNote.value.id,
channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
});
@ -325,10 +286,10 @@ function renote(viaKeyboard = false) {
text: i18n.ts.inChannelQuote,
icon: 'ti ti-quote',
action: () => {
if (!appearNote) return;
if (!appearNote.value) return;
os.post({
renote: appearNote,
channel: appearNote.channel,
renote: appearNote.value,
channel: appearNote.value.channel,
});
},
}, null]);
@ -338,7 +299,7 @@ function renote(viaKeyboard = false) {
text: i18n.ts.renote,
icon: 'ti ti-repeat',
action: () => {
if (!appearNote) return;
if (!appearNote.value) return;
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
@ -353,8 +314,8 @@ function renote(viaKeyboard = false) {
os.api('notes/create', {
localOnly,
visibility: smallerVisibility(appearNote.visibility, configuredVisibility),
renoteId: appearNote.id,
visibility: smallerVisibility(appearNote.value.visibility, configuredVisibility),
renoteId: appearNote.value.id,
}).then(() => {
os.toast(i18n.ts.renoted);
});
@ -363,9 +324,9 @@ function renote(viaKeyboard = false) {
text: i18n.ts.quote,
icon: 'ti ti-quote',
action: () => {
if (!appearNote) return;
if (!appearNote.value) return;
os.post({
renote: appearNote,
renote: appearNote.value,
});
},
}]);
@ -376,23 +337,23 @@ function renote(viaKeyboard = false) {
}
async function reply(viaKeyboard = false): void {
if (!appearNote) return;
if (!appearNote.value) return;
pleaseLogin();
await os.post({
reply: appearNote,
channel: appearNote.channel,
reply: appearNote.value,
channel: appearNote.value.channel,
animation: !viaKeyboard,
});
focus();
}
function react(viaKeyboard = false): void {
if (!appearNote) return;
if (!appearNote.value) return;
pleaseLogin();
showMovedDialog();
if (appearNote.reactionAcceptance === 'likeOnly') {
if (appearNote.value.reactionAcceptance === 'likeOnly') {
os.api('notes/reactions/create', {
noteId: appearNote.id,
noteId: appearNote.value.id,
reaction: '❤️',
});
const el = reactButton.value as HTMLElement | null | undefined;
@ -406,12 +367,12 @@ function react(viaKeyboard = false): void {
if (!reactButton.value) return;
blur();
reactionPicker.show(reactButton.value, reaction => {
if (!appearNote) return;
if (!appearNote.value) return;
os.api('notes/reactions/create', {
noteId: appearNote.id,
noteId: appearNote.value.id,
reaction: reaction,
});
if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead');
}
}, () => {
@ -438,39 +399,40 @@ function onContextmenu(ev: MouseEvent): void {
}
};
if (isLink(ev.target)) return;
if (window.getSelection().toString() !== '') return;
if (window.getSelection()?.toString() !== '') return;
if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault();
react();
} else {
if (!note) return;
os.contextMenu(getNoteMenu({ note: note, translating, translation, isDeleted, currentClip: currentClip?.value }), ev).then(focus);
if (!note.value) return;
os.contextMenu(getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value }), ev).then(focus);
}
}
function menu(viaKeyboard = false): void {
if (!note) return;
os.popupMenu(getNoteMenu({ note: note, translating, translation, isDeleted, currentClip: currentClip?.value }), menuButton.value, {
if (!note.value) return;
os.popupMenu(getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value }), menuButton.value, {
viaKeyboard,
}).then(focus);
}
async function clip() {
if (!note) return;
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
if (!note.value) return;
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return;
if (!isMyRenote.value) return;
pleaseLogin();
os.popupMenu([{
text: i18n.ts.unrenote,
icon: 'ti ti-trash',
danger: true,
action: () => {
if (!note.value) return;
os.api('notes/delete', {
noteId: note.id,
noteId: note.value.id,
});
isDeleted.value = true;
},
@ -496,28 +458,33 @@ function focusAfter() {
}
function readPromo() {
if (!appearNote) return;
if (!appearNote.value) return;
os.api('promo/read', {
noteId: appearNote.id,
noteId: appearNote.value.id,
});
isDeleted.value = true;
}
function showReactions(): void {
if (!appearNote) return;
if (!appearNote.value) return;
os.popup(defineAsyncComponent(() => import('@/components/MkReactedUsersDialog.vue')), {
noteId: appearNote.id,
noteId: appearNote.value.id,
}, {}, 'closed');
}
const unuse = ref<() => void>();
unuse.value = noteManager.useNote(props.note.id, true).unuse;
onMounted(() => {
executeInterruptor();
});
onUnmounted(() => {
if (unuse.value) {
unuse.value();
unuse.value = undefined;
}
interruptorUnwatch();
});
onActivated(() => {

View File

@ -4,14 +4,23 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div v-if="muted && note && appearNote" 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)">
<MkUserName :user="appearNote.user"/>
</MkA>
</template>
</I18n>
</div>
<div
v-if="!muted"
v-else-if="note && appearNote"
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
:class="$style.root"
>
<MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/>
<MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note" :setNote="true"/>
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
<div v-if="isRenote" :class="$style.renote">
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
@ -125,23 +134,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</button>
</footer>
</article>
<MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/>
</div>
<div v-else 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)">
<MkUserName :user="appearNote.user"/>
</MkA>
</template>
</I18n>
<MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true" :setNote="true"/>
</div>
</template>
<script lang="ts" setup>
import { computed, inject, onMounted, ref, shallowRef } from 'vue';
import * as mfm from 'mfm-js';
import * as misskey from 'misskey-js';
import { inject, onMounted, onUnmounted, ref, shallowRef } from 'vue';
import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
@ -156,46 +154,36 @@ import { checkWordMute } from '@/scripts/check-word-mute';
import { userPage } from '@/filters/user';
import { notePage } from '@/filters/note';
import * as os from '@/os';
import { defaultStore, noteViewInterruptors } from '@/store';
import { defaultStore } from '@/store';
import { reactionPicker } from '@/scripts/reaction-picker';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip';
import { claimAchievement } from '@/scripts/achievements';
import { MenuItem } from '@/types/menu';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog';
import { noteManager } from '@/scripts/entity-manager';
import { Note } from 'misskey-js/built/entities';
const props = defineProps<{
note: misskey.entities.Note;
note: { id: string };
pinned?: boolean;
setNote?: boolean;
}>();
const inChannel = inject('inChannel', null);
let note = $ref(deepClone(props.note));
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
let result = deepClone(note);
for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result);
}
note = result;
});
if (props.setNote) {
noteManager.set(props.note as any);
}
const isRenote = (
note.renote != null &&
note.text == null &&
note.fileIds.length === 0 &&
note.poll == null
);
const {
note, interruptorUnwatch, executeInterruptor,
isRenote, isMyRenote, appearNote,
urls, canRenote, showTicker,
} = noteManager.getNoteViewBase(props.note.id);
const el = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
@ -203,18 +191,13 @@ const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false);
const isDeleted = ref(false);
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
const muted = ref(appearNote.value && checkWordMute(appearNote.value, $i, defaultStore.state.mutedWords));
const translation = ref(null);
const translating = ref(false);
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
const conversation = ref<misskey.entities.Note[]>([]);
const replies = ref<misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
const conversation = ref<Note[]>([]);
const replies = ref<Note[]>([]);
const keymap = {
'r': () => reply(true),
@ -225,15 +208,11 @@ const keymap = {
's': () => showContent.value !== showContent.value,
};
useNoteCapture({
rootEl: el,
note: $$(appearNote),
isDeletedRef: isDeleted,
});
useTooltip(renoteButton, async (showing) => {
if (!appearNote.value) return;
const renotes = await os.api('notes/renotes', {
noteId: appearNote.id,
noteId: appearNote.value.id,
limit: 11,
});
@ -244,7 +223,7 @@ useTooltip(renoteButton, async (showing) => {
os.popup(MkUsersTooltip, {
showing,
users,
count: appearNote.renoteCount,
count: appearNote.value.renoteCount,
targetElement: renoteButton.value,
}, {}, 'closed');
});
@ -255,7 +234,7 @@ function renote(viaKeyboard = false) {
let items = [] as MenuItem[];
if (appearNote.channel) {
if (appearNote.value.channel) {
items = items.concat([{
text: i18n.ts.inChannelRenote,
icon: 'ti ti-repeat',
@ -269,8 +248,8 @@ function renote(viaKeyboard = false) {
}
os.api('notes/create', {
renoteId: appearNote.id,
channelId: appearNote.channelId,
renoteId: appearNote.value.id,
channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
});
@ -280,8 +259,8 @@ function renote(viaKeyboard = false) {
icon: 'ti ti-quote',
action: () => {
os.post({
renote: appearNote,
channel: appearNote.channel,
renote: appearNote.value,
channel: appearNote.value.channel,
});
},
}, null]);
@ -300,7 +279,7 @@ function renote(viaKeyboard = false) {
}
os.api('notes/create', {
renoteId: appearNote.id,
renoteId: appearNote.value.id,
}).then(() => {
os.toast(i18n.ts.renoted);
});
@ -310,7 +289,7 @@ function renote(viaKeyboard = false) {
icon: 'ti ti-quote',
action: () => {
os.post({
renote: appearNote,
renote: appearNote.value,
});
},
}]);
@ -324,8 +303,8 @@ function reply(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
os.post({
reply: appearNote,
channel: appearNote.channel,
reply: appearNote.value,
channel: appearNote.value.channel,
animation: !viaKeyboard,
}, () => {
focus();
@ -335,9 +314,9 @@ function reply(viaKeyboard = false): void {
function react(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
if (appearNote.reactionAcceptance === 'likeOnly') {
if (appearNote.value.reactionAcceptance === 'likeOnly') {
os.api('notes/reactions/create', {
noteId: appearNote.id,
noteId: appearNote.value.id,
reaction: '❤️',
});
const el = reactButton.value as HTMLElement | null | undefined;
@ -351,10 +330,10 @@ function react(viaKeyboard = false): void {
blur();
reactionPicker.show(reactButton.value, reaction => {
os.api('notes/reactions/create', {
noteId: appearNote.id,
noteId: appearNote.value.id,
reaction: reaction,
});
if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead');
}
}, () => {
@ -379,28 +358,31 @@ function onContextmenu(ev: MouseEvent): void {
}
};
if (isLink(ev.target)) return;
if (window.getSelection().toString() !== '') return;
if (window.getSelection()?.toString() !== '') return;
if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault();
react();
} else {
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), ev).then(focus);
if (!note.value) return;
os.contextMenu(getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted }), ev).then(focus);
}
}
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), menuButton.value, {
if (!note.value) return;
os.popupMenu(getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted }), menuButton.value, {
viaKeyboard,
}).then(focus);
}
async function clip() {
os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus);
if (!note.value) return;
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return;
if (!isMyRenote.value) return;
pleaseLogin();
os.popupMenu([{
text: i18n.ts.unrenote,
@ -418,27 +400,44 @@ function showRenoteMenu(viaKeyboard = false): void {
}
function focus() {
el.value.focus();
el.value?.focus();
}
function blur() {
el.value.blur();
el.value?.blur();
}
os.api('notes/children', {
noteId: appearNote.id,
const { note: fetching, unuse } = noteManager.useNote(props.note.id, true);
onMounted(async () => {
await fetching;
await executeInterruptor();
muted.value = appearNote.value && checkWordMute(appearNote.value, $i, defaultStore.state.mutedWords);
if (appearNote.value) {
os.api('notes/children', {
noteId: appearNote.value.id,
limit: 30,
}).then(res => {
}).then(res => {
replies.value = res;
});
if (appearNote.value.replyId) {
os.api('notes/conversation', {
noteId: appearNote.value.replyId,
}).then(res => {
if (!res) return;
conversation.value = res;
});
}
};
});
if (appearNote.replyId) {
os.api('notes/conversation', {
noteId: appearNote.replyId,
}).then(res => {
conversation.value = res.reverse();
});
}
onUnmounted(() => {
unuse();
interruptorUnwatch();
});
</script>
<style lang="scss" module>

View File

@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<template v-if="depth < 5">
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="$style.reply" :detail="true" :depth="depth + 1"/>
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="$style.reply" :detail="true" :depth="depth + 1" :setNote="true"/>
</template>
<div v-else :class="$style.more">
<MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></MkA>
@ -32,7 +32,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { } from 'vue';
import * as misskey from 'misskey-js';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
import MkCwButton from '@/components/MkCwButton.vue';
@ -40,10 +39,12 @@ import { notePage } from '@/filters/note';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { $i } from '@/account';
import { noteManager } from '@/scripts/entity-manager';
const props = withDefaults(defineProps<{
note: misskey.entities.Note;
note: { id: string };
detail?: boolean;
setNote?: boolean;
// how many notes are in between this one and the note being viewed in detail
depth?: number;
@ -51,8 +52,14 @@ const props = withDefaults(defineProps<{
depth: 1,
});
if (props.setNote) {
noteManager.set(props.note as any);
}
const note = noteManager.get(props.note.id);
let showContent = $ref(false);
let replies: misskey.entities.Note[] = $ref([]);
let replies: { id: string }[] = $ref([]);
if (props.detail) {
os.api('notes/children', {

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items: notifications, denyMoveTransition }">
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true" :denyMoveTransition="denyMoveTransition">
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="`showNotificationAsNote:${notification.id}`" :note="notification.note"/>
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="`showNotificationAsNote:${notification.id}`" :note="notification.note" :setNote="true"/>
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel notification"/>
</MkDateSeparatedList>
</template>

View File

@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div style="margin: 1em 0;">
<MkNote v-if="note && !block.detailed" :key="note.id + ':normal'" v-model:note="note"/>
<MkNoteDetailed v-if="note && block.detailed" :key="note.id + ':detail'" v-model:note="note"/>
<MkNote v-if="note && !block.detailed" :key="note.id + ':normal'" v-model:note="note" :setNote="true"/>
<MkNoteDetailed v-if="note && block.detailed" :key="note.id + ':detail'" v-model:note="note" :setNote="true"/>
</div>
</template>

View File

@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFoldableSection>
<template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
<div v-if="channel.pinnedNotes.length > 0" class="_gaps">
<MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/>
<MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note" :setNote="true"/>
</div>
</MkFoldableSection>
</div>

View File

@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-if="!showNext && hasNext" :class="$style.loadNext" @click="showNext = true"><i class="ti ti-chevron-up"></i></MkButton>
<div class="_margin _gaps_s">
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
<MkNoteDetailed :key="note.id" v-model:note="note" :class="$style.note"/>
<MkNoteDetailed :key="note.id" v-model:note="note" :setNote="true" :class="$style.note"/>
</div>
<div v-if="clips && clips.length > 0" class="_margin">
<div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div>
@ -56,6 +56,7 @@ import { i18n } from '@/i18n';
import { dateString } from '@/filters/date';
import MkClipPreview from '@/components/MkClipPreview.vue';
import { defaultStore } from '@/store';
import { noteManager } from '@/scripts/entity-manager';
const props = defineProps<{
noteId: string;
@ -115,7 +116,13 @@ function fetchNote() {
]).then(([_clips, prev, next]) => {
clips = _clips;
hasPrev = prev.length !== 0;
prev.map(n => {
noteManager.set(n);
});
hasNext = next.length !== 0;
next.map(n => {
noteManager.set(n);
});
});
}).catch(err => {
error = err;

View File

@ -15,8 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
<MkSwitch v-model="props.modelValue.detailed"><span>{{ i18n.ts._pages.blocks._note.detailed }}</span></MkSwitch>
<MkNote v-if="note && !props.modelValue.detailed" :key="note.id + ':normal'" v-model:note="note" style="margin-bottom: 16px;"/>
<MkNoteDetailed v-if="note && props.modelValue.detailed" :key="note.id + ':detail'" v-model:note="note" style="margin-bottom: 16px;"/>
<MkNote v-if="note && !props.modelValue.detailed" :key="note.id + ':normal'" v-model:note="note" :setNote="true" style="margin-bottom: 16px;"/>
<MkNoteDetailed v-if="note && props.modelValue.detailed" :key="note.id + ':detail'" v-model:note="note" :setNote="true" style="margin-bottom: 16px;"/>
</section>
</XContainer>
</template>

View File

@ -123,7 +123,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="contents _gaps">
<div v-if="user.pinnedNotes.length > 0" class="_gaps">
<MkNote v-for="note in user.pinnedNotes" :key="note.id" class="note _panel" :note="note" :pinned="true"/>
<MkNote v-for="note in user.pinnedNotes" :key="note.id" class="note _panel" :note="note" :pinned="true" :setNote="true"/>
</div>
<MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo>
<template v-if="narrow">

View File

@ -4,11 +4,16 @@
*/
import { Note, UserLite, DriveFile } from "misskey-js/built/entities";
import { Ref, ref, ComputedRef, computed } from "vue";
import { Ref, ref, ComputedRef, computed, watch, unref } from "vue";
import { api } from "./api";
import { useStream } from '@/stream';
import { Stream } from "misskey-js";
import { $i } from "@/account";
import { defaultStore, noteViewInterruptors } from '@/store';
import { deepClone } from "./clone";
import { shouldCollapsed } from "./collapsed";
import { extractUrlFromMfm } from "./extract-url-from-mfm";
import * as mfm from 'mfm-js';
export class EntitiyManager<T extends { id: string }> {
private entities: Map<T['id'], Ref<T>>;
@ -40,6 +45,7 @@ export const driveFileManager = new EntitiyManager<DriveFile>('driveFile');
type OmittedNote = Omit<Note, 'user' | 'renote' | 'reply'>;
type CachedNoteSource = Ref<OmittedNote | null>;
type CachedNote = ComputedRef<Note | null>;
type InterruptedCachedNote = Ref<Note | null>;
/**
*
@ -61,6 +67,7 @@ export class NoteManager {
* 0
*/
private notesComputed: Map<Note['id'], CachedNote>;
private updatedAt: Map<Note['id'], number>;
private captureing: Map<Note['id'], number>;
private connection: Stream | null;
@ -147,6 +154,68 @@ export class NoteManager {
return this.notesComputed.get(id)!;
}
/**
* Interruptorを適用する
*
*/
public getInterrupted(id: string): {
interruptedNote: InterruptedCachedNote,
interruptorUnwatch: () => void,
executeInterruptor: () => Promise<void>,
} {
const note = this.get(id);
const interruptedNote = ref<Note | null>(unref(note));
async function executeInterruptor() {
if (note.value == null) {
interruptedNote.value = null;
return;
}
if (noteViewInterruptors.length > 0) {
interruptedNote.value = unref(note);
return;
}
let result = deepClone(note.value);
for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result) as Note;
}
interruptedNote.value = result;
}
const interruptorUnwatch = watch(note, executeInterruptor);
return {
interruptedNote,
interruptorUnwatch,
executeInterruptor,
};
}
/**
*
*/
public getNoteViewBase(id: string) {
const { interruptedNote: note, interruptorUnwatch, executeInterruptor } = this.getInterrupted(id);
const isRenote = computed(() => (
note.value != null &&
note.value.renote != null &&
note.value.text == null &&
note.value.fileIds?.length === 0 &&
note.value.poll == null
));
const isMyRenote = computed(() => $i && ($i.id === note.value?.userId));
const appearNote = computed(() => (isRenote.value ? note.value?.renote : note.value) ?? null);
return {
note, interruptorUnwatch, executeInterruptor,
isRenote, isMyRenote, appearNote,
urls: computed(() => appearNote.value?.text ? extractUrlFromMfm(mfm.parse(appearNote.value.text)) : null),
isLong: computed(() => appearNote.value ? shouldCollapsed(appearNote.value) : false),
canRenote: computed(() => (!!appearNote.value && !!$i) && (['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i.id)),
showTicker: computed(() => !!appearNote.value && ((defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance))),
};
}
public async fetch(id: string, force = false): Promise<CachedNote> {
if (!force) {
const updatedAt = this.updatedAt.get(id);