wip
This commit is contained in:
parent
b05f2bb834
commit
0c75d9a9f9
|
@ -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(() => {
|
||||
|
|
|
@ -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,
|
||||
limit: 30,
|
||||
}).then(res => {
|
||||
replies.value = res;
|
||||
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 => {
|
||||
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>
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue