From b3dd1a4f09dc7d68c312749ff494794623329baa Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sat, 6 Jul 2024 22:58:56 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF=E3=81=AE?= =?UTF-8?q?=E5=88=86=E5=B2=90=E3=81=8C=E5=A4=9A=E3=81=8B=E3=81=A3=E3=81=9F?= =?UTF-8?q?MkNoteDetailed=E3=82=92=E5=88=86=E9=9B=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/MkNoteDetailed.vue | 246 ++------- .../frontend/src/components/MkNoteEmbed.vue | 495 ++++++++++++++++++ packages/frontend/src/pages/embed/note.vue | 4 +- 3 files changed, 549 insertions(+), 196 deletions(-) create mode 100644 packages/frontend/src/components/MkNoteEmbed.vue diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index d852aca799..bc1f416373 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-hotkey="keymap" :class="$style.root" > - <div v-if="!inEmbedPage && appearNote.reply && appearNote.reply.replyId"> + <div v-if="appearNote.reply && appearNote.reply.replyId"> <div v-if="!conversationLoaded" style="padding: 16px"> <MkButton style="margin: 0 auto;" primary rounded @click="loadConversation">{{ i18n.ts.loadConversation }}</MkButton> </div> @@ -43,38 +43,29 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> </div> </div> - <article :class="[$style.note, { [$style.embeddedNote]: inEmbedPage }]" @contextmenu.stop="onContextmenu"> + <article :class="$style.note" @contextmenu.stop="onContextmenu"> <header :class="$style.noteHeader"> <MkAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview/> <div :class="$style.noteHeaderBody"> - <div :class="$style.noteHeaderBodyUpper"> - <div style="min-width: 0;"> - <div class="_nowrap"> - <MkA v-user-preview="appearNote.user.id" :class="$style.noteHeaderName" :to="userPage(appearNote.user)"> - <MkUserName :nowrap="inEmbedPage" :user="appearNote.user"/> - </MkA> - <span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span> - </div> - <div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div> - </div> + <div> + <MkA v-user-preview="appearNote.user.id" :class="$style.noteHeaderName" :to="userPage(appearNote.user)"> + <MkUserName :nowrap="false" :user="appearNote.user"/> + </MkA> + <span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span> <div :class="$style.noteHeaderInfo"> - <a v-if="inEmbedPage" :href="url" :class="$style.noteHeaderInstanceIconLink" target="_blank" rel="noopener noreferrer"> - <img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.noteHeaderInstanceIcon"/> - </a> - <template v-else> - <div v-if="appearNote.visibility !== 'public'" :title="i18n.ts._visibility[appearNote.visibility]"> - <i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i> - <i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i> - <i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> - </div> - <div v-if="appearNote.localOnly" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></div> - </template> + <span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]"> + <i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i> + <i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i> + <i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> + </span> + <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> </div> </div> + <div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div> <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> </div> </header> - <div :class="[$style.noteContent, { [$style.contentCollapsed]: inEmbedPage && collapsed }]"> + <div :class="$style.noteContent"> <p v-if="appearNote.cw != null" :class="$style.cw"> <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/> @@ -101,101 +92,61 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div v-if="appearNote.files && appearNote.files.length > 0"> - <MkMediaList :mediaList="appearNote.files" :originalEntityUrl="`${url}/notes/${appearNote.id}`"/> + <MkMediaList :mediaList="appearNote.files"/> </div> <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/> - <div v-if="isEnabledUrlPreview && !inEmbedPage"> + <div v-if="isEnabledUrlPreview"> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/> </div> <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div> - <button v-if="inEmbedPage && isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false"> - <span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span> - </button> - <button v-else-if="inEmbedPage && isLong && !collapsed" :class="$style.showLess" class="_button" @click="collapsed = true"> - <span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span> - </button> </div> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> </div> <footer> <div :class="$style.noteFooterInfo"> - <template v-if="inEmbedPage"> - <span v-if="appearNote.visibility !== 'public'" style="display: inline-block; margin-right: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]"> - <i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i> - <i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i> - <i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> - </span> - <span v-if="appearNote.localOnly" style="display: inline-block; margin-right: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> - </template> <MkA :to="notePage(appearNote)"> <MkTime :time="appearNote.createdAt" mode="detail" colored/> </MkA> </div> - <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :maxNumber="inEmbedPage ? 16 : undefined" :note="appearNote"> - <template #more> - <MkA :to="`/notes/${appearNote.id}`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</MkA> - </template> - </MkReactionsViewer> - <template v-if="inEmbedPage"> - <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button"> - <i class="ti ti-arrow-back-up"></i> - </a> - <a v-if="canRenote" :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button"> - <i class="ti ti-repeat"></i> - <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p> - </a> - <a v-else :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button" disabled> - <i class="ti ti-ban"></i> - </a> - <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button"> - <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> - <i v-else class="ti ti-plus"></i> - <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> - </a> - <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button"> - <i class="ti ti-dots"></i> - </a> - </template> - <template v-else> - <button class="_button" :class="$style.noteFooterButton" @click="reply()"> - <i class="ti ti-arrow-back-up"></i> - <p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p> - </button> - <button - v-if="canRenote" - ref="renoteButton" - class="_button" - :class="$style.noteFooterButton" - @mousedown="renote()" - > - <i class="ti ti-repeat"></i> - <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p> - </button> - <button v-else class="_button" :class="$style.noteFooterButton" disabled> - <i class="ti ti-ban"></i> - </button> - <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()"> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> - <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> - <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> - <i v-else class="ti ti-plus"></i> - <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> - </button> - <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()"> - <i class="ti ti-paperclip"></i> - </button> - <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="showMenu()"> - <i class="ti ti-dots"></i> - </button> - </template> + <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/> + <button class="_button" :class="$style.noteFooterButton" @click="reply()"> + <i class="ti ti-arrow-back-up"></i> + <p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p> + </button> + <button + v-if="canRenote" + ref="renoteButton" + class="_button" + :class="$style.noteFooterButton" + @mousedown="renote()" + > + <i class="ti ti-repeat"></i> + <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p> + </button> + <button v-else class="_button" :class="$style.noteFooterButton" disabled> + <i class="ti ti-ban"></i> + </button> + <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()"> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> + <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> + <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> + <i v-else class="ti ti-plus"></i> + <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> + </button> + <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()"> + <i class="ti ti-paperclip"></i> + </button> + <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="showMenu()"> + <i class="ti ti-dots"></i> + </button> </footer> </article> - <div v-if="!inEmbedPage" :class="$style.tabs"> + <div :class="$style.tabs"> <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button> <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button> <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button> </div> - <div v-if="!inEmbedPage"> + <div> <div v-if="tab === 'replies'"> <div v-if="!repliesLoaded" style="padding: 16px"> <MkButton style="margin: 0 auto;" primary rounded @click="loadReplies">{{ i18n.ts.loadReplies }}</MkButton> @@ -277,13 +228,11 @@ import { useTooltip } from '@/scripts/use-tooltip.js'; import { claimAchievement } from '@/scripts/achievements.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; -import { shouldCollapsed } from '@/scripts/collapsed.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkPagination, { type Paging } from '@/components/MkPagination.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; -import { isEnabledUrlPreview, instance } from '@/instance.js'; -import { url } from '@/config.js'; +import { isEnabledUrlPreview } from '@/instance.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -293,7 +242,6 @@ const props = withDefaults(defineProps<{ }); const inChannel = inject('inChannel', null); -const inEmbedPage = inject<boolean>('EMBED_PAGE', false); const note = ref(deepClone(props.note)); @@ -340,8 +288,6 @@ const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null); const translating = ref(false); const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null; -const isLong = shouldCollapsed(appearNote.value, urls ?? []); -const collapsed = ref(appearNote.value.cw == null && isLong); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); const conversation = ref<Misskey.entities.Note[]>([]); const replies = ref<Misskey.entities.Note[]>([]); @@ -656,10 +602,6 @@ function loadConversation() { padding: 32px; font-size: 1.2em; - &.embeddedNote { - padding: 24px 32px 16px; - } - &:hover > .main > .footer > .button { opacity: 1; } @@ -679,26 +621,15 @@ function loadConversation() { height: 58px; } -.embeddedNote .noteHeaderAvatar { - width: 50px; - height: 50px; -} - .noteHeaderBody { flex: 1; display: flex; - min-width: 0; flex-direction: column; justify-content: center; padding-left: 16px; font-size: 0.95em; } -.noteHeaderBodyUpper { - display: flex; - min-width: 0; -} - .noteHeaderName { font-weight: bold; line-height: 1.3; @@ -715,21 +646,7 @@ function loadConversation() { } .noteHeaderInfo { - margin-left: auto; - display: flex; - gap: 0.5em; - align-items: center; -} - -.noteHeaderInstanceIconLink { - display: inline-block; - margin-left: 4px; -} - -.noteHeaderInstanceIcon { - width: 32px; - height: 32px; - border-radius: 4px; + float: right; } .noteHeaderUsername { @@ -789,52 +706,6 @@ function loadConversation() { font-size: 80%; } -.showLess { - width: 100%; - margin-top: 14px; - position: sticky; - bottom: calc(var(--stickyBottom, 0px) + 14px); -} - -.showLessLabel { - display: inline-block; - background: var(--popup); - padding: 6px 10px; - font-size: 0.8em; - border-radius: 999px; - box-shadow: 0 2px 6px rgb(0 0 0 / 20%); -} - -.contentCollapsed { - position: relative; - max-height: 9em; - overflow: clip; -} - -.collapsed { - display: block; - position: absolute; - bottom: 0; - left: 0; - z-index: 2; - width: 100%; - height: 64px; - background: linear-gradient(0deg, var(--panel), var(--X15)); - - &:hover > .collapsedLabel { - background: var(--panelHighlight); - } -} - -.collapsedLabel { - display: inline-block; - background: var(--panel); - padding: 6px 10px; - font-size: 0.8em; - border-radius: 999px; - box-shadow: 0 2px 6px rgb(0 0 0 / 20%); -} - .noteFooterInfo { margin: 16px 0; opacity: 0.7; @@ -855,12 +726,6 @@ function loadConversation() { } } -.footerButtonLink:hover, -.footerButtonLink:focus, -.footerButtonLink:active { - text-decoration: none; -} - .noteFooterButtonCount { display: inline; margin: 0 0 0 8px; @@ -968,11 +833,4 @@ function loadConversation() { text-align: center; opacity: 0.7; } - -.reactionOmitted { - display: inline-block; - margin-left: 8px; - opacity: .8; - font-size: 95%; -} </style> diff --git a/packages/frontend/src/components/MkNoteEmbed.vue b/packages/frontend/src/components/MkNoteEmbed.vue new file mode 100644 index 0000000000..7a5d17520b --- /dev/null +++ b/packages/frontend/src/components/MkNoteEmbed.vue @@ -0,0 +1,495 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + v-show="!isDeleted" + ref="rootEl" + :class="$style.root" +> + <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/> + <i class="ti ti-repeat" style="margin-right: 4px;"></i> + <span :class="$style.renoteText"> + <I18n :src="i18n.ts.renotedBy" tag="span"> + <template #user> + <MkA v-user-preview="note.userId" :class="$style.renoteName" :to="userPage(note.user)"> + <MkUserName :user="note.user"/> + </MkA> + </template> + </I18n> + </span> + <div :class="$style.renoteInfo"> + <div class="$style.renoteTime"> + <MkTime :time="note.createdAt"/> + </div> + <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> + <i v-if="note.visibility === 'home'" class="ti ti-home"></i> + <i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i> + <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> + </span> + <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> + </div> + </div> + <article :class="$style.note"> + <header :class="$style.noteHeader"> + <MkAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview/> + <div :class="$style.noteHeaderBody"> + <div :class="$style.noteHeaderBodyUpper"> + <div style="min-width: 0;"> + <div class="_nowrap"> + <MkA v-user-preview="appearNote.user.id" :class="$style.noteHeaderName" :to="userPage(appearNote.user)"> + <MkUserName :nowrap="true" :user="appearNote.user"/> + </MkA> + <span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span> + </div> + <div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div> + </div> + <div :class="$style.noteHeaderInfo"> + <a :href="url" :class="$style.noteHeaderInstanceIconLink" target="_blank" rel="noopener noreferrer"> + <img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.noteHeaderInstanceIcon"/> + </a> + </div> + </div> + <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> + </div> + </header> + <div :class="[$style.noteContent, { [$style.contentCollapsed]: collapsed }]"> + <p v-if="appearNote.cw != null" :class="$style.cw"> + <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> + <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/> + </p> + <div v-show="appearNote.cw == null || showContent"> + <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> + <MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> + <Mfm + v-if="appearNote.text" + :parsedNodes="parsed" + :text="appearNote.text" + :author="appearNote.user" + :nyaize="'respect'" + :emojiUrls="appearNote.emojis" + :enableEmojiMenu="true" + :enableEmojiMenuReaction="true" + /> + <a v-if="appearNote.renote != null" :class="$style.rn">RN:</a> + <div v-if="appearNote.files && appearNote.files.length > 0"> + <MkMediaList :mediaList="appearNote.files" :originalEntityUrl="`${url}/notes/${appearNote.id}`"/> + </div> + <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/> + <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div> + <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false"> + <span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span> + </button> + <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click="collapsed = true"> + <span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span> + </button> + </div> + <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> + </div> + <footer> + <div :class="$style.noteFooterInfo"> + <span v-if="appearNote.visibility !== 'public'" style="display: inline-block; margin-right: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]"> + <i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i> + <i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i> + <i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> + </span> + <span v-if="appearNote.localOnly" style="display: inline-block; margin-right: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> + <MkA :to="notePage(appearNote)"> + <MkTime :time="appearNote.createdAt" mode="detail" colored/> + </MkA> + </div> + <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :maxNumber="16" :note="appearNote"> + <template #more> + <MkA :to="`/notes/${appearNote.id}`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</MkA> + </template> + </MkReactionsViewer> + <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button"> + <i class="ti ti-arrow-back-up"></i> + </a> + <a v-if="canRenote" :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button"> + <i class="ti ti-repeat"></i> + <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p> + </a> + <a v-else :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button" disabled> + <i class="ti ti-ban"></i> + </a> + <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button"> + <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> + <i v-else class="ti ti-plus"></i> + <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> + </a> + <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button"> + <i class="ti ti-dots"></i> + </a> + </footer> + </article> +</div> +</template> + +<script lang="ts" setup> +import { computed, inject, ref } from 'vue'; +import * as mfm from 'mfm-js'; +import * as Misskey from 'misskey-js'; +import MkNoteSub from '@/components/MkNoteSub.vue'; +import MkNoteSimple from '@/components/MkNoteSimple.vue'; +import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; +import MkMediaList from '@/components/MkMediaList.vue'; +import MkCwButton from '@/components/MkCwButton.vue'; +import MkPoll from '@/components/MkPoll.vue'; +import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; +import { userPage } from '@/filters/user.js'; +import { notePage } from '@/filters/note.js'; +import number from '@/filters/number.js'; +import { defaultStore } from '@/store.js'; +import { $i } from '@/account.js'; +import { i18n } from '@/i18n.js'; +import { deepClone } from '@/scripts/clone.js'; +import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; +import { shouldCollapsed } from '@/scripts/collapsed.js'; +import { instance } from '@/instance.js'; +import { url } from '@/config.js'; + +const props = defineProps<{ + note: Misskey.entities.Note; +}>(); + +const inChannel = inject('inChannel', null); + +const note = ref(deepClone(props.note)); + +const isRenote = ( + note.value.renote != null && + note.value.reply == null && + note.value.text == null && + note.value.cw == null && + note.value.fileIds && note.value.fileIds.length === 0 && + note.value.poll == null +); + +const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); +const showContent = ref(false); +const isDeleted = ref(false); +const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; +const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null; +const isLong = shouldCollapsed(appearNote.value, urls ?? []); +const collapsed = ref(appearNote.value.cw == null && isLong); +const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); +const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id); +</script> + +<style lang="scss" module> +.root { + position: relative; + transition: box-shadow 0.1s ease; + overflow: clip; + contain: content; +} + +.replyTo { + opacity: 0.7; + padding-bottom: 0; +} + +.renote { + display: flex; + align-items: center; + padding: 16px 32px 8px 32px; + line-height: 28px; + white-space: pre; + color: var(--renote); +} + +.renoteAvatar { + flex-shrink: 0; + display: inline-block; + width: 28px; + height: 28px; + margin: 0 8px 0 0; + border-radius: 6px; +} + +.renoteText { + overflow: hidden; + flex-shrink: 1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.renoteName { + font-weight: bold; +} + +.renoteInfo { + margin-left: auto; + font-size: 0.9em; +} + +.renoteTime { + flex-shrink: 0; + color: inherit; +} + +.renote + .note { + padding-top: 8px; +} + +.note { + padding: 24px 32px 16px; + font-size: 1.2em; + + &:hover > .main > .footer > .button { + opacity: 1; + } +} + +.noteHeader { + display: flex; + position: relative; + margin-bottom: 16px; + align-items: center; +} + +.noteHeaderAvatar { + display: block; + flex-shrink: 0; + width: 50px; + height: 50px; +} + +.noteHeaderBody { + flex: 1; + display: flex; + min-width: 0; + flex-direction: column; + justify-content: center; + padding-left: 16px; + font-size: 0.95em; +} + +.noteHeaderBodyUpper { + display: flex; + min-width: 0; +} + +.noteHeaderName { + font-weight: bold; + line-height: 1.3; +} + +.isBot { + 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; +} + +.noteHeaderInfo { + margin-left: auto; + display: flex; + gap: 0.5em; + align-items: center; +} + +.noteHeaderInstanceIconLink { + display: inline-block; + margin-left: 4px; +} + +.noteHeaderInstanceIcon { + width: 32px; + height: 32px; + border-radius: 4px; +} + +.noteHeaderUsername { + margin-bottom: 2px; + line-height: 1.3; + word-wrap: anywhere; +} + +.noteContent { + container-type: inline-size; + overflow-wrap: break-word; +} + +.cw { + cursor: default; + display: block; + margin: 0; + padding: 0; + overflow-wrap: break-word; +} + +.noteReplyTarget { + color: var(--accent); + margin-right: 0.5em; +} + +.rn { + margin-left: 4px; + font-style: oblique; + color: var(--renote); +} + +.reactionOmitted { + display: inline-block; + margin-left: 8px; + opacity: .8; + font-size: 95%; +} + +.poll { + font-size: 80%; +} + +.quote { + padding: 8px 0; +} + +.quoteNote { + padding: 16px; + border: dashed 1px var(--renote); + border-radius: 8px; + overflow: clip; +} + +.channel { + opacity: 0.7; + font-size: 80%; +} + +.showLess { + width: 100%; + margin-top: 14px; + position: sticky; + bottom: calc(var(--stickyBottom, 0px) + 14px); +} + +.showLessLabel { + display: inline-block; + background: var(--popup); + padding: 6px 10px; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 2px 6px rgb(0 0 0 / 20%); +} + +.contentCollapsed { + position: relative; + max-height: 9em; + overflow: clip; +} + +.collapsed { + display: block; + position: absolute; + bottom: 0; + left: 0; + z-index: 2; + width: 100%; + height: 64px; + background: linear-gradient(0deg, var(--panel), var(--X15)); + + &:hover > .collapsedLabel { + background: var(--panelHighlight); + } +} + +.collapsedLabel { + display: inline-block; + background: var(--panel); + padding: 6px 10px; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 2px 6px rgb(0 0 0 / 20%); +} + +.noteFooterInfo { + margin: 16px 0; + opacity: 0.7; + font-size: 0.9em; +} + +.noteFooterButton { + margin: 0; + padding: 8px; + opacity: 0.7; + + &:not(:last-child) { + margin-right: 28px; + } + + &:hover { + color: var(--fgHighlighted); + } +} + +.footerButtonLink:hover, +.footerButtonLink:focus, +.footerButtonLink:active { + text-decoration: none; +} + +.noteFooterButtonCount { + display: inline; + margin: 0 0 0 8px; + opacity: 0.7; + + &.reacted { + color: var(--accent); + } +} + +@container (max-width: 500px) { + .root { + font-size: 0.9em; + } +} + +@container (max-width: 450px) { + .renote { + padding: 8px 16px 0 16px; + } + + .note { + padding: 16px; + } + + .noteHeaderAvatar { + width: 50px; + height: 50px; + } +} + +@container (max-width: 350px) { + .noteFooterButton { + &:not(:last-child) { + margin-right: 18px; + } + } +} + +@container (max-width: 300px) { + .root { + font-size: 0.825em; + } + + .noteHeaderAvatar { + width: 50px; + height: 50px; + } + + .noteFooterButton { + &:not(:last-child) { + margin-right: 12px; + } + } +} +</style> diff --git a/packages/frontend/src/pages/embed/note.vue b/packages/frontend/src/pages/embed/note.vue index b86a8ec951..c87f6757cc 100644 --- a/packages/frontend/src/pages/embed/note.vue +++ b/packages/frontend/src/pages/embed/note.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.noteEmbedRoot"> <MkLoading v-if="loading"/> - <MkNoteDetailed v-else-if="note" :note="note"/> + <MkNoteEmbed v-else-if="note" :note="note"/> <XNotFound v-else/> </div> </template> @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script setup lang="ts"> import { ref, provide, inject, onActivated } from 'vue'; import * as Misskey from 'misskey-js'; -import MkNoteDetailed from '@/components/MkNoteDetailed.vue'; +import MkNoteEmbed from '@/components/MkNoteEmbed.vue'; import XNotFound from '@/pages/not-found.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { useRouter } from '@/router/supplier.js';