enhance: Proxy custom emojis to reduce image size and accelerate the frontend (#9431)
* fix(server): /emoji to accept `@.` host expression
* fix(client): use MkEmoji for custom emoji in MkEmojiPicker
* change convertToWebp
* nanka iroiro
* remove
* fix
* nearLosslessは労多くして益少なしなのでやめる
* do not cleanup tmp for development
* update sharp.js to 0.31.3
* mixed: true
* fix MkAutocomplete of 912791b3ab
* clean up
* https://github.com/misskey-dev/misskey/pull/9431#discussion_r1059215943
This commit is contained in:
@@ -16,12 +16,12 @@
|
||||
</li>
|
||||
</ol>
|
||||
<ol v-else-if="emojis.length > 0" ref="suggests" class="emojis">
|
||||
<li v-for="emoji in emojis" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
|
||||
<span v-if="emoji.isCustomEmoji" class="emoji"><img :src="`/emoji/${emoji.name}.webp`" :alt="emoji.emoji"/></span>
|
||||
<span v-else-if="defaultStore.state.emojiStyle != 'native'" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
|
||||
<span v-else class="emoji">{{ emoji.emoji }}</span>
|
||||
<li v-for="emoji in emojis" tabindex="-1" :key="emoji.emoji" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
|
||||
<div class="emoji">
|
||||
<MkEmoji :emoji="emoji.emoji" />
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
|
||||
<span class="name" v-html="emoji.name.replace(q ?? '', `<b>${q}</b>`)"></span>
|
||||
<span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span>
|
||||
</li>
|
||||
</ol>
|
||||
@@ -37,7 +37,6 @@
|
||||
import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||
import contains from '@/scripts/contains';
|
||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import { acct } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import { MFM_TAGS } from '@/scripts/mfm-tags';
|
||||
@@ -49,9 +48,13 @@ import { i18n } from '@/i18n';
|
||||
type EmojiDef = {
|
||||
emoji: string;
|
||||
name: string;
|
||||
url: string;
|
||||
aliasOf?: string;
|
||||
url?: string;
|
||||
isCustomEmoji?: boolean;
|
||||
} | {
|
||||
emoji: string;
|
||||
name: string;
|
||||
aliasOf?: string;
|
||||
isCustomEmoji?: true;
|
||||
};
|
||||
|
||||
const lib = emojilist.filter(x => x.category !== 'flags');
|
||||
@@ -87,7 +90,6 @@ for (const x of customEmojis) {
|
||||
emojiDefinitions.push({
|
||||
name: x.name,
|
||||
emoji: `:${x.name}:`,
|
||||
url: x.url,
|
||||
isCustomEmoji: true,
|
||||
});
|
||||
|
||||
@@ -97,7 +99,6 @@ for (const x of customEmojis) {
|
||||
name: alias,
|
||||
aliasOf: x.name,
|
||||
emoji: `:${x.name}:`,
|
||||
url: x.url,
|
||||
isCustomEmoji: true,
|
||||
});
|
||||
}
|
||||
@@ -452,14 +453,20 @@ onBeforeUnmount(() => {
|
||||
> .emojis > li {
|
||||
|
||||
.emoji {
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
margin: 0 4px 0 0;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
|
||||
> img {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
vertical-align: bottom;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.alias {
|
||||
|
@@ -81,7 +81,6 @@ import { ref, computed, watch, onMounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XSection from '@/components/MkEmojiPicker.section.vue';
|
||||
import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import Ripple from '@/components/MkRipple.vue';
|
||||
import * as os from '@/os';
|
||||
import { isTouchUsing } from '@/scripts/touch';
|
||||
|
@@ -23,7 +23,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy';
|
||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
|
@@ -12,7 +12,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy';
|
||||
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
|
||||
import { acct, userPage } from '@/filters/user';
|
||||
import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
|
||||
|
@@ -7,7 +7,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy';
|
||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base';
|
||||
import { defaultStore } from '@/store';
|
||||
import { getEmojiName } from '@/scripts/emojilist';
|
||||
|
@@ -21,7 +21,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy';
|
||||
import { notePage } from '@/filters/note';
|
||||
import * as os from '@/os';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
|
@@ -1,19 +0,0 @@
|
||||
import { url as instanceUrl } from '@/config';
|
||||
import * as url from '@/scripts/url';
|
||||
|
||||
export function getStaticImageUrl(baseUrl: string): string {
|
||||
const u = new URL(baseUrl);
|
||||
if (u.href.startsWith(`${instanceUrl}/proxy/`)) {
|
||||
// もう既にproxyっぽそうだったらsearchParams付けるだけ
|
||||
u.searchParams.set('static', '1');
|
||||
return u.href;
|
||||
}
|
||||
|
||||
// 拡張子がないとキャッシュしてくれないCDNがあるのでダミーの名前を指定する
|
||||
const dummy = `${encodeURIComponent(`${u.host}${u.pathname}`)}.webp`;
|
||||
|
||||
return `${instanceUrl}/proxy/${dummy}?${url.query({
|
||||
url: u.href,
|
||||
static: '1',
|
||||
})}`;
|
||||
}
|
@@ -1,7 +1,15 @@
|
||||
import { query } from '@/scripts/url';
|
||||
import { query, appendQuery } from '@/scripts/url';
|
||||
import { url } from '@/config';
|
||||
|
||||
export function getProxiedImageUrl(imageUrl: string, type?: 'preview'): string {
|
||||
if (imageUrl.startsWith(`${url}/proxy/`) || imageUrl.startsWith('/proxy/')) {
|
||||
// もう既にproxyっぽそうだったらsearchParams付けるだけ
|
||||
return appendQuery(imageUrl, query({
|
||||
fallback: '1',
|
||||
...(type ? { [type]: '1' } : {}),
|
||||
}));
|
||||
}
|
||||
|
||||
return `${url}/proxy/image.webp?${query({
|
||||
url: imageUrl,
|
||||
fallback: '1',
|
||||
@@ -13,3 +21,27 @@ export function getProxiedImageUrlNullable(imageUrl: string | null | undefined,
|
||||
if (imageUrl == null) return null;
|
||||
return getProxiedImageUrl(imageUrl, type);
|
||||
}
|
||||
|
||||
export function getStaticImageUrl(baseUrl: string): string {
|
||||
const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url);
|
||||
|
||||
if (u.href.startsWith(`${url}/proxy/`)) {
|
||||
// もう既にproxyっぽそうだったらsearchParams付けるだけ
|
||||
u.searchParams.set('static', '1');
|
||||
return u.href;
|
||||
}
|
||||
|
||||
if (u.href.startsWith(`${url}/emoji/`)) {
|
||||
// もう既にemojiっぽそうだったらsearchParams付けるだけ
|
||||
u.searchParams.set('static', '1');
|
||||
return u.href;
|
||||
}
|
||||
|
||||
// 拡張子がないとキャッシュしてくれないCDNがあるのでダミーの名前を指定する
|
||||
const dummy = `${encodeURIComponent(`${u.host}${u.pathname}`)}.webp`;
|
||||
|
||||
return `${url}/proxy/${dummy}?${query({
|
||||
url: u.href,
|
||||
static: '1',
|
||||
})}`;
|
||||
}
|
||||
|
@@ -1,3 +1,8 @@
|
||||
/* objを検査して
|
||||
* 1. 配列に何も入っていない時はクエリを付けない
|
||||
* 2. プロパティがundefinedの時はクエリを付けない
|
||||
* (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない)
|
||||
*/
|
||||
export function query(obj: Record<string, any>): string {
|
||||
const params = Object.entries(obj)
|
||||
.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
|
||||
|
@@ -20,7 +20,7 @@ import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
||||
import { GetFormResultType } from '@/scripts/form';
|
||||
import { stream } from '@/stream';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import { getStaticImageUrl } from '@/scripts/media-proxy';
|
||||
import * as os from '@/os';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import { defaultStore } from '@/store';
|
||||
|
Reference in New Issue
Block a user