feat: ノート・ユーザTL埋め込み
This commit is contained in:
		@@ -764,9 +764,9 @@ export class ClientServerService {
 | 
				
			|||||||
		//#endregion
 | 
							//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		//#region embed pages
 | 
							//#region embed pages
 | 
				
			||||||
		fastify.get('/embed/:path(.*)', async (request, reply) => {
 | 
							fastify.get('/embed/*', async (request, reply) => {
 | 
				
			||||||
			reply.removeHeader('X-Frame-Options');
 | 
								reply.removeHeader('X-Frame-Options');
 | 
				
			||||||
			return await renderBase(reply, { noindex: true });
 | 
								return await renderBase(reply, { noindex: true, embed: true });
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		fastify.get('/_info_card_', async (request, reply) => {
 | 
							fastify.get('/_info_card_', async (request, reply) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,12 @@ html {
 | 
				
			|||||||
	color: var(--fg);
 | 
						color: var(--fg);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html.embed {
 | 
				
			||||||
 | 
						box-sizing: border-box;
 | 
				
			||||||
 | 
						background-color: transparent;
 | 
				
			||||||
 | 
						max-width: 500px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#splash {
 | 
					#splash {
 | 
				
			||||||
	position: fixed;
 | 
						position: fixed;
 | 
				
			||||||
	z-index: 10000;
 | 
						z-index: 10000;
 | 
				
			||||||
@@ -22,6 +28,13 @@ html {
 | 
				
			|||||||
	transition: opacity 0.5s ease;
 | 
						transition: opacity 0.5s ease;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html.embed #splash {
 | 
				
			||||||
 | 
						box-sizing: border-box;
 | 
				
			||||||
 | 
						min-height: 300px;
 | 
				
			||||||
 | 
						border-radius: var(--radius, 12px);
 | 
				
			||||||
 | 
						border: 1px solid var(--divider);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#splashIcon {
 | 
					#splashIcon {
 | 
				
			||||||
	position: absolute;
 | 
						position: absolute;
 | 
				
			||||||
	top: 0;
 | 
						top: 0;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -77,7 +77,7 @@ html
 | 
				
			|||||||
		script
 | 
							script
 | 
				
			||||||
			include ../boot.js
 | 
								include ../boot.js
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	body
 | 
						body(class=embed && 'embed')
 | 
				
			||||||
		noscript: p
 | 
							noscript: p
 | 
				
			||||||
			| JavaScriptを有効にしてください
 | 
								| JavaScriptを有効にしてください
 | 
				
			||||||
			br
 | 
								br
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,21 +7,33 @@
 | 
				
			|||||||
import 'vite/modulepreload-polyfill';
 | 
					import 'vite/modulepreload-polyfill';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import '@/style.scss';
 | 
					import '@/style.scss';
 | 
				
			||||||
 | 
					import type { CommonBootOptions } from '@/boot/common.js';
 | 
				
			||||||
import { mainBoot } from '@/boot/main-boot.js';
 | 
					import { mainBoot } from '@/boot/main-boot.js';
 | 
				
			||||||
import { subBoot } from '@/boot/sub-boot.js';
 | 
					import { subBoot } from '@/boot/sub-boot.js';
 | 
				
			||||||
import { isEmbedPage } from '@/scripts/embed-page.js';
 | 
					import { isEmbedPage } from '@/scripts/embed-page.js';
 | 
				
			||||||
 | 
					import { setIframeId, postMessageToParentWindow } from '@/scripts/post-message.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete', '/embed'];
 | 
					const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) {
 | 
					if (isEmbedPage()) {
 | 
				
			||||||
	if (isEmbedPage()) {
 | 
						const bootOptions: Partial<CommonBootOptions> = {};
 | 
				
			||||||
		const params = new URLSearchParams(location.search);
 | 
					
 | 
				
			||||||
		const color = params.get('color');
 | 
						const params = new URLSearchParams(location.search);
 | 
				
			||||||
		if (color && ['light', 'dark'].includes(color)) {
 | 
						const color = params.get('color');
 | 
				
			||||||
			subBoot({ forceColorMode: color as 'light' | 'dark' });
 | 
						if (color && ['light', 'dark'].includes(color)) {
 | 
				
			||||||
		}
 | 
							bootOptions.forceColorMode = color as 'light' | 'dark';
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
					
 | 
				
			||||||
 | 
						window.addEventListener('message', event => {
 | 
				
			||||||
 | 
							if (event.data?.type === 'misskey:embedParent:registerIframeId' && event.data.payload?.iframeId != null) {
 | 
				
			||||||
 | 
								setIframeId(event.data.payload.iframeId);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						subBoot(bootOptions, true).then(() => {
 | 
				
			||||||
 | 
							postMessageToParentWindow('misskey:embed:ready');
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					} else if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) {
 | 
				
			||||||
	subBoot();
 | 
						subBoot();
 | 
				
			||||||
} else {
 | 
					} else {
 | 
				
			||||||
	mainBoot();
 | 
						mainBoot();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,7 @@ import { fetchCustomEmojis } from '@/custom-emojis.js';
 | 
				
			|||||||
import { setupRouter } from '@/router/definition.js';
 | 
					import { setupRouter } from '@/router/definition.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type CommonBootOptions = {
 | 
					export type CommonBootOptions = {
 | 
				
			||||||
	forceColorMode?: 'dark' | 'light' | 'auto';
 | 
						forceColorMode: 'dark' | 'light' | 'auto';
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const defaultCommonBootOptions: CommonBootOptions = {
 | 
					const defaultCommonBootOptions: CommonBootOptions = {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,8 +7,8 @@ import { createApp, defineAsyncComponent } from 'vue';
 | 
				
			|||||||
import { common } from './common.js';
 | 
					import { common } from './common.js';
 | 
				
			||||||
import type { CommonBootOptions } from './common.js';
 | 
					import type { CommonBootOptions } from './common.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function subBoot(options?: CommonBootOptions) {
 | 
					export async function subBoot(options?: Partial<CommonBootOptions>, isEmbedPage?: boolean) {
 | 
				
			||||||
	const { isClientUpdated } = await common(() => createApp(
 | 
						const { isClientUpdated } = await common(() => createApp(
 | 
				
			||||||
		defineAsyncComponent(() => import('@/ui/minimum.vue')),
 | 
							defineAsyncComponent(() => isEmbedPage ? import('@/ui/embed.vue') : import('@/ui/minimum.vue')),
 | 
				
			||||||
	), options);
 | 
						), options);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +0,0 @@
 | 
				
			|||||||
<template>
 | 
					 | 
				
			||||||
<div></div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="scss" module>
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
							
								
								
									
										33
									
								
								packages/frontend/src/pages/embed/note.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								packages/frontend/src/pages/embed/note.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
						<div :class="$style.noteEmbedRoot">
 | 
				
			||||||
 | 
							<MkLoading v-if="loading"/>
 | 
				
			||||||
 | 
							<MkNote v-else-if="note" :note="note"/>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref } from 'vue';
 | 
				
			||||||
 | 
					import * as Misskey from 'misskey-js';
 | 
				
			||||||
 | 
					import MkNote from '@/components/MkNote.vue';
 | 
				
			||||||
 | 
					import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
						noteId: string;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const note = ref<Misskey.entities.Note | null>(null);
 | 
				
			||||||
 | 
					const loading = ref(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					misskeyApi('notes/show', {
 | 
				
			||||||
 | 
						noteId: props.noteId,
 | 
				
			||||||
 | 
					}).then(res => {
 | 
				
			||||||
 | 
						note.value = res;
 | 
				
			||||||
 | 
						loading.value = false;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss" module>
 | 
				
			||||||
 | 
					.noteEmbedRoot {
 | 
				
			||||||
 | 
						background-color: var(--panel);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										57
									
								
								packages/frontend/src/pages/embed/user-timeline.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								packages/frontend/src/pages/embed/user-timeline.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
						<div :class="$style.userTimelineRoot">
 | 
				
			||||||
 | 
							<MkLoading v-if="loading"/>
 | 
				
			||||||
 | 
							<template v-else-if="user">
 | 
				
			||||||
 | 
								<div v-if="normalizedShowHeader" :class="$style.userHeader">
 | 
				
			||||||
 | 
									<MkAvatar :user="user"/>{{ user.name }} のノート
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<MkNotes :class="$style.userTimelineNotes" :pagination="pagination" :noGap="true"/>
 | 
				
			||||||
 | 
							</template>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref, computed } from 'vue';
 | 
				
			||||||
 | 
					import * as Misskey from 'misskey-js';
 | 
				
			||||||
 | 
					import MkNotes from '@/components/MkNotes.vue';
 | 
				
			||||||
 | 
					import type { Paging } from '@/components/MkPagination.vue';
 | 
				
			||||||
 | 
					import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
						username: string;
 | 
				
			||||||
 | 
						showHeader?: string;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const normalizedShowHeader = computed(() => props.showHeader !== 'false');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					misskeyApi('users/show', {
 | 
				
			||||||
 | 
						username: props.username,
 | 
				
			||||||
 | 
					}).then(res => {
 | 
				
			||||||
 | 
						user.value = res;
 | 
				
			||||||
 | 
						loading.value = false;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss" module>
 | 
				
			||||||
 | 
					.userTimelineRoot {
 | 
				
			||||||
 | 
						background-color: var(--panel);
 | 
				
			||||||
 | 
						height: 100%;
 | 
				
			||||||
 | 
						max-height: var(--embedMaxHeight, none);
 | 
				
			||||||
 | 
						display: flex;
 | 
				
			||||||
 | 
						flex-direction: column;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.userTimelineNotes {
 | 
				
			||||||
 | 
						flex: 1;
 | 
				
			||||||
 | 
						overflow-y: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -556,9 +556,14 @@ const routes: RouteDef[] = [{
 | 
				
			|||||||
	component: page(() => import('@/pages/reversi/game.vue')),
 | 
						component: page(() => import('@/pages/reversi/game.vue')),
 | 
				
			||||||
	loginRequired: false,
 | 
						loginRequired: false,
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
	path: '/embed',
 | 
						path: '/embed/notes/:noteId',
 | 
				
			||||||
	component: page(() => import('@/pages/embed/index.vue')),
 | 
						component: page(() => import('@/pages/embed/note.vue')),
 | 
				
			||||||
//	children: [],
 | 
					}, {
 | 
				
			||||||
 | 
						path: '/embed/user-timeline/@:username',
 | 
				
			||||||
 | 
						component: page(() => import('@/pages/embed/user-timeline.vue')),
 | 
				
			||||||
 | 
						query: {
 | 
				
			||||||
 | 
							header: 'showHeader',
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
	path: '/timeline',
 | 
						path: '/timeline',
 | 
				
			||||||
	component: page(() => import('@/pages/timeline.vue')),
 | 
						component: page(() => import('@/pages/timeline.vue')),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const postMessageEventTypes = [
 | 
					export const postMessageEventTypes = [
 | 
				
			||||||
	'misskey:shareForm:shareCompleted',
 | 
						'misskey:shareForm:shareCompleted',
 | 
				
			||||||
 | 
						'misskey:embed:ready',
 | 
				
			||||||
	'misskey:embed:changeHeight',
 | 
						'misskey:embed:changeHeight',
 | 
				
			||||||
] as const;
 | 
					] as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -12,16 +13,29 @@ export type PostMessageEventType = typeof postMessageEventTypes[number];
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export type MiPostMessageEvent = {
 | 
					export type MiPostMessageEvent = {
 | 
				
			||||||
	type: PostMessageEventType;
 | 
						type: PostMessageEventType;
 | 
				
			||||||
 | 
						iframeId?: string;
 | 
				
			||||||
	payload?: any;
 | 
						payload?: any;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let defaultIframeId: string | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function setIframeId(id: string): void {
 | 
				
			||||||
 | 
						if (_DEV_) console.log('setIframeId', id);
 | 
				
			||||||
 | 
						defaultIframeId = id;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 親フレームにイベントを送信
 | 
					 * 親フレームにイベントを送信
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void {
 | 
					export function postMessageToParentWindow(type: PostMessageEventType, payload?: any, iframeId: string | null = null): void {
 | 
				
			||||||
	if (_DEV_) console.log('postMessageToParentWindow', type, payload);
 | 
						let _iframeId = iframeId;
 | 
				
			||||||
 | 
						if (_iframeId == null) {
 | 
				
			||||||
 | 
							_iframeId = defaultIframeId;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (_DEV_) console.log('postMessageToParentWindow', type, _iframeId, payload);
 | 
				
			||||||
	window.parent.postMessage({
 | 
						window.parent.postMessage({
 | 
				
			||||||
		type,
 | 
							type,
 | 
				
			||||||
 | 
							iframeId: _iframeId,
 | 
				
			||||||
		payload,
 | 
							payload,
 | 
				
			||||||
	}, '*');
 | 
						}, '*');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -93,9 +93,16 @@ html {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	&.embed {
 | 
						&.embed {
 | 
				
			||||||
		background-color: transparent;
 | 
							background-color: transparent;
 | 
				
			||||||
 | 
							overflow: hidden;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html.embed,
 | 
				
			||||||
 | 
					html.embed body,
 | 
				
			||||||
 | 
					html.embed #misskey_app {
 | 
				
			||||||
 | 
						height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
html._themeChanging_ {
 | 
					html._themeChanging_ {
 | 
				
			||||||
	&, * {
 | 
						&, * {
 | 
				
			||||||
		transition: background 1s ease, border 1s ease !important;
 | 
							transition: background 1s ease, border 1s ease !important;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										113
									
								
								packages/frontend/src/ui/embed.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								packages/frontend/src/ui/embed.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					SPDX-FileCopyrightText: syuilo and misskey-project
 | 
				
			||||||
 | 
					SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div
 | 
				
			||||||
 | 
						ref="rootEl"
 | 
				
			||||||
 | 
						:class="[
 | 
				
			||||||
 | 
							$style.rootForEmbedPage,
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								[$style.rounded]: embedRounded,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						]"
 | 
				
			||||||
 | 
						: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 } from 'vue';
 | 
				
			||||||
 | 
					import XCommon from './_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';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 params = new URLSearchParams(location.search);
 | 
				
			||||||
 | 
					const embedRounded = ref(params.get('rounded') !== '0');
 | 
				
			||||||
 | 
					const maxHeight = ref(params.get('maxHeight') ? parseInt(params.get('maxHeight')!) : 0);
 | 
				
			||||||
 | 
					//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//#region Embed Resizer
 | 
				
			||||||
 | 
					const rootEl = shallowRef<HTMLElement | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let resizeMessageThrottleTimer: number | null = null;
 | 
				
			||||||
 | 
					let resizeMessageThrottleFlag = false;
 | 
				
			||||||
 | 
					let previousHeight = 0;
 | 
				
			||||||
 | 
					const resizeObserver = new ResizeObserver(async () => {
 | 
				
			||||||
 | 
						const height = rootEl.value!.scrollHeight + 2; // border 上下1px
 | 
				
			||||||
 | 
						if (resizeMessageThrottleFlag && Math.abs(previousHeight - height) < 30) return;
 | 
				
			||||||
 | 
						if (resizeMessageThrottleTimer) window.clearTimeout(resizeMessageThrottleTimer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						postMessageToParentWindow('misskey:embed:changeHeight', {
 | 
				
			||||||
 | 
							height: (maxHeight.value > 0 && height > maxHeight.value) ? maxHeight.value : height,
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						previousHeight = height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resizeMessageThrottleFlag = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resizeMessageThrottleTimer = window.setTimeout(() => {
 | 
				
			||||||
 | 
							resizeMessageThrottleFlag = false; // 収縮をやりすぎるとチカチカする
 | 
				
			||||||
 | 
						}, 500);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
						resizeObserver.observe(rootEl.value!);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					onUnmounted(() => {
 | 
				
			||||||
 | 
						resizeObserver.disconnect();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.documentElement.style.maxWidth = '500px';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// サーバー起動の場合はもとから付与されているためdevのみ
 | 
				
			||||||
 | 
					if (_DEV_) document.documentElement.classList.add('embed');
 | 
				
			||||||
 | 
					</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);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.routerViewContainer {
 | 
				
			||||||
 | 
						container-type: inline-size;
 | 
				
			||||||
 | 
						max-height: var(--embedMaxHeight, none);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -4,15 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
-->
 | 
					-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<div
 | 
					<div :class="$style.root">
 | 
				
			||||||
	ref="rootEl"
 | 
					 | 
				
			||||||
	:class="isEmbed ? [
 | 
					 | 
				
			||||||
		$style.rootForEmbedPage,
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			[$style.rounded]: embedRounded,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	] : [$style.root]"
 | 
					 | 
				
			||||||
>
 | 
					 | 
				
			||||||
	<div style="container-type: inline-size;">
 | 
						<div style="container-type: inline-size;">
 | 
				
			||||||
		<RouterView/>
 | 
							<RouterView/>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
@@ -22,15 +14,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { computed, provide, ref, shallowRef, onMounted, onUnmounted } from 'vue';
 | 
					import { computed, provide, ref } from 'vue';
 | 
				
			||||||
import XCommon from './_common_/common.vue';
 | 
					import XCommon from './_common_/common.vue';
 | 
				
			||||||
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
 | 
					import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
 | 
				
			||||||
import { instanceName } from '@/config.js';
 | 
					import { instanceName } from '@/config.js';
 | 
				
			||||||
import { mainRouter } from '@/router/main.js';
 | 
					import { mainRouter } from '@/router/main.js';
 | 
				
			||||||
import { isEmbedPage } from '@/scripts/embed-page.js';
 | 
					 | 
				
			||||||
import { postMessageToParentWindow } from '@/scripts/post-message';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const isEmbed = isEmbedPage();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
 | 
					const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -50,35 +38,7 @@ provideMetadataReceiver((metadataGetter) => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
provideReactiveMetadata(pageMetadata);
 | 
					provideReactiveMetadata(pageMetadata);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//#region Embed Style
 | 
					document.documentElement.style.overflowY = 'scroll';
 | 
				
			||||||
const params = new URLSearchParams(location.search);
 | 
					 | 
				
			||||||
const embedRounded = ref(params.get('rounded') !== '0');
 | 
					 | 
				
			||||||
//#endregion
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
//#region Embed Resizer
 | 
					 | 
				
			||||||
const rootEl = shallowRef<HTMLElement | null>(null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if (isEmbed) {
 | 
					 | 
				
			||||||
	const resizeObserver = new ResizeObserver(async () => {
 | 
					 | 
				
			||||||
		postMessageToParentWindow('misskey:embed:changeHeight', {
 | 
					 | 
				
			||||||
			height: rootEl.value!.scrollHeight + 2, // border 上下1px
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
	onMounted(() => {
 | 
					 | 
				
			||||||
		resizeObserver.observe(rootEl.value!);
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
	onUnmounted(() => {
 | 
					 | 
				
			||||||
		resizeObserver.disconnect();
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
//#endregion
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if (isEmbed) {
 | 
					 | 
				
			||||||
	document.documentElement.style.maxWidth = '500px';
 | 
					 | 
				
			||||||
	document.documentElement.classList.add('embed');
 | 
					 | 
				
			||||||
} else {
 | 
					 | 
				
			||||||
	document.documentElement.style.overflowY = 'scroll';
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" module>
 | 
					<style lang="scss" module>
 | 
				
			||||||
@@ -86,16 +46,4 @@ if (isEmbed) {
 | 
				
			|||||||
	min-height: 100dvh;
 | 
						min-height: 100dvh;
 | 
				
			||||||
	box-sizing: border-box;
 | 
						box-sizing: border-box;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
.rootForEmbedPage {
 | 
					 | 
				
			||||||
	box-sizing: border-box;
 | 
					 | 
				
			||||||
	border: 1px solid var(--divider);
 | 
					 | 
				
			||||||
	background-color: var(--bg);
 | 
					 | 
				
			||||||
	overflow: hidden;
 | 
					 | 
				
			||||||
	position: relative;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	&.rounded {
 | 
					 | 
				
			||||||
		border-radius: var(--radius);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user