summaryrefslogtreecommitdiff
path: root/packages/client/src/components/MkNoteDetailed.vue
diff options
context:
space:
mode:
Diffstat (limited to 'packages/client/src/components/MkNoteDetailed.vue')
-rw-r--r--packages/client/src/components/MkNoteDetailed.vue677
1 files changed, 0 insertions, 677 deletions
diff --git a/packages/client/src/components/MkNoteDetailed.vue b/packages/client/src/components/MkNoteDetailed.vue
deleted file mode 100644
index 7ce8e039d9..0000000000
--- a/packages/client/src/components/MkNoteDetailed.vue
+++ /dev/null
@@ -1,677 +0,0 @@
-<template>
-<div
- v-if="!muted"
- v-show="!isDeleted"
- ref="el"
- v-hotkey="keymap"
- v-size="{ max: [500, 450, 350, 300] }"
- class="lxwezrsl _block"
- :tabindex="!isDeleted ? '-1' : null"
- :class="{ renote: isRenote }"
->
- <MkNoteSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/>
- <MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
- <div v-if="isRenote" class="renote">
- <MkAvatar class="avatar" :user="note.user"/>
- <i class="ti ti-repeat"></i>
- <I18n :src="i18n.ts.renotedBy" tag="span">
- <template #user>
- <MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
- <MkUserName :user="note.user"/>
- </MkA>
- </template>
- </I18n>
- <div class="info">
- <button ref="renoteTime" class="_button time" @click="showRenoteMenu()">
- <i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
- <MkTime :time="note.createdAt"/>
- </button>
- <MkVisibility :note="note"/>
- </div>
- </div>
- <article class="article" @contextmenu.stop="onContextmenu">
- <header class="header">
- <MkAvatar class="avatar" :user="appearNote.user" :show-indicator="true"/>
- <div class="body">
- <div class="top">
- <MkA v-user-preview="appearNote.user.id" class="name" :to="userPage(appearNote.user)">
- <MkUserName :nowrap="false" :user="appearNote.user"/>
- </MkA>
- <span v-if="appearNote.user.isBot" class="is-bot">bot</span>
- <div class="info">
- <MkVisibility :note="appearNote"/>
- </div>
- </div>
- <div class="username"><MkAcct :user="appearNote.user"/></div>
- <MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
- </div>
- </header>
- <div class="main">
- <div class="body">
- <p v-if="appearNote.cw != null" class="cw">
- <Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
- <XCwButton v-model="showContent" :note="appearNote"/>
- </p>
- <div v-show="appearNote.cw == null || showContent" class="content">
- <div class="text">
- <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
- <MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
- <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
- <a v-if="appearNote.renote != null" class="rp">RN:</a>
- <div v-if="translating || translation" class="translation">
- <MkLoading v-if="translating" mini/>
- <div v-else class="translated">
- <b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b>
- <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
- </div>
- </div>
- </div>
- <div v-if="appearNote.files.length > 0" class="files">
- <XMediaList :media-list="appearNote.files"/>
- </div>
- <XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
- <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/>
- <div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div>
- </div>
- <MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
- </div>
- <footer class="footer">
- <div class="info">
- <MkA class="created-at" :to="notePage(appearNote)">
- <MkTime :time="appearNote.createdAt" mode="detail"/>
- </MkA>
- </div>
- <XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
- <button class="button _button" @click="reply()">
- <i class="ti ti-arrow-back-up"></i>
- <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
- </button>
- <XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
- <button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()">
- <i class="ti ti-plus"></i>
- </button>
- <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
- <i class="ti ti-minus"></i>
- </button>
- <button ref="menuButton" class="button _button" @click="menu()">
- <i class="ti ti-dots"></i>
- </button>
- </footer>
- </div>
- </article>
- <MkNoteSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
-</div>
-<div v-else class="_panel muted" @click="muted = false">
- <I18n :src="i18n.ts.userSaysSomething" tag="small">
- <template #name>
- <MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
- <MkUserName :user="appearNote.user"/>
- </MkA>
- </template>
- </I18n>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
-import * as mfm from 'mfm-js';
-import * as misskey from 'misskey-js';
-import MkNoteSub from '@/components/MkNoteSub.vue';
-import XNoteSimple from '@/components/MkNoteSimple.vue';
-import XReactionsViewer from '@/components/MkReactionsViewer.vue';
-import XMediaList from '@/components/MkMediaList.vue';
-import XCwButton from '@/components/MkCwButton.vue';
-import XPoll from '@/components/MkPoll.vue';
-import XRenoteButton from '@/components/MkRenoteButton.vue';
-import MkUrlPreview from '@/components/MkUrlPreview.vue';
-import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
-import MkVisibility from '@/components/MkVisibility.vue';
-import { pleaseLogin } from '@/scripts/please-login';
-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 { reactionPicker } from '@/scripts/reaction-picker';
-import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
-import { $i } from '@/account';
-import { i18n } from '@/i18n';
-import { getNoteMenu } from '@/scripts/get-note-menu';
-import { useNoteCapture } from '@/scripts/use-note-capture';
-import { deepClone } from '@/scripts/clone';
-
-const props = defineProps<{
- note: misskey.entities.Note;
- pinned?: 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;
- });
-}
-
-const isRenote = (
- note.renote != null &&
- note.text == null &&
- note.fileIds.length === 0 &&
- note.poll == null
-);
-
-const el = ref<HTMLElement>();
-const menuButton = ref<HTMLElement>();
-const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
-const renoteTime = ref<HTMLElement>();
-const reactButton = ref<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 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 keymap = {
- 'r': () => reply(true),
- 'e|a|plus': () => react(true),
- 'q': () => renoteButton.value.renote(true),
- 'esc': blur,
- 'm|o': () => menu(true),
- 's': () => showContent.value !== showContent.value,
-};
-
-useNoteCapture({
- rootEl: el,
- note: $$(appearNote),
- isDeletedRef: isDeleted,
-});
-
-function reply(viaKeyboard = false): void {
- pleaseLogin();
- os.post({
- reply: appearNote,
- animation: !viaKeyboard,
- }, () => {
- focus();
- });
-}
-
-function react(viaKeyboard = false): void {
- pleaseLogin();
- blur();
- reactionPicker.show(reactButton.value, reaction => {
- os.api('notes/reactions/create', {
- noteId: appearNote.id,
- reaction: reaction,
- });
- }, () => {
- focus();
- });
-}
-
-function undoReact(note): void {
- const oldReaction = note.myReaction;
- if (!oldReaction) return;
- os.api('notes/reactions/delete', {
- noteId: note.id,
- });
-}
-
-function onContextmenu(ev: MouseEvent): void {
- const isLink = (el: HTMLElement) => {
- if (el.tagName === 'A') return true;
- if (el.parentElement) {
- return isLink(el.parentElement);
- }
- };
- if (isLink(ev.target)) 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);
- }
-}
-
-function menu(viaKeyboard = false): void {
- os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted }), menuButton.value, {
- viaKeyboard,
- }).then(focus);
-}
-
-function showRenoteMenu(viaKeyboard = false): void {
- if (!isMyRenote) return;
- os.popupMenu([{
- text: i18n.ts.unrenote,
- icon: 'ti ti-trash',
- danger: true,
- action: () => {
- os.api('notes/delete', {
- noteId: note.id,
- });
- isDeleted.value = true;
- },
- }], renoteTime.value, {
- viaKeyboard: viaKeyboard,
- });
-}
-
-function focus() {
- el.value.focus();
-}
-
-function blur() {
- el.value.blur();
-}
-
-os.api('notes/children', {
- noteId: appearNote.id,
- limit: 30,
-}).then(res => {
- replies.value = res;
-});
-
-if (appearNote.replyId) {
- os.api('notes/conversation', {
- noteId: appearNote.replyId,
- }).then(res => {
- conversation.value = res.reverse();
- });
-}
-</script>
-
-<style lang="scss" scoped>
-.lxwezrsl {
- position: relative;
- transition: box-shadow 0.1s ease;
- overflow: hidden;
- contain: content;
-
- &:focus-visible {
- outline: none;
-
- &:after {
- content: "";
- pointer-events: none;
- display: block;
- position: absolute;
- z-index: 10;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- margin: auto;
- width: calc(100% - 8px);
- height: calc(100% - 8px);
- border: dashed 1px var(--focus);
- border-radius: var(--radius);
- box-sizing: border-box;
- }
- }
-
- &:hover > .article > .main > .footer > .button {
- opacity: 1;
- }
-
- > .reply-to {
- opacity: 0.7;
- padding-bottom: 0;
- }
-
- > .reply-to-more {
- opacity: 0.7;
- }
-
- > .renote {
- display: flex;
- align-items: center;
- padding: 16px 32px 8px 32px;
- line-height: 28px;
- white-space: pre;
- color: var(--renote);
-
- > .avatar {
- flex-shrink: 0;
- display: inline-block;
- width: 28px;
- height: 28px;
- margin: 0 8px 0 0;
- border-radius: 6px;
- }
-
- > i {
- margin-right: 4px;
- }
-
- > span {
- overflow: hidden;
- flex-shrink: 1;
- text-overflow: ellipsis;
- white-space: nowrap;
-
- > .name {
- font-weight: bold;
- }
- }
-
- > .info {
- margin-left: auto;
- font-size: 0.9em;
-
- > .time {
- flex-shrink: 0;
- color: inherit;
-
- > .dropdownIcon {
- margin-right: 4px;
- }
- }
- }
- }
-
- > .renote + .article {
- padding-top: 8px;
- }
-
- > .article {
- padding: 32px;
- font-size: 1.2em;
-
- > .header {
- display: flex;
- position: relative;
- margin-bottom: 16px;
- align-items: center;
-
- > .avatar {
- display: block;
- flex-shrink: 0;
- width: 58px;
- height: 58px;
- }
-
- > .body {
- flex: 1;
- display: flex;
- flex-direction: column;
- justify-content: center;
- padding-left: 16px;
- font-size: 0.95em;
-
- > .top {
- > .name {
- font-weight: bold;
- line-height: 1.3;
- }
-
- > .is-bot {
- display: inline-block;
- margin: 0 0.5em;
- padding: 4px 6px;
- font-size: 80%;
- line-height: 1;
- border: solid 0.5px var(--divider);
- border-radius: 4px;
- }
-
- > .info {
- float: right;
- }
- }
-
- > .username {
- margin-bottom: 2px;
- line-height: 1.3;
- word-wrap: anywhere;
- }
- }
- }
-
- > .main {
- > .body {
- container-type: inline-size;
-
- > .cw {
- cursor: default;
- display: block;
- margin: 0;
- padding: 0;
- overflow-wrap: break-word;
-
- > .text {
- margin-right: 8px;
- }
- }
-
- > .content {
- > .text {
- overflow-wrap: break-word;
-
- > .reply {
- color: var(--accent);
- margin-right: 0.5em;
- }
-
- > .rp {
- margin-left: 4px;
- font-style: oblique;
- color: var(--renote);
- }
-
- > .translation {
- border: solid 0.5px var(--divider);
- border-radius: var(--radius);
- padding: 12px;
- margin-top: 8px;
- }
- }
-
- > .url-preview {
- margin-top: 8px;
- }
-
- > .poll {
- font-size: 80%;
- }
-
- > .renote {
- padding: 8px 0;
-
- > * {
- padding: 16px;
- border: dashed 1px var(--renote);
- border-radius: 8px;
- }
- }
- }
-
- > .channel {
- opacity: 0.7;
- font-size: 80%;
- }
- }
-
- > .footer {
- > .info {
- margin: 16px 0;
- opacity: 0.7;
- font-size: 0.9em;
- }
-
- > .button {
- margin: 0;
- padding: 8px;
- opacity: 0.7;
-
- &:not(:last-child) {
- margin-right: 28px;
- }
-
- &:hover {
- color: var(--fgHighlighted);
- }
-
- > .count {
- display: inline;
- margin: 0 0 0 8px;
- opacity: 0.7;
- }
-
- &.reacted {
- color: var(--accent);
- }
- }
- }
- }
- }
-
- > .reply {
- border-top: solid 0.5px var(--divider);
- }
-
- &.max-width_500px {
- font-size: 0.9em;
- }
-
- &.max-width_450px {
- > .renote {
- padding: 8px 16px 0 16px;
- }
-
- > .article {
- padding: 16px;
-
- > .header {
- > .avatar {
- width: 50px;
- height: 50px;
- }
- }
- }
- }
-
- &.max-width_350px {
- > .article {
- > .main {
- > .footer {
- > .button {
- &:not(:last-child) {
- margin-right: 18px;
- }
- }
- }
- }
- }
- }
-
- &.max-width_300px {
- font-size: 0.825em;
-
- > .article {
- > .header {
- > .avatar {
- width: 50px;
- height: 50px;
- }
- }
-
- > .main {
- > .footer {
- > .button {
- &:not(:last-child) {
- margin-right: 12px;
- }
- }
- }
- }
- }
- }
-}
-
-@container (max-width: 500px) {
- .lxwezrsl {
- font-size: 0.9em;
- }
-}
-
-@container (max-width: 450px) {
- .lxwezrsl {
- > .renote {
- padding: 8px 16px 0 16px;
- }
-
- > .article {
- padding: 16px;
-
- > .header {
- > .avatar {
- width: 50px;
- height: 50px;
- }
- }
- }
- }
-}
-
-@container (max-width: 350px) {
- .lxwezrsl {
- > .article {
- > .main {
- > .footer {
- > .button {
- &:not(:last-child) {
- margin-right: 18px;
- }
- }
- }
- }
- }
- }
-}
-
-@container (max-width: 300px) {
- .lxwezrsl {
- font-size: 0.825em;
-
- > .article {
- > .header {
- > .avatar {
- width: 50px;
- height: 50px;
- }
- }
-
- > .main {
- > .footer {
- > .button {
- &:not(:last-child) {
- margin-right: 12px;
- }
- }
- }
- }
- }
- }
-}
-
-.muted {
- padding: 8px;
- text-align: center;
- opacity: 0.7;
-}
-</style>