wip
This commit is contained in:
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// https://vitejs.dev/config/build-options.html#build-modulepreload
|
||||
import 'vite/modulepreload-polyfill';
|
||||
|
||||
import '@/style.scss';
|
||||
import '@/embed/style.scss';
|
||||
import { createApp, defineAsyncComponent } from 'vue';
|
||||
import type { CommonBootOptions } from '@/boot/common.js';
|
||||
import { common } from '@/boot/common.js';
|
||||
import { setIframeId, postMessageToParentWindow } from '@/scripts/post-message.js';
|
||||
import { parseEmbedParams } from '@/scripts/embed-page.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { createEmbedRouter } from '@/embed/router.js';
|
||||
|
||||
const bootOptions: Partial<CommonBootOptions> = {
|
||||
routerFactory: createEmbedRouter,
|
||||
};
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const embedParams = parseEmbedParams(params);
|
||||
|
||||
// カラーモードのオーバーライド
|
||||
if (embedParams.colorMode != null) {
|
||||
bootOptions.forceColorMode = embedParams.colorMode;
|
||||
}
|
||||
|
||||
// サイズの制限
|
||||
document.documentElement.style.maxWidth = '500px';
|
||||
|
||||
// サーバー起動の場合はもとから付与されているけど一応
|
||||
document.documentElement.classList.add('embed');
|
||||
|
||||
// 外部タブでのstoreの変更の影響を受けないように
|
||||
defaultStore.setConfig({
|
||||
disableMessageChannel: true,
|
||||
});
|
||||
|
||||
// iframeIdの設定
|
||||
function setIframeIdHandler(event: MessageEvent) {
|
||||
if (event.data?.type === 'misskey:embedParent:registerIframeId' && event.data.payload?.iframeId != null) {
|
||||
setIframeId(event.data.payload.iframeId);
|
||||
window.removeEventListener('message', setIframeIdHandler);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', setIframeIdHandler);
|
||||
|
||||
// 起動
|
||||
common(() => createApp(
|
||||
defineAsyncComponent(() => import('@/embed/ui.vue')),
|
||||
), bootOptions).then(async ({ app }) => {
|
||||
//#region Embed Provide
|
||||
app.provide('EMBED_PAGE', true);
|
||||
app.provide('embedParams', embedParams);
|
||||
//#endregion
|
||||
|
||||
//#region defaultStoreを書き換え
|
||||
await defaultStore.ready;
|
||||
|
||||
defaultStore.set('sound_notUseSound', true);
|
||||
//#endregion
|
||||
|
||||
//#region Embed Link Behavior
|
||||
//強制的に新しいタブで開く
|
||||
const router = useRouter();
|
||||
router.navHook = (path, flag): boolean => {
|
||||
window.open(path, '_blank', 'noopener');
|
||||
return true;
|
||||
};
|
||||
//#endregion
|
||||
|
||||
// 起動完了を通知(このあとクライアント側から misskey:embedParent:registerIframeId が送信される)
|
||||
postMessageToParentWindow('misskey:embed:ready');
|
||||
});
|
@@ -1,55 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<a :href="href" target="_blank" :class="$style.root">
|
||||
<div :class="$style.label">
|
||||
<template v-if="media.type.startsWith('audio')"><i class="ti ti-music"></i> {{ i18n.ts.audio }}</template>
|
||||
<template v-else><i class="ti ti-file"></i> {{ i18n.ts.file }}</template>
|
||||
</div>
|
||||
<div :class="$style.go">
|
||||
<i class="ti ti-chevron-right"></i>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
defineProps<{
|
||||
media: Misskey.entities.DriveFile;
|
||||
href: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: var(--margin);
|
||||
margin-top: 4px;
|
||||
border: 1px solid var(--inputBorder);
|
||||
border-radius: var(--radius);
|
||||
background-color: var(--panel);
|
||||
transition: background-color .1s, border-color .1s;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
border-color: var(--inputBorderHover);
|
||||
background-color: var(--buttonHoverBg);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.go {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
@@ -1,154 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[hide ? $style.hidden : $style.visible]" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick">
|
||||
<a
|
||||
:title="image.name"
|
||||
:class="$style.imageContainer"
|
||||
:href="href ?? image.url"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<ImgWithBlurhash
|
||||
:hash="image.blurhash"
|
||||
:src="hide ? null : url"
|
||||
:forceBlurhash="hide"
|
||||
:cover="hide || cover"
|
||||
:alt="image.comment || image.name"
|
||||
:title="image.comment || image.name"
|
||||
:width="image.properties.width"
|
||||
:height="image.properties.height"
|
||||
:style="hide ? 'filter: brightness(0.7);' : null"
|
||||
/>
|
||||
</a>
|
||||
<template v-if="hide">
|
||||
<div :class="$style.hiddenText">
|
||||
<div :class="$style.hiddenTextWrapper">
|
||||
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</b>
|
||||
<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ i18n.ts.image }}</b>
|
||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div :class="$style.indicators">
|
||||
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
|
||||
<div v-if="image.comment" :class="$style.indicator">ALT</div>
|
||||
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
|
||||
</div>
|
||||
<i v-if="!hide" class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
image: Misskey.entities.DriveFile;
|
||||
href?: string;
|
||||
raw?: boolean;
|
||||
cover?: boolean;
|
||||
}>(), {
|
||||
cover: false,
|
||||
});
|
||||
|
||||
const hide = ref(props.image.isSensitive);
|
||||
const darkMode = ref<boolean>(false); // TODO
|
||||
|
||||
const url = computed(() => (props.raw)
|
||||
? props.image.url
|
||||
: props.image.thumbnailUrl,
|
||||
);
|
||||
|
||||
async function onclick(ev: MouseEvent) {
|
||||
if (hide.value) {
|
||||
ev.stopPropagation();
|
||||
hide.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.hidden {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hiddenText {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
background-color: var(--fg);
|
||||
color: var(--accentLighten);
|
||||
font-size: 12px;
|
||||
opacity: .5;
|
||||
padding: 5px 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.hiddenTextWrapper {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.visible {
|
||||
position: relative;
|
||||
//box-shadow: 0 0 0 1px var(--divider) inset;
|
||||
background: var(--bg);
|
||||
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.indicators {
|
||||
display: inline-flex;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
pointer-events: none;
|
||||
opacity: .5;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
/* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */
|
||||
background-color: black;
|
||||
border-radius: 6px;
|
||||
color: var(--accentLighten);
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
font-size: 0.8em;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
</style>
|
@@ -1,146 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :class="$style.banner">
|
||||
<XBanner :media="media" :href="originalEntityUrl"/>
|
||||
</div>
|
||||
<div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container">
|
||||
<div
|
||||
:class="[
|
||||
$style.medias,
|
||||
count === 1 ? [$style.n1] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany,
|
||||
]"
|
||||
>
|
||||
<div v-for="media in mediaList.filter(media => previewable(media))" :class="$style.media">
|
||||
<XVideo v-if="media.type.startsWith('video')" :key="`video:${media.id}`" :class="$style.mediaInner" :video="media" :href="originalEntityUrl"/>
|
||||
<XImage v-else-if="media.type.startsWith('image')" :key="`image:${media.id}`" :class="$style.mediaInner" class="image" :image="media" :raw="raw" :href="originalEntityUrl"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XBanner from './EmMediaBanner.vue';
|
||||
import XImage from './EmMediaImage.vue';
|
||||
import XVideo from './EmMediaVideo.vue';
|
||||
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||
|
||||
const props = defineProps<{
|
||||
mediaList: Misskey.entities.DriveFile[];
|
||||
raw?: boolean;
|
||||
|
||||
/** 埋め込みページ用 親要素の正規URL */
|
||||
originalEntityUrl: string;
|
||||
}>();
|
||||
|
||||
const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
|
||||
|
||||
const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
||||
if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue
|
||||
// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
|
||||
return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.medias {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&.n1 {
|
||||
grid-template-rows: 1fr;
|
||||
|
||||
// default but fallback (expand)
|
||||
min-height: 64px;
|
||||
max-height: clamp(
|
||||
64px,
|
||||
50cqh,
|
||||
min(360px, 50vh)
|
||||
);
|
||||
|
||||
&.n116_9 {
|
||||
min-height: initial;
|
||||
max-height: initial;
|
||||
aspect-ratio: 16 / 9; // fallback
|
||||
}
|
||||
|
||||
&.n11_1{
|
||||
min-height: initial;
|
||||
max-height: initial;
|
||||
aspect-ratio: 1 / 1; // fallback
|
||||
}
|
||||
|
||||
&.n12_3 {
|
||||
min-height: initial;
|
||||
max-height: initial;
|
||||
aspect-ratio: 2 / 3; // fallback
|
||||
}
|
||||
}
|
||||
|
||||
&.n2 {
|
||||
aspect-ratio: 16/9;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
&.n3 {
|
||||
aspect-ratio: 16/9;
|
||||
grid-template-columns: 1fr 0.5fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
|
||||
> .media:nth-child(1) {
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
> .media:nth-child(3) {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
&.n4 {
|
||||
aspect-ratio: 16/9;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
|
||||
&.nMany {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
> .media {
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media {
|
||||
overflow: hidden; // clipにするとバグる
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
|
||||
>.mediaInner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.banner {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
@@ -1,64 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<a :href="href" target="_blank" :class="$style.root">
|
||||
<img v-if="!video.isSensitive && video.thumbnailUrl" :class="$style.thumbnail" :src="video.thumbnailUrl">
|
||||
<div :class="$style.videoOverlayPlayButton"><i class="ti ti-player-play-filled"></i></div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
defineProps<{
|
||||
video: Misskey.entities.DriveFile;
|
||||
href: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16 / 9;
|
||||
padding: var(--margin);
|
||||
border: 1px solid var(--divider);
|
||||
border-radius: var(--radius);
|
||||
background-color: #000;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.videoOverlayPlayButton {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
border-radius: 99rem;
|
||||
|
||||
font-size: 1rem;
|
||||
line-height: 1rem;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
</style>
|
File diff suppressed because it is too large
Load Diff
@@ -1,493 +0,0 @@
|
||||
<!--
|
||||
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/>
|
||||
<i class="ti ti-repeat" style="margin-right: 4px;"></i>
|
||||
<span :class="$style.renoteText">
|
||||
<I18n :src="i18n.ts.renotedBy" tag="span">
|
||||
<template #user>
|
||||
<MkA :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/>
|
||||
<div :class="$style.noteHeaderBody">
|
||||
<div :class="$style.noteHeaderBodyUpper">
|
||||
<div style="min-width: 0;">
|
||||
<div class="_nowrap">
|
||||
<MkA :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"
|
||||
/>
|
||||
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
|
||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||
<EmMediaList :mediaList="appearNote.files" :originalEntityUrl="`${url}/notes/${appearNote.id}`"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :readOnly="true" :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 EmMediaList from './EmMediaList.vue';
|
||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import MkReactionsViewer from '@/components/MkReactionsViewer.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>
|
@@ -1,77 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.noNotes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items: notes }">
|
||||
<div :class="[$style.root, { [$style.noGap]: noGap }]">
|
||||
<MkDateSeparatedList
|
||||
ref="notes"
|
||||
v-slot="{ item: note }"
|
||||
:items="notes"
|
||||
:direction="pagination.reversed ? 'up' : 'down'"
|
||||
:reversed="pagination.reversed"
|
||||
:noGap="noGap"
|
||||
:ad="ad"
|
||||
:class="$style.notes"
|
||||
>
|
||||
<EmNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
</MkDateSeparatedList>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from 'vue';
|
||||
import EmNote from '@/components/EmNote.vue';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
pagination: Paging;
|
||||
noGap?: boolean;
|
||||
disableAutoLoad?: boolean;
|
||||
ad?: boolean;
|
||||
}>(), {
|
||||
ad: true,
|
||||
});
|
||||
|
||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
defineExpose({
|
||||
pagingComponent,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
&.noGap {
|
||||
> .notes {
|
||||
background: var(--panel);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.noGap) {
|
||||
> .notes {
|
||||
background: var(--bg);
|
||||
|
||||
.note {
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,39 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.timelineRoot">
|
||||
<div v-if="showHeader" :class="$style.header"><slot name="header"></slot></div>
|
||||
<div :class="$style.body"><slot name="body"></slot></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{
|
||||
showHeader?: boolean;
|
||||
}>(), {
|
||||
showHeader: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.timelineRoot {
|
||||
background-color: var(--panel);
|
||||
height: 100%;
|
||||
max-height: var(--embedMaxHeight, none);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--divider);
|
||||
}
|
||||
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
@@ -1,154 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MkLoading v-if="loading"/>
|
||||
<EmTimelineContainer v-else-if="clip" :showHeader="embedParams.header">
|
||||
<template #header>
|
||||
<div :class="$style.clipHeader">
|
||||
<div :class="$style.headerClipIconRoot">
|
||||
<i class="ti ti-paperclip"></i>
|
||||
</div>
|
||||
<div :class="$style.headerTitle" @click="top">
|
||||
<div class="_nowrap"><a :href="`/clips/${clip.id}`" target="_blank" rel="noopener">{{ clip.name }}</a></div>
|
||||
<div :class="$style.sub">{{ i18n.tsx.fromX({ x: instanceName }) }}</div>
|
||||
</div>
|
||||
<a :href="url" :class="$style.instanceIconLink" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
:class="$style.instanceIcon"
|
||||
:src="instance.iconUrl || '/favicon.ico'"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<EmNotes
|
||||
ref="notesEl"
|
||||
:pagination="pagination"
|
||||
:disableAutoLoad="!embedParams.autoload"
|
||||
:noGap="true"
|
||||
:ad="false"
|
||||
/>
|
||||
</template>
|
||||
</EmTimelineContainer>
|
||||
<XNotFound v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, shallowRef, inject, onActivated } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import EmNotes from '@/embed/components/EmNotes.vue';
|
||||
import XNotFound from '@/pages/not-found.vue';
|
||||
import EmTimelineContainer from '@/embed/components/EmTimelineContainer.vue';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { url, instanceName } from '@/config.js';
|
||||
import { scrollToTop } from '@/scripts/scroll.js';
|
||||
import { isLink } from '@/scripts/is-link.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { defaultEmbedParams } from '@/scripts/embed-page.js';
|
||||
import type { ParsedEmbedParams } from '@/scripts/embed-page.js';
|
||||
|
||||
const props = defineProps<{
|
||||
clipId: string;
|
||||
}>();
|
||||
|
||||
function redirectIfNotEmbedPage() {
|
||||
const inEmbedPage = inject<boolean>('EMBED_PAGE', false);
|
||||
|
||||
if (!inEmbedPage) {
|
||||
const router = useRouter();
|
||||
router.replace(`/clips/${props.clipId}`);
|
||||
}
|
||||
}
|
||||
|
||||
redirectIfNotEmbedPage();
|
||||
|
||||
onActivated(redirectIfNotEmbedPage);
|
||||
|
||||
const embedParams = inject<ParsedEmbedParams>('embedParams', defaultEmbedParams);
|
||||
|
||||
const clip = ref<Misskey.entities.Clip | null>(null);
|
||||
const pagination = computed(() => ({
|
||||
endpoint: 'clips/notes',
|
||||
params: {
|
||||
clipId: props.clipId,
|
||||
},
|
||||
} as Paging));
|
||||
const loading = ref(true);
|
||||
|
||||
const notesEl = shallowRef<InstanceType<typeof EmNotes> | null>(null);
|
||||
|
||||
function top(ev: MouseEvent) {
|
||||
const target = ev.target as HTMLElement | null;
|
||||
if (target && isLink(target)) return;
|
||||
|
||||
if (notesEl.value) {
|
||||
scrollToTop(notesEl.value.$el as HTMLElement, { behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
misskeyApi('clips/show', {
|
||||
clipId: props.clipId,
|
||||
}).then(res => {
|
||||
clip.value = res;
|
||||
loading.value = false;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
loading.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.clipHeader {
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--margin);
|
||||
overflow: hidden;
|
||||
|
||||
.headerClipIconRoot {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
flex-grow: 1;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
min-width: 0;
|
||||
|
||||
.sub {
|
||||
font-size: 0.8em;
|
||||
font-weight: 400;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.instanceIconLink {
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.instanceIcon {
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,62 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.noteEmbedRoot">
|
||||
<MkLoading v-if="loading"/>
|
||||
<EmNoteDetailed v-else-if="note" :note="note"/>
|
||||
<XNotFound v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, provide, inject, onActivated } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import EmNoteDetailed from '@/embed/components/EmNoteDetailed.vue';
|
||||
import XNotFound from '@/pages/not-found.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
|
||||
const props = defineProps<{
|
||||
noteId: string;
|
||||
}>();
|
||||
|
||||
function redirectIfNotEmbedPage() {
|
||||
const inEmbedPage = inject<boolean>('EMBED_PAGE', false);
|
||||
|
||||
if (!inEmbedPage) {
|
||||
const router = useRouter();
|
||||
router.replace(`/notes/${props.noteId}`);
|
||||
}
|
||||
}
|
||||
|
||||
redirectIfNotEmbedPage();
|
||||
|
||||
onActivated(redirectIfNotEmbedPage);
|
||||
|
||||
provide('EMBED_ORIGINAL_ENTITY_URL', `/notes/${props.noteId}`);
|
||||
|
||||
const note = ref<Misskey.entities.Note | null>(null);
|
||||
const loading = ref(true);
|
||||
|
||||
misskeyApi('notes/show', {
|
||||
noteId: props.noteId,
|
||||
}).then(res => {
|
||||
// リモートのノートは埋め込ませない
|
||||
if (res.url == null && res.uri == null) {
|
||||
note.value = res;
|
||||
}
|
||||
loading.value = false;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
loading.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.noteEmbedRoot {
|
||||
background-color: var(--panel);
|
||||
}
|
||||
</style>
|
@@ -1,139 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<EmTimelineContainer v-if="tag" :showHeader="embedParams.header">
|
||||
<template #header>
|
||||
<div :class="$style.clipHeader">
|
||||
<div :class="$style.headerClipIconRoot">
|
||||
<i class="ti ti-hash"></i>
|
||||
</div>
|
||||
<div :class="$style.headerTitle" @click="top">
|
||||
<div class="_nowrap"><a :href="`/tags/${tag}`" target="_blank" rel="noopener">#{{ tag }}</a></div>
|
||||
<div :class="$style.sub">{{ i18n.tsx.fromX({ x: instanceName }) }}</div>
|
||||
</div>
|
||||
<a :href="url" :class="$style.instanceIconLink" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
:class="$style.instanceIcon"
|
||||
:src="instance.iconUrl || '/favicon.ico'"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<EmNotes
|
||||
ref="notesEl"
|
||||
:pagination="pagination"
|
||||
:disableAutoLoad="!embedParams.autoload"
|
||||
:noGap="true"
|
||||
:ad="false"
|
||||
/>
|
||||
</template>
|
||||
</EmTimelineContainer>
|
||||
<XNotFound v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, shallowRef, inject, onActivated } from 'vue';
|
||||
import EmNotes from '@/embed/components/EmNotes.vue';
|
||||
import XNotFound from '@/pages/not-found.vue';
|
||||
import EmTimelineContainer from '@/embed/components/EmTimelineContainer.vue';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { url, instanceName } from '@/config.js';
|
||||
import { scrollToTop } from '@/scripts/scroll.js';
|
||||
import { isLink } from '@/scripts/is-link.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { defaultEmbedParams } from '@/scripts/embed-page.js';
|
||||
import type { ParsedEmbedParams } from '@/scripts/embed-page.js';
|
||||
|
||||
const props = defineProps<{
|
||||
tag: string;
|
||||
}>();
|
||||
|
||||
function redirectIfNotEmbedPage() {
|
||||
const inEmbedPage = inject<boolean>('EMBED_PAGE', false);
|
||||
|
||||
if (!inEmbedPage) {
|
||||
const router = useRouter();
|
||||
router.replace(`/tags/${props.tag}`);
|
||||
}
|
||||
}
|
||||
|
||||
redirectIfNotEmbedPage();
|
||||
|
||||
onActivated(redirectIfNotEmbedPage);
|
||||
|
||||
const embedParams = inject<ParsedEmbedParams>('embedParams', defaultEmbedParams);
|
||||
|
||||
const pagination = computed(() => ({
|
||||
endpoint: 'notes/search-by-tag',
|
||||
params: {
|
||||
tag: props.tag,
|
||||
},
|
||||
} as Paging));
|
||||
|
||||
const notesEl = shallowRef<InstanceType<typeof EmNotes> | null>(null);
|
||||
|
||||
function top(ev: MouseEvent) {
|
||||
const target = ev.target as HTMLElement | null;
|
||||
if (target && isLink(target)) return;
|
||||
|
||||
if (notesEl.value) {
|
||||
scrollToTop(notesEl.value.$el as HTMLElement, { behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.clipHeader {
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--margin);
|
||||
overflow: hidden;
|
||||
|
||||
.headerClipIconRoot {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
flex-grow: 1;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
min-width: 0;
|
||||
|
||||
.sub {
|
||||
font-size: 0.8em;
|
||||
font-weight: 400;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.instanceIconLink {
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.instanceIcon {
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,159 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MkLoading v-if="loading"/>
|
||||
<EmTimelineContainer v-else-if="user" :showHeader="embedParams.header">
|
||||
<template #header>
|
||||
<div :class="$style.userHeader">
|
||||
<a :href="`/@${user.username}`" target="_blank" rel="noopener noreferrer" :class="$style.avatarLink">
|
||||
<MkAvatar :class="$style.avatar" :user="user"/>
|
||||
</a>
|
||||
<div :class="$style.headerTitle" @click="top">
|
||||
<I18n :src="i18n.ts.noteOf" tag="div" class="_nowrap">
|
||||
<template #user>
|
||||
<a v-if="user != null" :href="`/@${user.username}`" target="_blank" rel="noopener noreferrer">
|
||||
<MkUserName :user="user"/>
|
||||
</a>
|
||||
<span v-else>{{ i18n.ts.user }}</span>
|
||||
</template>
|
||||
</I18n>
|
||||
<div :class="$style.sub">{{ i18n.tsx.fromX({ x: instanceName }) }}</div>
|
||||
</div>
|
||||
<a :href="url" :class="$style.instanceIconLink" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
:class="$style.instanceIcon"
|
||||
:src="instance.iconUrl || '/favicon.ico'"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<EmNotes
|
||||
ref="notesEl"
|
||||
:pagination="pagination"
|
||||
:disableAutoLoad="!embedParams.autoload"
|
||||
:noGap="true"
|
||||
:ad="false"
|
||||
/>
|
||||
</template>
|
||||
</EmTimelineContainer>
|
||||
<XNotFound v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, shallowRef, inject, onActivated } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import type { ParsedEmbedParams } from '@/scripts/embed-page.js';
|
||||
import EmNotes from '@/embed/components/EmNotes.vue';
|
||||
import XNotFound from '@/pages/not-found.vue';
|
||||
import EmTimelineContainer from '@/embed/components/EmTimelineContainer.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { url, instanceName } from '@/config.js';
|
||||
import { scrollToTop } from '@/scripts/scroll.js';
|
||||
import { isLink } from '@/scripts/is-link.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { defaultEmbedParams } from '@/scripts/embed-page.js';
|
||||
|
||||
const props = defineProps<{
|
||||
username: string;
|
||||
}>();
|
||||
|
||||
function redirectIfNotEmbedPage() {
|
||||
const inEmbedPage = inject<boolean>('EMBED_PAGE', false);
|
||||
|
||||
if (!inEmbedPage) {
|
||||
const router = useRouter();
|
||||
router.replace(`/@${props.username}`);
|
||||
}
|
||||
}
|
||||
|
||||
redirectIfNotEmbedPage();
|
||||
|
||||
onActivated(redirectIfNotEmbedPage);
|
||||
|
||||
const embedParams = inject<ParsedEmbedParams>('embedParams', defaultEmbedParams);
|
||||
|
||||
const user = ref<Misskey.entities.UserLite | null>(null);
|
||||
const pagination = computed(() => ({
|
||||
endpoint: 'users/notes',
|
||||
params: {
|
||||
userId: user.value?.id,
|
||||
},
|
||||
} as Paging));
|
||||
const loading = ref(true);
|
||||
|
||||
const notesEl = shallowRef<InstanceType<typeof EmNotes> | null>(null);
|
||||
|
||||
function top(ev: MouseEvent) {
|
||||
const target = ev.target as HTMLElement | null;
|
||||
if (target && isLink(target)) return;
|
||||
|
||||
if (notesEl.value) {
|
||||
scrollToTop(notesEl.value.$el as HTMLElement, { behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
misskeyApi('users/show', {
|
||||
username: props.username,
|
||||
}).then(res => {
|
||||
user.value = res;
|
||||
loading.value = false;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
loading.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.userHeader {
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--margin);
|
||||
overflow: hidden;
|
||||
|
||||
.avatarLink {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
flex-grow: 1;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
min-width: 0;
|
||||
|
||||
.sub {
|
||||
font-size: 0.8em;
|
||||
font-weight: 400;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.instanceIconLink {
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.instanceIcon {
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { IRouter, RouteDef } from '@/nirax.js';
|
||||
import { Router } from '@/nirax.js';
|
||||
import { page } from '@/router/definition.js';
|
||||
|
||||
const routes: RouteDef[] = [{
|
||||
path: '/embed/notes/:noteId',
|
||||
component: page(() => import('@/embed/pages/note.vue')),
|
||||
}, {
|
||||
path: '/embed/user-timeline/@:username',
|
||||
component: page(() => import('@/embed/pages/user-timeline.vue')),
|
||||
}, {
|
||||
path: '/embed/clips/:clipId',
|
||||
component: page(() => import('@/embed/pages/clip.vue')),
|
||||
}, {
|
||||
path: '/embed/tags/:tag',
|
||||
component: page(() => import('@/embed/pages/tag.vue')),
|
||||
}, {
|
||||
path: '/:(*)',
|
||||
component: page(() => import('@/pages/not-found.vue')),
|
||||
}];
|
||||
|
||||
export function createEmbedRouter(path: string): IRouter {
|
||||
return new Router(routes, path, false, page(() => import('@/pages/not-found.vue')));
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
@charset "utf-8";
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
html.embed {
|
||||
background-color: transparent;
|
||||
color-scheme: light dark;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html.embed,
|
||||
html.embed body,
|
||||
html.embed #misskey_app {
|
||||
height: 100%;
|
||||
}
|
@@ -1,107 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="rootEl"
|
||||
:class="[
|
||||
$style.rootForEmbedPage,
|
||||
{
|
||||
[$style.rounded]: embedRounded,
|
||||
[$style.noBorder]: embedNoBorder,
|
||||
}
|
||||
]"
|
||||
:style="maxHeight > 0 ? { maxHeight: `${maxHeight}px`, '--embedMaxHeight': `${maxHeight}px` } : {}"
|
||||
>
|
||||
<div
|
||||
:class="$style.routerViewContainer"
|
||||
>
|
||||
<RouterView/>
|
||||
</div>
|
||||
|
||||
<XCommon/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, ref, shallowRef, onMounted, onUnmounted, inject } from 'vue';
|
||||
import XCommon from '@/ui/_common_/common.vue';
|
||||
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
import { instanceName } from '@/config.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
import { postMessageToParentWindow } from '@/scripts/post-message.js';
|
||||
import { defaultEmbedParams } from '@/scripts/embed-page.js';
|
||||
import type { ParsedEmbedParams } from '@/scripts/embed-page.js';
|
||||
|
||||
const embedParams = inject<ParsedEmbedParams>('embedParams', defaultEmbedParams);
|
||||
|
||||
const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
|
||||
|
||||
const pageMetadata = ref<null | PageMetadata>(null);
|
||||
|
||||
provide('router', mainRouter);
|
||||
provideMetadataReceiver((metadataGetter) => {
|
||||
const info = metadataGetter();
|
||||
pageMetadata.value = info;
|
||||
if (pageMetadata.value) {
|
||||
if (isRoot.value && pageMetadata.value.title === instanceName) {
|
||||
document.title = pageMetadata.value.title;
|
||||
} else {
|
||||
document.title = `${pageMetadata.value.title} | ${instanceName}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
provideReactiveMetadata(pageMetadata);
|
||||
|
||||
//#region Embed Style
|
||||
const embedRounded = ref(embedParams.rounded);
|
||||
const embedNoBorder = ref(!embedParams.border);
|
||||
const maxHeight = ref(embedParams.maxHeight ?? 0);
|
||||
//#endregion
|
||||
|
||||
//#region Embed Resizer
|
||||
const rootEl = shallowRef<HTMLElement | null>(null);
|
||||
|
||||
let previousHeight = 0;
|
||||
const resizeObserver = new ResizeObserver(async () => {
|
||||
const height = rootEl.value!.scrollHeight + (embedNoBorder.value ? 0 : 2); // border 上下1px
|
||||
if (Math.abs(previousHeight - height) < 1) return; // 1px未満の変化は無視
|
||||
postMessageToParentWindow('misskey:embed:changeHeight', {
|
||||
height: (maxHeight.value > 0 && height > maxHeight.value) ? maxHeight.value : height,
|
||||
});
|
||||
previousHeight = height;
|
||||
});
|
||||
onMounted(() => {
|
||||
resizeObserver.observe(rootEl.value!);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
resizeObserver.disconnect();
|
||||
});
|
||||
//#endregion
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.rootForEmbedPage {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--divider);
|
||||
background-color: var(--bg);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: auto;
|
||||
|
||||
&.rounded {
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
&.noBorder {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.routerViewContainer {
|
||||
container-type: inline-size;
|
||||
max-height: var(--embedMaxHeight, none);
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user