mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-25 17:10:43 +00:00
Merge 463bd6913d into e90ef7eba2
This commit is contained in:
commit
d2fdbb9c39
13 changed files with 90 additions and 16 deletions
|
|
@ -15,6 +15,10 @@
|
|||
### General
|
||||
- Feat: ジョブキュー管理画面からキューの一時停止/再開ができるように
|
||||
- Feat: アンテナのタイムラインから個別のノートを削除できるように
|
||||
- Enhance: リンクプレビューで大きいカード表示に対応
|
||||
- `twitter:card = summary_large_image` が設定されているサイトの場合、リンクカードを拡大して表示します(メディアの添付もあるなどの条件によっては拡大表示をしない場合があります)
|
||||
- ユーザー側で無効化することも可能です
|
||||
- Fix: コンパネからrootユーザーのパスワードをリセットしようとした際にエラーが通知されない問題を修正
|
||||
- Feat: ノート検索で投稿日時の期間を条件に加えられるように(#16035)
|
||||
- Fix: コンパネからrootユーザーのパスワードをリセットしようとした際にエラーが通知されない問題を修正
|
||||
|
||||
|
|
|
|||
|
|
@ -900,6 +900,7 @@ customCss: "カスタムCSS"
|
|||
customCssWarn: "この設定は必ず知識のある方が行ってください。不適切な設定を行うとクライアントが正常に使用できなくなる恐れがあります。"
|
||||
global: "グローバル"
|
||||
squareAvatars: "アイコンを四角形で表示"
|
||||
forceCompactUrlPreview: "URLプレビューを常にコンパクト表示にする"
|
||||
sent: "送信"
|
||||
received: "受信"
|
||||
searchResult: "検索結果"
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ type ProxyQuery = {
|
|||
static?: string;
|
||||
preview?: string;
|
||||
badge?: string;
|
||||
thumbnail?: string;
|
||||
origin?: string;
|
||||
url?: string;
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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' } : {}),
|
||||
})}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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`"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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">
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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');
|
||||
|
|
@ -998,6 +1007,7 @@ watch([
|
|||
limitWidthOfReaction,
|
||||
instanceTicker,
|
||||
squareAvatars,
|
||||
forceCompactUrlPreview,
|
||||
highlightSensitiveMedia,
|
||||
enableSeasonalScreenEffect,
|
||||
chatShowSenderName,
|
||||
|
|
|
|||
|
|
@ -339,6 +339,9 @@ export const PREF_DEF = definePreferences({
|
|||
useGroupedNotifications: {
|
||||
default: true,
|
||||
},
|
||||
forceCompactUrlPreview: {
|
||||
default: false,
|
||||
},
|
||||
dataSaver: {
|
||||
default: {
|
||||
media: false,
|
||||
|
|
|
|||
|
|
@ -3612,6 +3612,10 @@ export interface Locale extends ILocale {
|
|||
* アイコンを四角形で表示
|
||||
*/
|
||||
"squareAvatars": string;
|
||||
/**
|
||||
* URLプレビューを常にコンパクト表示にする
|
||||
*/
|
||||
"forceCompactUrlPreview": string;
|
||||
/**
|
||||
* 送信
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue