From 1c35bf32bb04f6eca43f807717bf4a7053496e39 Mon Sep 17 00:00:00 2001
From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Mon, 3 Jun 2024 19:05:17 +0900
Subject: [PATCH] =?UTF-8?q?media=E3=81=BE=E3=82=8F=E3=82=8A=E3=81=AE?=
 =?UTF-8?q?=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/index.d.ts                            |  4 ++
 locales/ja-JP.yml                             |  1 +
 .../frontend/src/components/MkMediaImage.vue  |  2 +
 .../frontend/src/components/MkMediaList.vue   | 42 ++++++++++++++++---
 packages/frontend/src/components/MkNote.vue   |  3 +-
 .../src/components/MkNoteDetailed.vue         |  4 +-
 .../components/MkReactionsViewer.reaction.vue |  3 +-
 .../src/components/MkSubNoteContent.vue       |  3 +-
 packages/frontend/src/pages/embed/note.vue    |  5 ++-
 .../src/pages/embed/user-timeline.vue         |  6 ++-
 packages/frontend/src/ui/embed.vue            | 12 +-----
 11 files changed, 60 insertions(+), 25 deletions(-)

diff --git a/locales/index.d.ts b/locales/index.d.ts
index 0b1b86d373..1ba4ad79c3 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -4984,6 +4984,10 @@ export interface Locale extends ILocale {
      * お問い合わせ
      */
     "inquiry": string;
+    /**
+     * {x}から
+     */
+    "fromX": ParameterizedString<"x">;
     "_delivery": {
         /**
          * 配信状態
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index a89cfbd843..da45b0159b 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1242,6 +1242,7 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
 noDescription: "説明文はありません"
 alwaysConfirmFollow: "フォローの際常に確認する"
 inquiry: "お問い合わせ"
+fromX: "{x}から"
 
 _delivery:
   status: "配信状態"
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 82f36fe5c4..30e6cf11db 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -14,6 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 			title: image.name,
 			class: $style.imageContainer,
 			href: image.url,
+			target: '_blank',
+			rel: 'noopener',
 			style: 'cursor: zoom-in;'
 		}"
 	>
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index b1321a8ef9..4909842db6 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -5,7 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div>
-	<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
+	<div v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :class="$style.banner">
+		<XBanner :media="media"/>
+		<a v-if="inEmbedPage && originalEntityUrl" :href="originalEntityUrl" target="_blank" rel="noopener" :class="$style.mediaLinkForEmbed"></a>
+	</div>
 	<div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container">
 		<div
 			ref="gallery"
@@ -18,17 +21,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 				}] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany,
 			]"
 		>
-			<template v-for="media in mediaList.filter(media => previewable(media))">
-				<XVideo v-if="media.type.startsWith('video')" :key="`video:${media.id}`" :class="$style.media" :video="media"/>
-				<XImage v-else-if="media.type.startsWith('image')" :key="`image:${media.id}`" :class="$style.media" class="image" :data-id="media.id" :image="media" :raw="raw"/>
-			</template>
+			<div v-for="media in mediaList.filter(media => previewable(media))" :class="$style.media">
+				<XVideo v-if="media.type.startsWith('video')" :key="`video:${media.id}`" :video="media" :class="$style.mediaInner"/>
+				<XImage v-else-if="media.type.startsWith('image')" :key="`image:${media.id}`" :class="$style.mediaInner" class="image" :data-id="media.id" :image="media" :raw="raw"/>
+				<a v-if="inEmbedPage && originalEntityUrl" :href="originalEntityUrl" target="_blank" rel="noopener" :class="$style.mediaLinkForEmbed"></a>
+			</div>
 		</div>
 	</div>
 </div>
 </template>
 
 <script lang="ts" setup>
-import { computed, onMounted, onUnmounted, shallowRef } from 'vue';
+import { computed, onMounted, onUnmounted, shallowRef, inject } from 'vue';
 import * as Misskey from 'misskey-js';
 import PhotoSwipeLightbox from 'photoswipe/lightbox';
 import PhotoSwipe from 'photoswipe';
@@ -43,8 +47,13 @@ import { defaultStore } from '@/store.js';
 const props = defineProps<{
 	mediaList: Misskey.entities.DriveFile[];
 	raw?: boolean;
+
+	/** 埋め込みページ用 親要素の正規URL */
+	originalEntityUrl?: string;
 }>();
 
+const inEmbedPage = inject<boolean>('EMBED_PAGE', false);
+
 const gallery = shallowRef<HTMLDivElement>();
 const pswpZIndex = os.claimZIndex('middle');
 document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
@@ -90,6 +99,7 @@ async function calcAspectRatio() {
 
 onMounted(() => {
 	calcAspectRatio();
+	if (defaultStore.state.imageNewTab || inEmbedPage) return;
 
 	lightbox = new PhotoSwipeLightbox({
 		dataSource: props.mediaList
@@ -284,6 +294,26 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
 .media {
 	overflow: hidden; // clipにするとバグる
 	border-radius: 8px;
+	position: relative;
+
+	>.mediaInner {
+		width: 100%;
+		height: 100%;
+	}
+}
+
+.banner {
+	position: relative;
+}
+
+.mediaLinkForEmbed::after {
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	z-index: 1;
+	content: '';
 }
 
 :global(.pswp) {
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 5bdf7c90cf..70a42f34c3 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</div>
 					</div>
 					<div v-if="appearNote.files && appearNote.files.length > 0">
-						<MkMediaList :mediaList="appearNote.files"/>
+						<MkMediaList :mediaList="appearNote.files" :originalEntityUrl="`${url}/notes/${appearNote.id}`"/>
 					</div>
 					<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
 					<div v-if="isEnabledUrlPreview">
@@ -216,6 +216,7 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import { shouldCollapsed } from '@/scripts/collapsed.js';
 import { isEnabledUrlPreview } from '@/instance.js';
+import { url } from '@/config.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 31bda0302c..a69a625f62 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -101,7 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</div>
 				</div>
 				<div v-if="appearNote.files && appearNote.files.length > 0">
-					<MkMediaList :mediaList="appearNote.files"/>
+					<MkMediaList :mediaList="appearNote.files" :originalEntityUrl="`${url}/notes/${appearNote.id}`"/>
 				</div>
 				<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
 				<div v-if="isEnabledUrlPreview">
@@ -645,7 +645,7 @@ function loadConversation() {
 	font-size: 1.2em;
 
 	&.embeddedNote {
-		padding: 16px 32px;
+		padding: 24px 32px 16px;
 	}
 
 	&:hover > .main > .footer > .button {
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index c41811febe..c6ddb8cd7f 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -44,6 +44,7 @@ const props = defineProps<{
 }>();
 
 const mock = inject<boolean>('mock', false);
+const inEmbedPage = inject<boolean>('EMBED_PAGE', false);
 
 const emit = defineEmits<{
 	(ev: 'reactionToggled', emoji: string, newCount: number): void;
@@ -140,7 +141,7 @@ onMounted(() => {
 	if (!props.isInitial) anime();
 });
 
-if (!mock) {
+if (!mock && !inEmbedPage) {
 	useTooltip(buttonEl, async (showing) => {
 		const reactions = await misskeyApiGet('notes/reactions', {
 			noteId: props.note.id,
diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue
index 9a07826f1a..2b26e449ca 100644
--- a/packages/frontend/src/components/MkSubNoteContent.vue
+++ b/packages/frontend/src/components/MkSubNoteContent.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 	<details v-if="note.files && note.files.length > 0">
 		<summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary>
-		<MkMediaList :mediaList="note.files"/>
+		<MkMediaList :mediaList="note.files" :originalEntityUrl="`${url}/notes/${note.id}`"/>
 	</details>
 	<details v-if="note.poll">
 		<summary>{{ i18n.ts.poll }}</summary>
@@ -36,6 +36,7 @@ import MkMediaList from '@/components/MkMediaList.vue';
 import MkPoll from '@/components/MkPoll.vue';
 import { i18n } from '@/i18n.js';
 import { shouldCollapsed } from '@/scripts/collapsed.js';
+import { url } from '@/config.js';
 
 const props = defineProps<{
 	note: Misskey.entities.Note;
diff --git a/packages/frontend/src/pages/embed/note.vue b/packages/frontend/src/pages/embed/note.vue
index 3689a0d244..adf5c1f4b1 100644
--- a/packages/frontend/src/pages/embed/note.vue
+++ b/packages/frontend/src/pages/embed/note.vue
@@ -12,16 +12,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue';
+import { ref, provide } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
 import XNotFound from '@/pages/not-found.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
+import { url } from '@/config.js';
 
 const props = defineProps<{
 	noteId: string;
 }>();
 
+provide('EMBED_ORIGINAL_ENTITY_URL', `${url}/notes/${props.noteId}`);
+
 const note = ref<Misskey.entities.Note | null>(null);
 const loading = ref(true);
 
diff --git a/packages/frontend/src/pages/embed/user-timeline.vue b/packages/frontend/src/pages/embed/user-timeline.vue
index 6cb35e3e5f..360c6a8d89 100644
--- a/packages/frontend/src/pages/embed/user-timeline.vue
+++ b/packages/frontend/src/pages/embed/user-timeline.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 							</a>
 						</template>
 					</I18n>
-					<div :class="$style.sub"></div>
+					<div :class="$style.sub">{{ i18n.tsx.fromX({ x: instanceName }) }}</div>
 				</div>
 				<a :href="url" :class="$style.instanceIconLink" target="_blank" rel="noopener noreferrer">
 					<img
@@ -48,7 +48,7 @@ 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 } from '@/config.js';
+import { url, instanceName } from '@/config.js';
 
 const props = defineProps<{
 	username: string;
@@ -101,6 +101,7 @@ misskeyApi('users/show', {
 
 	.headerTitle {
 		font-weight: 700;
+		line-height: 1.1;
 
 		.sub {
 			font-size: 0.8em;
@@ -112,6 +113,7 @@ misskeyApi('users/show', {
 	.instanceIconLink {
 		display: block;
 		margin-left: auto;
+		height: 24px;
 	}
 
 	.instanceIcon {
diff --git a/packages/frontend/src/ui/embed.vue b/packages/frontend/src/ui/embed.vue
index c7d44a67bb..42ce7ff7af 100644
--- a/packages/frontend/src/ui/embed.vue
+++ b/packages/frontend/src/ui/embed.vue
@@ -71,24 +71,14 @@ const maxHeight = ref(params.get('maxHeight') ? parseInt(params.get('maxHeight')
 //#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; // プラマイ30px未満の変化は無視
-	if (resizeMessageThrottleTimer) window.clearTimeout(resizeMessageThrottleTimer);
-
+	if (Math.abs(previousHeight - height) < 1) return; // 1px未満の変化は無視
 	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!);