Notes refactoring (?) + new CW design (#9888)

Moved a lot of the duplicated code in the different note components into the SubNoteContent component

I've also replaced the detailed note stuff with just the MkNote component

Co-authored-by: Freeplay <Freeplay@duck.com>
Reviewed-on: https://codeberg.org/calckey/calckey/pulls/9888
Co-authored-by: Free <freeplay@duck.com>
Co-committed-by: Free <freeplay@duck.com>
This commit is contained in:
Free 2023-04-23 00:21:38 +00:00 committed by Kainoa Kanter
parent 4974088061
commit 29818a067b
6 changed files with 340 additions and 868 deletions

View file

@ -1,8 +1,14 @@
<template> <template>
<button class="nrvgflfu _button" @click.stop="toggle"> <button
<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b> class="_button"
<span v-if="!modelValue">{{ label }}</span> :class="{showLess: modelValue, fade: !modelValue}"
@click.stop="toggle"
>
<span>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}
<span v-if="!modelValue">{{ label }}</span>
</span>
</button> </button>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -30,7 +36,8 @@ const label = computed(() => {
? [i18n.t("_cw.files", { count: props.note.files.length })] ? [i18n.t("_cw.files", { count: props.note.files.length })]
: [], : [],
props.note.poll != null ? [i18n.ts.poll] : [], props.note.poll != null ? [i18n.ts.poll] : [],
] as string[][]).join(" / "); props.note.renote != null ? [i18n.ts.quoteAttached] : [],
] as string[][]).join(", ");
}); });
const toggle = () => { const toggle = () => {
@ -39,37 +46,25 @@ const toggle = () => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.nrvgflfu { ._button {
position: relative; font-weight: 700;
z-index: 2;
display: inline-block;
padding: 4px 8px;
font-size: 0.8em;
color: var(--cwFg);
background: var(--cwBg);
padding: 6px 10px;
width: 90%;
border-radius: 10px;
border: 1px solid var(--divider);
margin-top: 10px;
margin-bottom: 10px;
transition: background-color 0.25s ease-in-out;
&:hover {
background: var(--cwFg);
color: var(--cwBg);
}
> span { > span {
margin-left: 4px; background: var(--cwBg) !important;
color: var(--cwFg);
&:before { transition: background .2s, color .2s;
content: "("; > span {
} font-weight: 500;
&::before {
&:after { content: "("
content: ")"; }
&::after {
content: ")"
}
} }
} }
&:hover > span {
background: var(--cwFg) !important;
color: var(--cwBg) !important;
}
} }
</style> </style>

View file

@ -10,11 +10,11 @@
:class="{ renote: isRenote }" :class="{ renote: isRenote }"
> >
<MkNoteSub <MkNoteSub
v-if="appearNote.reply" v-if="appearNote.reply && !detailedView"
:note="appearNote.reply" :note="appearNote.reply"
class="reply-to" class="reply-to"
/> />
<div class="note-context" @click="noteClick"> <div v-if="!detailedView" class="note-context" @click="noteClick">
<div class="line"></div> <div class="line"></div>
<div v-if="appearNote._prId_" class="info"> <div v-if="appearNote._prId_" class="info">
<i class="ph-megaphone-simple-bold ph-lg"></i> <i class="ph-megaphone-simple-bold ph-lg"></i>
@ -77,93 +77,34 @@
/> />
</div> </div>
<div class="body"> <div class="body">
<p v-if="appearNote.cw != null" class="cw"> <MkSubNoteContent
<Mfm class="text"
v-if="appearNote.cw != ''" :note="appearNote"
class="text" :detailed="true"
:text="appearNote.cw" :detailedView="detailedView"
:author="appearNote.user" :parentId="appearNote.parentId"
:custom-emojis="appearNote.emojis" @push="(e) => router.push(notePage(e))"
:i="$i" ></MkSubNoteContent>
/>
<br />
<XCwButton v-model="showContent" :note="appearNote" />
</p>
<div <div
v-show="appearNote.cw == null || showContent" v-if="translating || translation"
class="content" class="translation"
:class="{ collapsed, isLong }"
> >
<div class="text"> <MkLoading v-if="translating" mini />
<div v-else class="translated">
<b
>{{
i18n.t("translatedFrom", {
x: translation.sourceLang,
})
}}:
</b>
<Mfm <Mfm
v-if="appearNote.text" :text="translation.text"
:text="appearNote.text"
:author="appearNote.user" :author="appearNote.user"
:i="$i" :i="$i"
:custom-emojis="appearNote.emojis" :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
>{{
i18n.t("translatedFrom", {
x: translation.sourceLang,
})
}}:
</b>
<Mfm
:text="translation.text"
:author="appearNote.user"
:i="$i"
:custom-emojis="appearNote.emojis"
/>
</div>
</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="false"
class="url-preview"
/>
<div v-if="appearNote.renote" class="renote">
<XNoteSimple
:note="appearNote.renote"
@click.stop="
router.push(notePage(appearNote.renote))
"
/>
</div>
<button
v-if="isLong && collapsed"
class="fade _button"
@click.stop="collapsed = false"
>
<span>{{ i18n.ts.showMore }}</span>
</button>
<button
v-else-if="isLong && !collapsed"
class="showLess _button"
@click.stop="collapsed = true"
>
<span>{{ i18n.ts.showLess }}</span>
</button>
</div> </div>
<MkA <MkA
v-if="appearNote.channel && !inChannel" v-if="appearNote.channel && !inChannel"
@ -174,6 +115,14 @@
{{ appearNote.channel.name }}</MkA {{ appearNote.channel.name }}</MkA
> >
</div> </div>
<div v-if="detailedView" class="info">
<MkA class="created-at" :to="notePage(appearNote)">
<MkTime
:time="appearNote.createdAt"
mode="absolute"
/>
</MkA>
</div>
<footer ref="el" class="footer" @click.stop> <footer ref="el" class="footer" @click.stop>
<XReactionsViewer <XReactionsViewer
v-if="enableEmojiReactions" v-if="enableEmojiReactions"
@ -277,6 +226,7 @@ import * as mfm from "mfm-js";
import type { Ref } from "vue"; import type { Ref } from "vue";
import type * as misskey from "calckey-js"; import type * as misskey from "calckey-js";
import MkNoteSub from "@/components/MkNoteSub.vue"; import MkNoteSub from "@/components/MkNoteSub.vue";
import MkSubNoteContent from "./MkSubNoteContent.vue";
import XNoteHeader from "@/components/MkNoteHeader.vue"; import XNoteHeader from "@/components/MkNoteHeader.vue";
import XNoteSimple from "@/components/MkNoteSimple.vue"; import XNoteSimple from "@/components/MkNoteSimple.vue";
import XMediaList from "@/components/MkMediaList.vue"; import XMediaList from "@/components/MkMediaList.vue";
@ -297,7 +247,6 @@ import { userPage } from "@/filters/user";
import * as os from "@/os"; import * as os from "@/os";
import { defaultStore, noteViewInterruptors } from "@/store"; import { defaultStore, noteViewInterruptors } from "@/store";
import { reactionPicker } from "@/scripts/reaction-picker"; import { reactionPicker } from "@/scripts/reaction-picker";
import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
import { $i } from "@/account"; import { $i } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { getNoteMenu } from "@/scripts/get-note-menu"; import { getNoteMenu } from "@/scripts/get-note-menu";
@ -310,6 +259,7 @@ const router = useRouter();
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
pinned?: boolean; pinned?: boolean;
detailedView?: boolean;
}>(); }>();
const inChannel = inject("inChannel", null); const inChannel = inject("inChannel", null);
@ -344,18 +294,10 @@ let appearNote = $computed(() =>
); );
const isMyRenote = $i && $i.id === note.userId; const isMyRenote = $i && $i.id === note.userId;
const showContent = ref(false); const showContent = ref(false);
const isLong =
appearNote.cw == null &&
appearNote.text != null &&
(appearNote.text.split("\n").length > 9 || appearNote.text.length > 500);
const collapsed = ref(appearNote.cw == null && isLong);
const isDeleted = ref(false); const isDeleted = ref(false);
const muted = ref(getWordMute(appearNote, $i, defaultStore.state.mutedWords)); const muted = ref(getWordMute(appearNote, $i, defaultStore.state.mutedWords));
const translation = ref(null); const translation = ref(null);
const translating = ref(false); const translating = ref(false);
const urls = appearNote.text
? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5)
: null;
const enableEmojiReactions = defaultStore.state.enableEmojiReactions; const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
const keymap = { const keymap = {
@ -503,7 +445,7 @@ function focusAfter() {
} }
function noteClick(e) { function noteClick(e) {
if (document.getSelection().type === "Range") { if (document.getSelection().type === "Range" || props.detailedView) {
e.stopPropagation(); e.stopPropagation();
} else { } else {
router.push(notePage(appearNote)); router.push(notePage(appearNote));
@ -690,123 +632,24 @@ function readPromo() {
> .body { > .body {
margin-top: 0.7em; margin-top: 0.7em;
> .cw {
cursor: default;
display: block;
margin: 0;
padding: 0;
overflow-wrap: break-word;
> .text {
margin-right: 8px;
}
}
> .content { > .content {
&.isLong { > .translation {
> .showLess { border: solid 0.5px var(--divider);
width: 100%; border-radius: var(--radius);
margin-top: 1em; padding: 12px;
position: sticky;
bottom: var(--stickyBottom);
> span {
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%);
}
}
}
&.collapsed {
position: relative;
max-height: 9em;
overflow: hidden;
> .text {
max-height: 9em;
mask: linear-gradient(
black calc(100% - 64px),
transparent
);
-webkit-mask: linear-gradient(
black calc(100% - 64px),
transparent
);
}
> .fade {
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
> span {
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%);
}
&:hover {
> span {
background: var(--panelHighlight);
}
}
}
}
> .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;
}
}
> .files {
margin-top: 0.4em;
margin-bottom: 0.4em;
}
> .url-preview {
margin-top: 8px; margin-top: 8px;
} }
}
> .poll { > .renote {
font-size: 80%; padding-top: 8px;
} > * {
padding: 16px;
> .renote { border: solid 1px var(--renote);
padding: 8px 0; border-radius: 8px;
transition: background 0.2s;
> * { &:hover,
padding: 16px; &:focus-within {
border: solid 1px var(--renote); background-color: var(--panelHighlight);
border-radius: 8px;
transition: background 0.2s;
&:hover,
&:focus-within {
background-color: var(--panelHighlight);
}
} }
} }
} }
@ -816,13 +659,18 @@ function readPromo() {
font-size: 80%; font-size: 80%;
} }
} }
> .info {
margin-block: 16px;
opacity: 0.7;
font-size: 0.9em;
}
> .footer { > .footer {
position: relative; position: relative;
z-index: 2; z-index: 2;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
pointer-events: none; // Allow clicking anything w/out pointer-events: all; to open post pointer-events: none; // Allow clicking anything w/out pointer-events: all; to open post
margin-top: .4em;
> .button { > .button {
margin: 0; margin: 0;
padding: 8px; padding: 8px;

View file

@ -20,246 +20,16 @@
:note="appearNote.reply" :note="appearNote.reply"
class="reply-to" class="reply-to"
/> />
<div v-if="isRenote" class="renote">
<MkAvatar class="avatar" :user="note.user" /> <div ref="noteEl" class="article" tabindex="-1">
<i class="ph-repeat ph-bold ph-lg"></i> <MkNote
<I18n :src="i18n.ts.renotedBy" tag="span"> @contextmenu.stop="onContextmenu"
<template #user> tabindex="-1"
<MkA :note="appearNote"
v-user-preview="note.userId" :detailedView="true"
class="name" ></MkNote>
: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="ph-dots-three-outline ph-bold ph-lg dropdownIcon"
></i>
<MkTime :time="note.createdAt" />
</button>
<MkVisibility :note="note" />
</div>
</div> </div>
<article
ref="noteEl"
class="article"
@contextmenu.stop="onContextmenu"
tabindex="-1"
>
<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 :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">
<div 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"
/>
<br />
<XCwButton v-model="showContent" :note="appearNote" />
</div>
<div
v-show="appearNote.cw == null || showContent"
class="content"
>
<div class="text">
<Mfm
v-if="appearNote.text"
:text="appearNote.text"
:author="appearNote.user"
:i="$i"
:custom-emojis="appearNote.emojis"
/>
<div
v-if="translating || translation"
class="translation"
>
<MkLoading v-if="translating" mini />
<div v-else class="translated">
<b
>{{
i18n.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"
@click.stop="
router.push(notePage(appearNote.renote))
"
/>
</div>
</div>
<MkA
v-if="appearNote.channel && !inChannel"
class="channel"
:to="`/channels/${appearNote.channel.id}`"
><i class="ph-television ph-bold ph-lg"></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
v-if="enableEmojiReactions"
ref="reactionsViewer"
:note="appearNote"
/>
<button
v-tooltip.noDelay.bottom="i18n.ts.reply"
class="button _button"
@click="reply()"
>
<template v-if="appearNote.reply"
><i class="ph-arrow-u-up-left ph-bold ph-lg"></i
></template>
<template v-else
><i class="ph-arrow-bend-up-left ph-bold ph-lg"></i
></template>
<p v-if="appearNote.repliesCount > 0" class="count">
{{ appearNote.repliesCount }}
</p>
</button>
<XRenoteButton
ref="renoteButton"
class="button"
:note="appearNote"
:count="appearNote.renoteCount"
/>
<XStarButtonNoEmoji
v-if="!enableEmojiReactions"
class="button"
:note="appearNote"
:count="
Object.values(appearNote.reactions).reduce(
(partialSum, val) => partialSum + val,
0
)
"
:reacted="appearNote.myReaction != null"
/>
<XStarButton
v-if="
enableEmojiReactions &&
appearNote.myReaction == null
"
ref="starButton"
class="button"
:note="appearNote"
/>
<button
v-if="
enableEmojiReactions &&
appearNote.myReaction == null
"
ref="reactButton"
v-tooltip.noDelay.bottom="i18n.ts.reaction"
class="button _button"
@click="react()"
>
<i class="ph-smiley ph-bold ph-lg"></i>
</button>
<button
v-if="
enableEmojiReactions &&
appearNote.myReaction != null
"
ref="reactButton"
class="button _button reacted"
@click="undoReact(appearNote)"
>
<i class="ph-minus ph-bold ph-lg"></i>
</button>
<XQuoteButton class="button" :note="appearNote" />
<button
ref="menuButton"
v-tooltip.noDelay.bottom="i18n.ts.more"
class="button _button"
@click="menu()"
>
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
</button>
</footer>
</div>
</article>
<MkNoteSub <MkNoteSub
v-for="note in directReplies" v-for="note in directReplies"
:key="note.id" :key="note.id"
@ -298,6 +68,7 @@ import {
} from "vue"; } from "vue";
import * as mfm from "mfm-js"; import * as mfm from "mfm-js";
import type * as misskey from "calckey-js"; import type * as misskey from "calckey-js";
import MkNote from "@/components/MkNote.vue";
import MkNoteSub from "@/components/MkNoteSub.vue"; import MkNoteSub from "@/components/MkNoteSub.vue";
import XNoteSimple from "@/components/MkNoteSimple.vue"; import XNoteSimple from "@/components/MkNoteSimple.vue";
import XReactionsViewer from "@/components/MkReactionsViewer.vue"; import XReactionsViewer from "@/components/MkReactionsViewer.vue";
@ -672,8 +443,7 @@ onUnmounted(() => {
} }
> .article { > .article {
padding: 32px; padding-block: 28px 6px;
padding-bottom: 6px;
&:last-child { &:last-child {
padding-bottom: 24px; padding-bottom: 24px;
} }
@ -681,151 +451,8 @@ onUnmounted(() => {
overflow: clip; overflow: clip;
outline: none; outline: none;
scroll-margin-top: calc(var(--stickyTop) + 20vh); scroll-margin-top: calc(var(--stickyTop) + 20vh);
> .header { :deep(.article) {
display: flex; cursor: unset;
position: relative;
margin-bottom: 16px;
> .avatar {
display: block;
flex-shrink: 0;
width: var(--avatarSize);
height: var(--avatarSize);
}
> .body {
width: 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 14px;
font-size: 0.95em;
> .top {
display: flex;
align-items: center;
> .name {
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
}
> .is-bot {
flex-shrink: 0;
align-self: center;
margin: 0 0.5em;
padding: 4px 6px;
font-size: 80%;
border: solid 0.5px var(--divider);
border-radius: 4px;
}
> .info {
float: right;
}
}
}
}
> .main {
> .body {
> .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: solid 1px var(--renote);
border-radius: 8px;
transition: background 0.2s;
&:hover,
&:focus-within {
background-color: var(--panelHighlight);
}
}
}
}
> .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: 16px;
}
&:hover {
color: var(--fgHighlighted);
}
> .count {
display: inline;
margin: 0 0 0 8px;
opacity: 0.7;
}
&.reacted {
color: var(--accent);
}
}
}
} }
} }
@ -909,7 +536,7 @@ onUnmounted(() => {
} }
> .article { > .article {
padding: 16px; padding: 6px 0 0 0;
> .header > .body { > .header > .body {
padding-left: 10px; padding-left: 10px;
} }

View file

@ -4,21 +4,7 @@
<div class="main"> <div class="main">
<XNoteHeader class="header" :note="note" :mini="true" /> <XNoteHeader class="header" :note="note" :mini="true" />
<div class="body"> <div class="body">
<p v-if="note.cw != null" class="cw"> <MkSubNoteContent class="text" :note="note" />
<Mfm
v-if="note.cw != ''"
class="text"
:text="note.cw"
:author="note.user"
:i="$i"
:custom-emojis="note.emojis"
/>
<br />
<XCwButton v-model="showContent" :note="note" />
</p>
<div v-show="note.cw == null || showContent" class="content">
<MkSubNoteContent class="text" :note="note" />
</div>
</div> </div>
</div> </div>
</div> </div>
@ -29,7 +15,6 @@ import {} from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import XNoteHeader from "@/components/MkNoteHeader.vue"; import XNoteHeader from "@/components/MkNoteHeader.vue";
import MkSubNoteContent from "@/components/MkSubNoteContent.vue"; import MkSubNoteContent from "@/components/MkSubNoteContent.vue";
import XCwButton from "@/components/MkCwButton.vue";
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
@ -79,28 +64,6 @@ const showContent = $ref(false);
> .header { > .header {
margin-bottom: 2px; margin-bottom: 2px;
} }
> .body {
> .cw {
cursor: default;
display: block;
margin: 0;
padding: 0;
overflow-wrap: break-word;
> .text {
margin-right: 8px;
}
}
> .content {
> .text {
cursor: default;
margin: 0;
padding: 0;
}
}
}
} }
} }
</style> </style>

View file

@ -21,51 +21,12 @@
<div class="body"> <div class="body">
<XNoteHeader class="header" :note="note" :mini="true" /> <XNoteHeader class="header" :note="note" :mini="true" />
<div class="body"> <div class="body">
<p v-if="appearNote.cw != null" class="cw"> <MkSubNoteContent
<MkA class="text"
v-if="appearNote.replyId" :note="note"
:to="`/notes/${appearNote.replyId}`" :parentId="appearNote.parentId"
class="reply-icon" :conversation="conversation"
@click.stop />
>
<i class="ph-arrow-bend-left-up ph-bold ph-lg"></i>
</MkA>
<MkA
v-if="
conversation &&
appearNote.renoteId &&
appearNote.renoteId != parentId &&
!appearNote.replyId
"
:to="`/notes/${appearNote.renoteId}`"
class="reply-icon"
@click.stop
>
<i class="ph-quotes ph-bold ph-lg"></i>
</MkA>
<Mfm
v-if="appearNote.cw != ''"
class="text"
:text="appearNote.cw"
:author="appearNote.user"
:i="$i"
:custom-emojis="appearNote.emojis"
/>
<br />
<XCwButton v-model="showContent" :note="note" />
</p>
<div
v-show="appearNote.cw == null || showContent"
class="content"
>
<MkSubNoteContent
class="text"
:note="note"
:detailed="true"
:parentId="appearNote.parentId"
:conversation="conversation"
/>
</div>
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini /> <MkLoading v-if="translating" mini />
<div v-else class="translated"> <div v-else class="translated">
@ -212,7 +173,6 @@ import XStarButton from "@/components/MkStarButton.vue";
import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue"; import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue";
import XRenoteButton from "@/components/MkRenoteButton.vue"; import XRenoteButton from "@/components/MkRenoteButton.vue";
import XQuoteButton from "@/components/MkQuoteButton.vue"; import XQuoteButton from "@/components/MkQuoteButton.vue";
import XCwButton from "@/components/MkCwButton.vue";
import { pleaseLogin } from "@/scripts/please-login"; import { pleaseLogin } from "@/scripts/please-login";
import { getNoteMenu } from "@/scripts/get-note-menu"; import { getNoteMenu } from "@/scripts/get-note-menu";
import { notePage } from "@/filters/note"; import { notePage } from "@/filters/note";
@ -262,7 +222,6 @@ let appearNote = $computed(() =>
const isDeleted = ref(false); const isDeleted = ref(false);
const translation = ref(null); const translation = ref(null);
const translating = ref(false); const translating = ref(false);
let showContent = $ref(false);
const replies: misskey.entities.Note[] = const replies: misskey.entities.Note[] =
props.conversation props.conversation
?.filter( ?.filter(
@ -400,35 +359,6 @@ function noteClick(e) {
} }
> .body { > .body {
.reply-icon {
display: inline-block;
border-radius: 6px;
padding: 0.2em 0.2em;
margin-right: 0.2em;
color: var(--accent);
transition: background 0.2s;
&:hover,
&:focus {
background: var(--buttonHoverBg);
}
}
> .cw {
cursor: default;
display: block;
margin: 0;
padding: 0;
overflow-wrap: break-word;
> .text {
margin-right: 8px;
}
}
> .content {
> .text {
margin: 0;
padding: 0;
}
}
> .translation { > .translation {
border: solid 0.5px var(--divider); border: solid 0.5px var(--divider);
border-radius: var(--radius); border-radius: var(--radius);

View file

@ -1,77 +1,110 @@
<template> <template>
<div class="wrmlmaau" :class="{ collapsed, isLong }"> <p v-if="note.cw != null" class="cw">
<div class="body"> <MkA
<span v-if="note.deletedAt" style="opacity: 0.5" v-if="!detailed && note.replyId"
>({{ i18n.ts.deleted }})</span :to="`/notes/${note.replyId}`"
> class="reply-icon"
<template v-if="!note.cw"> @click.stop
<MkA
v-if="note.replyId"
:to="`/notes/${note.replyId}`"
class="reply-icon"
@click.stop
>
<i class="ph-arrow-bend-left-up ph-bold ph-lg"></i>
</MkA>
<MkA
v-if="
conversation &&
note.renoteId &&
note.renoteId != parentId &&
!note.replyId
"
:to="`/notes/${note.renoteId}`"
class="reply-icon"
@click.stop
>
<i class="ph-quotes ph-bold ph-lg"></i>
</MkA>
</template>
<Mfm
v-if="note.text"
:text="note.text"
:author="note.user"
:i="$i"
:custom-emojis="note.emojis"
/>
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`"
>{{ i18n.ts.quoteAttached }}: ...</MkA
>
</div>
<div v-if="note.files.length > 0">
<XMediaList :media-list="note.files" />
</div>
<div v-if="note.poll">
<summary>{{ i18n.ts.poll }}</summary>
<XPoll :note="note" />
</div>
<template v-if="detailed">
<!-- <div v-if="note.renoteId" class="renote">
<XNoteSimple :note="note.renote"/>
</div> -->
<MkUrlPreview
v-for="url in urls"
:key="url"
:url="url"
:compact="true"
:detail="false"
class="url-preview"
/>
</template>
<button
v-if="isLong && collapsed"
class="fade _button"
@click.stop="collapsed = false"
> >
<span>{{ i18n.ts.showMore }}</span> <i class="ph-arrow-bend-left-up ph-bold ph-lg"></i>
</button> </MkA>
<button <MkA
v-if="isLong && !collapsed" v-if="
class="showLess _button" conversation &&
@click.stop="collapsed = true" note.renoteId &&
note.renoteId != parentId &&
!note.replyId
"
:to="`/notes/${note.renoteId}`"
class="reply-icon"
@click.stop
> >
<span>{{ i18n.ts.showLess }}</span> <i class="ph-quotes ph-bold ph-lg"></i>
</button> </MkA>
<Mfm
v-if="note.cw != ''"
class="text"
:text="note.cw"
:author="note.user"
:i="$i"
:custom-emojis="note.emojis"
/>
</p>
<div
class="wrmlmaau"
>
<div class="content" :class="{ collapsed, isLong, showContent: note.cw && !showContent }">
<div class="body">
<span v-if="note.deletedAt" style="opacity: 0.5"
>({{ i18n.ts.deleted }})</span
>
<template v-if="!note.cw">
<MkA
v-if="!detailed && note.replyId"
:to="`/notes/${note.replyId}`"
class="reply-icon"
@click.stop
>
<i class="ph-arrow-bend-left-up ph-bold ph-lg"></i>
</MkA>
<MkA
v-if="
conversation &&
note.renoteId &&
note.renoteId != parentId &&
!note.replyId
"
:to="`/notes/${note.renoteId}`"
class="reply-icon"
@click.stop
>
<i class="ph-quotes ph-bold ph-lg"></i>
</MkA>
</template>
<Mfm
v-if="note.text"
:text="note.text"
:author="note.user"
:i="$i"
:custom-emojis="note.emojis"
/>
<MkA v-if="!detailed && note.renoteId" class="rp" :to="`/notes/${note.renoteId}`"
>{{ i18n.ts.quoteAttached }}: ...</MkA
>
<div v-if="note.files.length > 0" class="files">
<XMediaList :media-list="note.files" />
</div>
<XPoll v-if="note.poll" :note="note" class="poll"/>
<template v-if="detailed">
<MkUrlPreview
v-for="url in urls"
:key="url"
:url="url"
:compact="true"
:detail="false"
class="url-preview"
/>
<div v-if="note.renote" class="renote" @click.stop="emit('push', note.renote)">
<XNoteSimple :note="note.renote"/>
</div>
</template>
</div>
<button
v-if="isLong && collapsed"
class="fade _button"
@click.stop="collapsed = false"
>
<span>{{ i18n.ts.showMore }}</span>
</button>
<button
v-if="isLong && !collapsed"
class="showLess _button"
@click.stop="collapsed = true"
>
<span>{{ i18n.ts.showLess }}</span>
</button>
<XCwButton v-if="note.cw" v-model="showContent" :note="note" />
</div>
</div> </div>
</template> </template>
@ -83,6 +116,7 @@ import XNoteSimple from "@/components/MkNoteSimple.vue";
import XMediaList from "@/components/MkMediaList.vue"; import XMediaList from "@/components/MkMediaList.vue";
import XPoll from "@/components/MkPoll.vue"; import XPoll from "@/components/MkPoll.vue";
import MkUrlPreview from "@/components/MkUrlPreview.vue"; import MkUrlPreview from "@/components/MkUrlPreview.vue";
import XCwButton from "@/components/MkCwButton.vue";
import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm"; import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
@ -91,82 +125,157 @@ const props = defineProps<{
parentId?; parentId?;
conversation?; conversation?;
detailed?: boolean; detailed?: boolean;
detailedView?: boolean;
}>(); }>();
const isLong = const emit = defineEmits<{
props.note.cw == null && (ev: "push", v): void;
props.note.text != null && }>();
(props.note.text.split("\n").length > 9 || props.note.text.length > 500);
const isLong = !props.detailedView && (
props.note.cw == null &&
props.note.text != null &&
(props.note.text.split("\n").length > 9 || props.note.text.length > 500)
);
const collapsed = $ref(props.note.cw == null && isLong); const collapsed = $ref(props.note.cw == null && isLong);
const urls = props.note.text const urls = props.note.text
? extractUrlFromMfm(mfm.parse(props.note.text)) ? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5)
: null; : null;
let showContent = $ref(false);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.wrmlmaau { .reply-icon {
display: inline-block;
border-radius: 6px;
padding: 0.2em 0.2em;
margin-right: 0.2em;
color: var(--accent);
transition: background 0.2s;
&:hover, &:focus {
background: var(--buttonHoverBg);
}
}
.cw {
cursor: default;
display: block;
margin: 0;
padding: 0;
margin-bottom: 10px;
overflow-wrap: break-word; overflow-wrap: break-word;
> .text {
> .body { margin-right: 8px;
> .rp {
margin-left: 4px;
font-style: oblique;
color: var(--renote);
}
.reply-icon {
display: inline-block;
border-radius: 6px;
padding: 0.2em 0.2em;
margin-right: 0.2em;
color: var(--accent);
transition: background 0.2s;
&:hover,
&:focus {
background: var(--buttonHoverBg);
}
}
} }
}
> .mk-url-preview { .wrmlmaau {
margin-top: 8px; .content {
} overflow-wrap: break-word;
&.collapsed {
position: relative;
max-height: 9em;
overflow: hidden;
> .body { > .body {
max-height: 9em; transition: filter .1s;
mask: linear-gradient(black calc(100% - 64px), transparent); > .rp {
-webkit-mask: linear-gradient(black calc(100% - 64px), transparent); margin-left: 4px;
} font-style: oblique;
> .fade { color: var(--renote);
display: block; }
position: absolute; .reply-icon {
bottom: 0;
left: 0;
width: 100%;
height: 64px;
> span {
display: inline-block; display: inline-block;
background: var(--panel); border-radius: 6px;
padding: 6px 10px; padding: 0.2em 0.2em;
font-size: 0.8em; margin-right: 0.2em;
border-radius: 999px; color: var(--accent);
box-shadow: 0 2px 6px rgb(0 0 0 / 20%); transition: background 0.2s;
&:hover,
&:focus {
background: var(--buttonHoverBg);
}
}
> .files {
margin-top: 0.4em;
margin-bottom: 0.4em;
}
> .url-preview {
margin-top: 8px;
} }
&:hover { > .poll {
font-size: 80%;
}
> .renote {
padding-top: 8px;
> * {
padding: 16px;
border: solid 1px var(--renote);
border-radius: 8px;
transition: background 0.2s;
&:hover,
&:focus-within {
background-color: var(--panelHighlight);
}
}
}
}
&.collapsed, &.showContent {
position: relative;
max-height: calc(9em + 50px);
> .body {
max-height: inherit;
mask: linear-gradient(black calc(100% - 64px), transparent);
-webkit-mask: linear-gradient(black calc(100% - 64px), transparent);
padding-inline: 50px;
margin-inline: -50px;
margin-top: -50px;
padding-top: 50px;
overflow: hidden;
}
&.collapsed > .body {
box-sizing: border-box;
}
&.showContent {
> .body {
min-height: 2em;
max-height: 5em;
filter: blur(4px);
}
:deep(.fade) {
inset: 0;
top: 40px;
}
:deep(span) {
animation: none !important;
}
}
:deep(.fade) {
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
> span { > span {
background: var(--panelHighlight); display: inline-block;
background: var(--panel);
padding: .4em 1em;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
&:hover {
> span {
background: var(--panelHighlight);
}
} }
} }
} }
}
&.isLong { :deep(.showLess) {
> .showLess {
width: 100%; width: 100%;
margin-top: 1em; margin-top: 1em;
position: sticky; position: sticky;