fix(frontend): URLプレイヤーウィンドウでiframeが読み込まれるまでの間にinvalid urlと表示される問題を修正 (#17417)

* fix(frontend): URLプレイヤーウィンドウでiframeが読み込まれるまでの間にinvalid urlと表示される問題を修正

* Update Changelog

* fix

* fix lint

* Update Changelog

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
かっこかり 2026-05-28 21:32:17 +09:00 committed by GitHub
commit 7e0eb61495
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 71 additions and 49 deletions

View file

@ -5,6 +5,7 @@
- Feat: アンテナのタイムラインから個別のノートを削除できるように
### Client
- Fix: URLプレビューのプレイヤーをウィンドウで開いたとき、プレイヤーが読み込まれるまでの間 `Invalid URL` と表示される問題を修正
- Fix: 一部の実績が正しく表示されない問題を修正
- Fix: アクセストークン発行時のダイアログのタイトルが「確認コード」となっているのを修正

View file

@ -83,7 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue';
import { computed, defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue';
import { url as local } from '@@/js/config.js';
import { versatileLang } from '@@/js/intl-const.js';
import type { SummalyResult } from '@misskey-dev/summaly';
@ -115,17 +115,14 @@ const self = maybeRelativeUrl !== props.url;
const attr = self ? 'to' : 'href';
const target = self ? null : '_blank';
const fetching = ref(true);
const title = ref<string | null>(null);
const description = ref<string | null>(null);
const thumbnail = ref<string | null>(null);
const icon = ref<string | null>(null);
const sitename = ref<string | null>(null);
const sensitive = ref<boolean>(false);
const player = ref({
url: null,
width: null,
height: null,
} as SummalyResult['player']);
const summalyResult = ref<SummalyResult | null>(null);
const title = computed(() => summalyResult.value?.title ?? null);
const description = computed(() => summalyResult.value?.description ?? null);
const thumbnail = computed(() => summalyResult.value?.thumbnail ?? null);
const icon = computed(() => summalyResult.value?.icon ?? null);
const sitename = computed(() => summalyResult.value?.sitename ?? null);
const sensitive = computed(() => summalyResult.value?.sensitive ?? false);
const player = computed(() => summalyResult.value?.player ?? { url: null, width: null, height: null });
const playerEnabled = ref(false);
const tweetId = ref<string | null>(null);
const tweetExpanded = ref(props.detail);
@ -137,7 +134,7 @@ onDeactivated(() => {
playerEnabled.value = false;
});
const requestUrl = new URL(props.url);
const requestUrl = new URL(props.url, window.location.href);
if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url');
if (requestUrl.hostname === 'twitter.com' || requestUrl.hostname === 'mobile.twitter.com' || requestUrl.hostname === 'x.com' || requestUrl.hostname === 'mobile.x.com') {
@ -172,13 +169,7 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa
fetching.value = false;
unknownUrl.value = false;
title.value = info.title;
description.value = info.description;
thumbnail.value = info.thumbnail;
icon.value = info.icon;
sitename.value = info.sitename;
player.value = info.player;
sensitive.value = info.sensitive ?? false;
summalyResult.value = info;
});
function adjustTweetHeight(message: MessageEvent) {
@ -191,8 +182,10 @@ function adjustTweetHeight(message: MessageEvent) {
}
function openPlayer(): void {
if (!summalyResult.value) return;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), {
url: requestUrl.href,
urlOrSummalyResult: summalyResult.value,
}, {
closed: () => {
dispose();

View file

@ -11,14 +11,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div class="poamfof">
<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
<div v-if="player.url && (player.url.startsWith('http://') || player.url.startsWith('https://'))" class="player">
<iframe v-if="!fetching" :src="transformPlayerUrl(player.url)" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
</div>
<span v-else>invalid url</span>
</Transition>
<MkLoading v-if="fetching"/>
<MkError v-else-if="!player.url" @retry="ytFetch()"/>
<MkLoading v-if="fetching || !iframeLoaded"/>
<div v-if="!fetching && player?.url != null" class="player">
<iframe
:src="transformPlayerUrl(player.url)"
frameborder="0"
:allow="player.allow.join('; ')"
allowfullscreen
:style="{ opacity: iframeLoaded ? 1 : 0, transition: 'opacity 0.3s' }"
@load="onFrameLoad"
></iframe>
</div>
<MkError v-else @retry="ytFetch()"/>
</div>
</MkWindow>
</template>
@ -28,41 +32,65 @@ import { ref } from 'vue';
import { versatileLang } from '@@/js/intl-const.js';
import MkWindow from '@/components/MkWindow.vue';
import { transformPlayerUrl } from '@/utility/url-preview.js';
import { prefer } from '@/preferences.js';
import type { SummalyResult } from '@misskey-dev/summaly';
const props = defineProps<{
url: string;
urlOrSummalyResult: string | SummalyResult;
}>();
const emit = defineEmits<{
(ev: 'closed'): void;
}>();
const requestUrl = new URL(props.url);
if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url');
const fetching = ref(true);
const iframeLoaded = ref(false);
const title = ref<string | null>(null);
const player = ref({
url: null as string | null,
width: null,
height: null,
});
const player = ref<SummalyResult['player'] | null>(null);
const ytFetch = (): void => {
async function ytFetch() {
title.value = null;
player.value = null;
fetching.value = true;
window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`).then(res => {
res.json().then(info => {
if (info.url == null) return;
title.value = info.title;
iframeLoaded.value = false;
let info: SummalyResult;
if (typeof props.urlOrSummalyResult === 'string') {
const requestUrl = new URL(props.urlOrSummalyResult, window.location.href);
if (requestUrl.protocol !== 'http:' && requestUrl.protocol !== 'https:') {
// Invalid URL
fetching.value = false;
player.value = info.player;
});
});
return;
}
const res = await window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`);
info = await res.json() as SummalyResult;
} else {
info = props.urlOrSummalyResult;
}
if (info.url == null || info.player?.url == null) {
// No URL or player info
fetching.value = false;
return;
}
if (!info.player.url.startsWith('https://') && !info.player.url.startsWith('http://')) {
// Invalid player URL
fetching.value = false;
return;
}
title.value = info.title;
player.value = info.player;
fetching.value = false;
}
const onFrameLoad = (): void => {
iframeLoaded.value = true;
};
ytFetch();
void ytFetch();
</script>
<style lang="scss">