summaryrefslogtreecommitdiff
path: root/packages/frontend-embed/src/components/EmNote.vue
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2024-09-29 11:42:24 +0000
committerGitHub <noreply@github.com>2024-09-29 11:42:24 +0000
commit5fc8b3bc5018a2cb553f114a570cc33ef6831163 (patch)
tree40edc874ae384548fd13e55fff6e317d1ef84fbb /packages/frontend-embed/src/components/EmNote.vue
parentMerge pull request #14391 from misskey-dev/develop (diff)
parentRelease: 2024.9.0 (diff)
downloadsharkey-5fc8b3bc5018a2cb553f114a570cc33ef6831163.tar.gz
sharkey-5fc8b3bc5018a2cb553f114a570cc33ef6831163.tar.bz2
sharkey-5fc8b3bc5018a2cb553f114a570cc33ef6831163.zip
Merge pull request #14580 from misskey-dev/develop
Release: 2024.9.0
Diffstat (limited to 'packages/frontend-embed/src/components/EmNote.vue')
-rw-r--r--packages/frontend-embed/src/components/EmNote.vue611
1 files changed, 611 insertions, 0 deletions
diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue
new file mode 100644
index 0000000000..f7899bfb03
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmNote.vue
@@ -0,0 +1,611 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+ v-show="!isDeleted"
+ ref="rootEl"
+ :class="[$style.root]"
+ :tabindex="isDeleted ? '-1' : '0'"
+>
+ <EmNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
+ <div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
+ <!--<div v-if="appearNote._prId_" class="tip"><i class="ti ti-speakerphone"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>-->
+ <!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>-->
+ <div v-if="isRenote" :class="$style.renote">
+ <div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
+ <EmAvatar :class="$style.renoteAvatar" :user="note.user" link/>
+ <i class="ti ti-repeat" style="margin-right: 4px;"></i>
+ <I18n :src="i18n.ts.renotedBy" tag="span" :class="$style.renoteText">
+ <template #user>
+ <EmA :class="$style.renoteUserName" :to="userPage(note.user)">
+ <EmUserName :user="note.user"/>
+ </EmA>
+ </template>
+ </I18n>
+ <div :class="$style.renoteInfo">
+ <button ref="renoteTime" :class="$style.renoteTime" class="_button">
+ <i class="ti ti-dots" :class="$style.renoteMenu"></i>
+ <EmTime :time="note.createdAt"/>
+ </button>
+ <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>
+ <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
+ </div>
+ </div>
+ <article :class="$style.article">
+ <div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
+ <EmAvatar :class="$style.avatar" :user="appearNote.user" link/>
+ <div :class="$style.main">
+ <EmNoteHeader :note="appearNote" :mini="true"/>
+ <EmInstanceTicker v-if="appearNote.user.instance != null" :instance="appearNote.user.instance"/>
+ <div style="container-type: inline-size;">
+ <p v-if="appearNote.cw != null" :class="$style.cw">
+ <EmMfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
+ <button style="display: block; width: 100%; margin: 4px 0;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button>
+ </p>
+ <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
+ <div :class="$style.text">
+ <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
+ <EmA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></EmA>
+ <EmMfm
+ v-if="appearNote.text"
+ :parsedNodes="parsed"
+ :text="appearNote.text"
+ :author="appearNote.user"
+ :nyaize="'respect'"
+ :emojiUrls="appearNote.emojis"
+ :enableEmojiMenu="!true"
+ :enableEmojiMenuReaction="true"
+ />
+ </div>
+ <div v-if="appearNote.files && appearNote.files.length > 0">
+ <EmMediaList :mediaList="appearNote.files" :originalEntityUrl="`${url}/notes/${appearNote.id}`"/>
+ </div>
+ <EmPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :readOnly="true" :class="$style.poll"/>
+ <div v-if="appearNote.renote" :class="$style.quote"><EmNoteSimple :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>
+ <EmA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</EmA>
+ </div>
+ <EmReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16">
+ <template #more>
+ <EmA :to="`/notes/${appearNote.id}/reactions`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</EmA>
+ </template>
+ </EmReactionsViewer>
+ <footer :class="$style.footer">
+ <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.footerButton, $style.footerButtonLink]" class="_button">
+ <i class="ti ti-arrow-back-up"></i>
+ </a>
+ <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.footerButton, $style.footerButtonLink]" class="_button">
+ <i class="ti ti-repeat"></i>
+ </a>
+ <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.footerButton, $style.footerButtonLink]" class="_button">
+ <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
+ <i v-else class="ti ti-plus"></i>
+ </a>
+ <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.footerButton, $style.footerButtonLink]" class="_button">
+ <i class="ti ti-dots"></i>
+ </a>
+ </footer>
+ </div>
+ </article>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, inject, ref, shallowRef } from 'vue';
+import * as mfm from 'mfm-js';
+import * as Misskey from 'misskey-js';
+import I18n from '@/components/I18n.vue';
+import EmNoteSub from '@/components/EmNoteSub.vue';
+import EmNoteHeader from '@/components/EmNoteHeader.vue';
+import EmNoteSimple from '@/components/EmNoteSimple.vue';
+import EmInstanceTicker from '@/components/EmInstanceTicker.vue';
+import EmReactionsViewer from '@/components/EmReactionsViewer.vue';
+import EmMediaList from '@/components/EmMediaList.vue';
+import EmPoll from '@/components/EmPoll.vue';
+import EmMfm from '@/components/EmMfm.js';
+import EmA from '@/components/EmA.vue';
+import EmAvatar from '@/components/EmAvatar.vue';
+import EmUserName from '@/components/EmUserName.vue';
+import EmTime from '@/components/EmTime.vue';
+import { userPage } from '@/utils.js';
+import { i18n } from '@/i18n.js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
+import { url } from '@@/js/config.js';
+
+function getAppearNote(note: Misskey.entities.Note) {
+ return Misskey.note.isPureRenote(note) ? note.renote : note;
+}
+
+const props = withDefaults(defineProps<{
+ note: Misskey.entities.Note;
+ pinned?: boolean;
+}>(), {
+});
+
+const emit = defineEmits<{
+ (ev: 'reaction', emoji: string): void;
+ (ev: 'removeReaction', emoji: string): void;
+}>();
+
+const inChannel = inject('inChannel', null);
+
+const note = ref((props.note));
+
+const isRenote = Misskey.note.isPureRenote(note.value);
+
+const rootEl = shallowRef<HTMLElement>();
+const renoteTime = shallowRef<HTMLElement>();
+const appearNote = computed(() => getAppearNote(note.value));
+const showContent = ref(false);
+const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
+const isLong = shouldCollapsed(appearNote.value, []);
+const collapsed = ref(appearNote.value.cw == null && isLong);
+const isDeleted = ref(false);
+</script>
+
+<style lang="scss" module>
+.root {
+ position: relative;
+ transition: box-shadow 0.1s ease;
+ font-size: 1.05em;
+ overflow: clip;
+ contain: content;
+
+ // これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
+ // 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
+ // ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
+ // 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
+ // 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?)
+ //content-visibility: auto;
+ //contain-intrinsic-size: 0 128px;
+
+ &: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 2px var(--focus);
+ border-radius: var(--radius);
+ box-sizing: border-box;
+ }
+ }
+
+ .footer {
+ position: relative;
+ z-index: 1;
+ }
+
+ &:hover > .article > .main > .footer > .footerButton {
+ opacity: 1;
+ }
+
+ &.showActionsOnlyHover {
+ .footer {
+ visibility: hidden;
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ padding: 0 4px;
+ margin-bottom: 0 !important;
+ background: var(--popup);
+ border-radius: 8px;
+ box-shadow: 0px 4px 32px var(--shadow);
+ }
+
+ .footerButton {
+ font-size: 90%;
+
+ &:not(:last-child) {
+ margin-right: 0;
+ }
+ }
+ }
+
+ &.showActionsOnlyHover:hover {
+ .footer {
+ visibility: visible;
+ }
+ }
+}
+
+.tip {
+ display: flex;
+ align-items: center;
+ padding: 16px 32px 8px 32px;
+ line-height: 24px;
+ font-size: 90%;
+ white-space: pre;
+ color: #d28a3f;
+}
+
+.tip + .article {
+ padding-top: 8px;
+}
+
+.replyTo {
+ opacity: 0.7;
+ padding-bottom: 0;
+}
+
+.renote {
+ position: relative;
+ display: flex;
+ align-items: center;
+ padding: 16px 32px 8px 32px;
+ line-height: 28px;
+ white-space: pre;
+ color: var(--renote);
+
+ & + .article {
+ padding-top: 8px;
+ }
+
+ > .colorBar {
+ height: calc(100% - 6px);
+ }
+}
+
+.renoteAvatar {
+ flex-shrink: 0;
+ display: inline-block;
+ width: 28px;
+ height: 28px;
+ margin: 0 8px 0 0;
+}
+
+.renoteText {
+ overflow: hidden;
+ flex-shrink: 1;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.renoteUserName {
+ font-weight: bold;
+}
+
+.renoteInfo {
+ margin-left: auto;
+ font-size: 0.9em;
+}
+
+.renoteTime {
+ flex-shrink: 0;
+ color: inherit;
+}
+
+.renoteMenu {
+ margin-right: 4px;
+}
+
+.collapsedRenoteTarget {
+ display: flex;
+ align-items: center;
+ line-height: 28px;
+ white-space: pre;
+ padding: 0 32px 18px;
+}
+
+.collapsedRenoteTargetAvatar {
+ flex-shrink: 0;
+ display: inline-block;
+ width: 28px;
+ height: 28px;
+ margin: 0 8px 0 0;
+}
+
+.collapsedRenoteTargetText {
+ overflow: hidden;
+ flex-shrink: 1;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 90%;
+ opacity: 0.7;
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+.article {
+ position: relative;
+ display: flex;
+ padding: 28px 32px;
+}
+
+.colorBar {
+ position: absolute;
+ top: 8px;
+ left: 8px;
+ width: 5px;
+ height: calc(100% - 16px);
+ border-radius: 999px;
+ pointer-events: none;
+}
+
+.avatar {
+ flex-shrink: 0;
+ display: block !important;
+ margin: 0 14px 0 0;
+ width: 58px;
+ height: 58px;
+ position: sticky !important;
+ top: calc(22px + var(--stickyTop, 0px));
+ left: 0;
+}
+
+.main {
+ flex: 1;
+ min-width: 0;
+}
+
+.cw {
+ cursor: default;
+ display: block;
+ margin: 0;
+ padding: 0;
+ overflow-wrap: break-word;
+}
+
+.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), color(from var(--panel) srgb r g b / 0));
+
+ &: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%);
+}
+
+.text {
+ overflow-wrap: break-word;
+}
+
+.replyIcon {
+ color: var(--accent);
+ margin-right: 0.5em;
+}
+
+.translation {
+ border: solid 0.5px var(--divider);
+ border-radius: var(--radius);
+ padding: 12px;
+ margin-top: 8px;
+}
+
+.urlPreview {
+ margin-top: 8px;
+}
+
+.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%;
+}
+
+.footer {
+ margin-bottom: -14px;
+}
+
+.footerButton {
+ 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;
+}
+
+.footerButtonCount {
+ display: inline;
+ margin: 0 0 0 8px;
+ opacity: 0.7;
+}
+
+@container (max-width: 580px) {
+ .root {
+ font-size: 0.95em;
+ }
+
+ .renote {
+ padding: 12px 26px 0 26px;
+ }
+
+ .article {
+ padding: 24px 26px;
+ }
+
+ .avatar {
+ width: 50px;
+ height: 50px;
+ }
+}
+
+@container (max-width: 500px) {
+ .root {
+ font-size: 0.9em;
+ }
+
+ .renote {
+ padding: 10px 22px 0 22px;
+ }
+
+ .article {
+ padding: 20px 22px;
+ }
+
+ .footer {
+ margin-bottom: -8px;
+ }
+}
+
+@container (max-width: 480px) {
+ .renote {
+ padding: 8px 16px 0 16px;
+ }
+
+ .tip {
+ padding: 8px 16px 0 16px;
+ }
+
+ .collapsedRenoteTarget {
+ padding: 0 16px 9px;
+ margin-top: 4px;
+ }
+
+ .article {
+ padding: 14px 16px;
+ }
+}
+
+@container (max-width: 450px) {
+ .avatar {
+ margin: 0 10px 0 0;
+ width: 46px;
+ height: 46px;
+ top: calc(14px + var(--stickyTop, 0px));
+ }
+}
+
+@container (max-width: 400px) {
+ .root:not(.showActionsOnlyHover) {
+ .footerButton {
+ &:not(:last-child) {
+ margin-right: 18px;
+ }
+ }
+ }
+}
+
+@container (max-width: 350px) {
+ .root:not(.showActionsOnlyHover) {
+ .footerButton {
+ &:not(:last-child) {
+ margin-right: 12px;
+ }
+ }
+ }
+
+ .colorBar {
+ top: 6px;
+ left: 6px;
+ width: 4px;
+ height: calc(100% - 12px);
+ }
+}
+
+@container (max-width: 300px) {
+ .avatar {
+ width: 44px;
+ height: 44px;
+ }
+
+ .root:not(.showActionsOnlyHover) {
+ .footerButton {
+ &:not(:last-child) {
+ margin-right: 8px;
+ }
+ }
+ }
+}
+
+@container (max-width: 250px) {
+ .quoteNote {
+ padding: 12px;
+ }
+}
+
+.reactionOmitted {
+ display: inline-block;
+ margin-left: 8px;
+ opacity: .8;
+ font-size: 95%;
+}
+</style>