Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f290e57
feat(frontend): URLプレビューでsummary_large_image形式の大きいカード表示に対応
chan-mai May 11, 2026
b93959c
enhance: 圧縮した画像を利用するように
chan-mai May 12, 2026
5f71f1f
chore: 2件以上のリンクがある場合はsummary表示を強制するように
chan-mai May 12, 2026
6952126
fix: submoduke参照を削除
chan-mai May 12, 2026
b06460d
fix: lint
chan-mai May 12, 2026
156fd1e
fix: submoduleが何故か含まれていたので削除
chan-mai May 12, 2026
45cb9b3
chore: 強制summary表示をホバーとメディア添付ノートにも適用
chan-mai May 12, 2026
defaf2d
chore: DMも対象に
chan-mai May 12, 2026
7e08d4f
:art:
kakkokari-gtyih May 13, 2026
1f74b4b
playerの場合は強制的に縮小表示
kakkokari-gtyih May 13, 2026
c44f70f
Merge upstream/develop into feat/url-preview-large-image-card
chan-mai May 14, 2026
688e36f
Merge branch 'feat/url-preview-large-image-card' of https://github.co…
chan-mai May 14, 2026
0c6251a
Merge branch 'develop' into feat/url-preview-large-image-card
kakkokari-gtyih May 23, 2026
71b4aa7
Update Changelog
kakkokari-gtyih May 23, 2026
3f3c360
:art:
kakkokari-gtyih May 23, 2026
d39f667
fix indent
kakkokari-gtyih May 23, 2026
6bb9fb5
Merge remote-tracking branch 'msky/develop' into feat/url-preview-lar…
kakkokari-gtyih May 23, 2026
122f940
Merge branch 'develop' into feat/url-preview-large-image-card
chan-mai May 27, 2026
9620b04
Merge branch 'develop' into feat/url-preview-large-image-card
kakkokari-gtyih May 29, 2026
4778287
Merge branch 'develop' into feat/url-preview-large-image-card
chan-mai May 30, 2026
8898b5d
Merge remote-tracking branch 'msky/develop' into feat/url-preview-lar…
kakkokari-gtyih Jun 3, 2026
b5b8e91
Merge branch 'develop' into feat/url-preview-large-image-card
chan-mai Jun 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
### General
- Feat: ジョブキュー管理画面からキューの一時停止/再開ができるように
- Feat: アンテナのタイムラインから個別のノートを削除できるように
- Enhance: リンクプレビューで大きいカード表示に対応
- `twitter:card = summary_large_image` が設定されているサイトの場合、リンクカードを拡大して表示します(メディアの添付もあるなどの条件によっては拡大表示をしない場合があります)
- ユーザー側で無効化することも可能です
- Fix: コンパネからrootユーザーのパスワードをリセットしようとした際にエラーが通知されない問題を修正
- Feat: ノート検索で投稿日時の期間を条件に加えられるように(#16035)

Expand Down
1 change: 1 addition & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,7 @@ customCss: "カスタムCSS"
customCssWarn: "この設定は必ず知識のある方が行ってください。不適切な設定を行うとクライアントが正常に使用できなくなる恐れがあります。"
global: "グローバル"
squareAvatars: "アイコンを四角形で表示"
forceCompactUrlPreview: "URLプレビューを常にコンパクト表示にする"
sent: "送信"
received: "受信"
searchResult: "検索結果"
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"@misskey-dev/emoji-assets": "17.0.3",
"@misskey-dev/emoji-data": "17.0.3",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.3.0",
"@misskey-dev/summaly": "5.4.0",
"@napi-rs/canvas": "1.0.0",
"@nestjs/common": "11.1.24",
"@nestjs/core": "11.1.24",
Expand Down
7 changes: 6 additions & 1 deletion packages/backend/src/server/file/FileServerProxyHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type ProxyQuery = {
static?: string;
preview?: string;
badge?: string;
thumbnail?: string;
origin?: string;
url?: string;
};
Expand Down Expand Up @@ -131,7 +132,7 @@ export class FileServerProxyHandler {
): Promise<IImageStreamable> {
const query = request.query;

const requiresImageConversion = 'emoji' in query || 'avatar' in query || 'static' in query || 'preview' in query || 'badge' in query;
const requiresImageConversion = 'emoji' in query || 'avatar' in query || 'static' in query || 'preview' in query || 'badge' in query || 'thumbnail' in query;
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image-with-bmp');
if (requiresImageConversion && !isConvertibleImage) {
throw new StatusError('Unexpected mime', 404);
Expand All @@ -141,6 +142,10 @@ export class FileServerProxyHandler {
return this.processEmojiOrAvatar(file, query);
}

if ('thumbnail' in query) {
return this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 1280, 720);
}

if ('static' in query) {
return this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend-embed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
"devDependencies": {
"@misskey-dev/emoji-assets": "17.0.3",
"@misskey-dev/summaly": "5.3.0",
"@misskey-dev/summaly": "5.4.0",
"@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.9",
Expand Down
8 changes: 6 additions & 2 deletions packages/frontend-shared/js/media-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class MediaProxy {
this.url = url;
}

public getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string {
public getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar' | 'thumbnail', mustOrigin = false, noFallback = false): string {
const localProxy = `${this.url}/proxy`;
let _imageUrl = imageUrl;

Expand All @@ -26,11 +26,15 @@ export class MediaProxy {

return `${mustOrigin ? localProxy : this.serverMetadata.mediaProxy}/${
type === 'preview' ? 'preview.webp'
: type === 'thumbnail' ? 'thumbnail.webp'
: 'image.webp'
}?${query({
url: _imageUrl,
...(!noFallback ? { 'fallback': '1' } : {}),
...(type ? { [type]: '1' } : {}),
// thumbnail を理解しない外部プロキシでも GIF アニメ解除と縮小がかかるよう static=1 をフォールバックとして併用
...(type === 'thumbnail'
? { thumbnail: '1', static: '1' }
: type ? { [type]: '1' } : {}),
...(mustOrigin ? { origin: '1' } : {}),
})}`;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
},
"devDependencies": {
"@misskey-dev/emoji-assets": "17.0.3",
"@misskey-dev/summaly": "5.3.0",
"@misskey-dev/summaly": "5.4.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/pluginutils": "5.4.0",
"@storybook/addon-essentials": "8.6.18",
Expand Down
5 changes: 2 additions & 3 deletions packages/frontend/src/components/MkLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ import { useTooltip } from '@/composables/use-tooltip.js';
import * as os from '@/os.js';
import { isEnabledUrlPreview } from '@/utility/url-preview.js';

const props = withDefaults(defineProps<{
const props = defineProps<{
url: string;
rel?: null | string;
navigationBehavior?: MkABehavior;
}>(), {
});
}>();

const maybeRelativeUrl = maybeMakeRelative(props.url, local);
const self = maybeRelativeUrl !== props.url;
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/MkNote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.poll"
/>
<div v-if="isEnabledUrlPreview">
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :forceCompactCard="(urls?.length ?? 0) >= 2 || (appearNote.files != null && appearNote.files.length > 0)" :class="$style.urlPreview"/>
</div>
<div v-if="appearNote.renoteId" :class="$style.quote"><MkNoteSimple :note="appearNote?.renote ?? null" :class="$style.quoteNote"/></div>
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/MkNoteDetailed.vue
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.poll"
/>
<div v-if="isEnabledUrlPreview">
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :forceCompactCard="(urls?.length ?? 0) >= 2 || (appearNote.files != null && appearNote.files.length > 0)" style="margin-top: 6px;"/>
</div>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
</div>
Expand Down
56 changes: 50 additions & 6 deletions packages/frontend/src/components/MkUrlPreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->

<template>
<template v-if="player.url && playerEnabled">
<div v-if="player.url && playerEnabled">
<div
:class="$style.player"
:style="player.width ? `padding: ${(player.height || 0) / player.width * 100}% 0 0` : `padding: ${(player.height || 0)}px 0 0`"
Expand All @@ -25,8 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-x"></i> {{ i18n.ts.disablePlayer }}
</MkButton>
</div>
</template>
<template v-else-if="tweetId && tweetExpanded">
</div>
<div v-else-if="tweetId && tweetExpanded">
<div ref="twitter">
<iframe
ref="tweet"
Expand All @@ -42,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-x"></i> {{ i18n.ts.close }}
</MkButton>
</div>
</template>
</div>
<div v-else>
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="maybeRelativeUrl" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="prefer.s.dataSaver.urlPreviewThumbnail ? '' : { backgroundImage: `url('${thumbnail}')` }">
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact, [$style.large]: isLargeImage }]" :[attr]="maybeRelativeUrl" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="displayThumbnail ? { backgroundImage: `url('${displayThumbnail}')` } : ''">
</div>
<article :class="$style.body">
<header :class="$style.header">
Expand Down Expand Up @@ -101,10 +101,12 @@ const props = withDefaults(defineProps<{
detail?: boolean;
compact?: boolean;
showActions?: boolean;
forceCompactCard?: boolean;
}>(), {
detail: false,
compact: false,
showActions: true,
forceCompactCard: false,
});

const MOBILE_THRESHOLD = 500;
Expand All @@ -119,9 +121,33 @@ 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 thumbnailStyle = computed(() => summalyResult.value?.thumbnailStyle ?? null);
const icon = computed(() => summalyResult.value?.icon ?? null);
const sitename = computed(() => summalyResult.value?.sitename ?? null);
const sensitive = computed(() => summalyResult.value?.sensitive ?? false);
const isLargeImage = computed(() =>
thumbnail.value != null &&
tweetId.value == null &&
!sensitive.value &&
thumbnailStyle.value === 'summary_large_image' &&
!prefer.s.forceCompactUrlPreview &&
!props.forceCompactCard,
);
const displayThumbnail = computed(() => {
if (!thumbnail.value || prefer.s.dataSaver.urlPreviewThumbnail) return null;
if (!isLargeImage.value) return thumbnail.value;
// large card: preview=1 を thumbnail=1 (1280x720, GIFアニメ解除) に切り替える
// thumbnail を理解しない外部プロキシでも GIF アニメが止まるよう static=1 もフォールバックとして併記
try {
const u = new URL(thumbnail.value);
u.searchParams.delete('preview');
u.searchParams.set('thumbnail', '1');
u.searchParams.set('static', '1');
return u.toString();
} catch {
return thumbnail.value;
}
});
const player = computed(() => summalyResult.value?.player ?? { url: null, width: null, height: null });
const playerEnabled = ref(false);
const tweetId = ref<string | null>(null);
Expand Down Expand Up @@ -259,6 +285,24 @@ onUnmounted(() => {
}
}
}

&.large {
> .thumbnail {
position: relative;
width: 100%;
height: auto;
aspect-ratio: 1.91;

& + .body {
left: 0;
width: 100%;
}
}

> .body {
padding: 16px;
}
}
}

.thumbnail {
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/MkUrlPreviewPopup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')">
<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url" :showActions="false"/>
<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url" :showActions="false" forceCompactCard/>
</Transition>
</div>
</template>
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/pages/chat/XMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
/>
<MkMediaList v-if="message.file" :mediaList="[message.file]"/>
</MkFukidashi>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" style="margin: 8px 0;"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :forceCompactCard="urls.length >= 2 || message.file != null" style="margin: 8px 0;"/>
<div :class="$style.footer">
<button class="_textButton" style="color: currentColor;" @click="showMenu"><i class="ti ti-dots-circle-horizontal"></i></button>
<MkTime :class="$style.time" :time="message.createdAt"/>
Expand Down
10 changes: 10 additions & 0 deletions packages/frontend/src/pages/settings/preferences.vue
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSelect>
</MkPreferenceContainer>
</SearchMarker>

<SearchMarker :keywords="['urlpreview', 'link', 'preview', 'card', 'large', 'compact']">
<MkPreferenceContainer k="forceCompactUrlPreview">
<MkSwitch v-model="forceCompactUrlPreview" :disabled="!instance.enableUrlPreview || dataSaver.disableUrlPreview">
<template #label><SearchLabel>{{ i18n.ts.forceCompactUrlPreview }}</SearchLabel></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
</div>
</div>
</MkFolder>
Expand Down Expand Up @@ -936,6 +944,7 @@ const reactionsDisplaySize = prefer.model('reactionsDisplaySize');
const limitWidthOfReaction = prefer.model('limitWidthOfReaction');
const squareAvatars = prefer.model('squareAvatars');
const enableSeasonalScreenEffect = prefer.model('enableSeasonalScreenEffect');
const forceCompactUrlPreview = prefer.model('forceCompactUrlPreview');
const showAvatarDecorations = prefer.model('showAvatarDecorations');
const nsfw = prefer.model('nsfw');
const emojiStyle = prefer.model('emojiStyle');
Expand Down Expand Up @@ -998,6 +1007,7 @@ watch([
limitWidthOfReaction,
instanceTicker,
squareAvatars,
forceCompactUrlPreview,
highlightSensitiveMedia,
enableSeasonalScreenEffect,
chatShowSenderName,
Expand Down
3 changes: 3 additions & 0 deletions packages/frontend/src/preferences/def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,9 @@ export const PREF_DEF = definePreferences({
useGroupedNotifications: {
default: true,
},
forceCompactUrlPreview: {
default: false,
},
dataSaver: {
default: {
media: false,
Expand Down
4 changes: 4 additions & 0 deletions packages/i18n/src/autogen/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3612,6 +3612,10 @@ export interface Locale extends ILocale {
* アイコンを四角形で表示
*/
"squareAvatars": string;
/**
* URLプレビューを常にコンパクト表示にする
*/
"forceCompactUrlPreview": string;
/**
* 送信
*/
Expand Down
27 changes: 10 additions & 17 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading