Merge tag '13.14.2' into io

This commit is contained in:
まっちゃとーにゅ
2023-07-27 18:21:05 +09:00
46 changed files with 2221 additions and 326 deletions

View File

@@ -108,8 +108,7 @@ function waitForDecode() {
.then(() => {
loaded = true;
}, error => {
console.error('Error occurred during decoding image', img.value, error);
throw Error(error);
console.log('Error occurred during decoding image', img.value, error);
});
} else {
loaded = false;

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div ref="root">
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
<div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container">
<div
@@ -22,8 +22,43 @@
</div>
</template>
<script lang="ts">
/**
* アスペクト比算出のためにHTMLElement.clientWidthを使うが、
* 大変重たいのでコンテナ要素とメディアリスト幅のペアをキャッシュする
* (タイムラインごとにスクロールコンテナが存在する前提だが……)
*/
const widthCache = new Map<Element, number>();
/**
* コンテナ要素がリサイズされたらキャッシュを削除する
*/
const ro = new ResizeObserver(entries => {
for (const entry of entries) {
widthCache.delete(entry.target);
}
});
async function getClientWidthWithCache(targetEl: HTMLElement, containerEl: HTMLElement, count = 0) {
if (_DEV_) console.log('getClientWidthWithCache', { targetEl, containerEl, count, cache: widthCache.get(containerEl) });
if (widthCache.has(containerEl)) return widthCache.get(containerEl)!;
const width = targetEl.clientWidth;
if (count <= 10 && width < 64) {
// widthが64未満はおかしいのでリトライする
await new Promise(resolve => setTimeout(resolve, 50));
return getClientWidthWithCache(targetEl, containerEl, count + 1);
}
widthCache.set(containerEl, width);
ro.observe(containerEl);
return width;
}
</script>
<script lang="ts" setup>
import { onMounted, watch, shallowRef } from 'vue';
import { onMounted, onUnmounted, shallowRef } from 'vue';
import * as misskey from 'misskey-js';
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe';
@@ -34,19 +69,33 @@ import XVideo from '@/components/MkMediaVideo.vue';
import * as os from '@/os';
import { FILE_TYPE_BROWSERSAFE } from '@/const';
import { defaultStore } from '@/store';
import { getScrollContainer, getBodyScrollHeight } from '@/scripts/scroll';
const props = defineProps<{
mediaList: misskey.entities.DriveFile[];
raw?: boolean;
}>();
const root = shallowRef<HTMLDivElement>();
const container = shallowRef<HTMLElement | null | undefined>(undefined);
const gallery = shallowRef<HTMLDivElement>();
const pswpZIndex = os.claimZIndex('middle');
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
const count = $computed(() => props.mediaList.filter(media => previewable(media)).length);
let lightbox: PhotoSwipeLightbox | null;
function calcAspectRatio() {
if (!gallery.value) return;
const popstateHandler = (): void => {
if (lightbox.pswp && lightbox.pswp.isOpen === true) {
lightbox.pswp.close();
}
};
/**
* アスペクト比をmediaListWithOneImageAppearanceに基づいていい感じに調整する
* aspect-ratioではなくheightを使う
*/
async function calcAspectRatio() {
if (!gallery.value || !root.value) return;
let img = props.mediaList[0];
@@ -55,29 +104,47 @@ function calcAspectRatio() {
return;
}
// アスペクト比上限設定では、横長の場合は高さを縮小させる
const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
if (!container.value) container.value = getScrollContainer(root.value);
const width = container.value ? await getClientWidthWithCache(root.value, container.value) : root.value.clientWidth;
const heightMin = (ratio: number) => {
const imgResizeRatio = width / img.properties.width;
const imgDrawHeight = img.properties.height * imgResizeRatio;
const maxHeight = width * ratio;
const height = Math.min(imgDrawHeight, maxHeight);
if (_DEV_) console.log('Image height calculated:', { width, properties: img.properties, imgResizeRatio, imgDrawHeight, maxHeight, height });
return `${height}px`;
};
switch (defaultStore.state.mediaListWithOneImageAppearance) {
case '16_9':
gallery.value.style.aspectRatio = ratioMax(16 / 9);
gallery.value.style.height = heightMin(9 / 16);
break;
case '1_1':
gallery.value.style.aspectRatio = ratioMax(1);
gallery.value.style.height = heightMin(1);
break;
case '2_3':
gallery.value.style.aspectRatio = ratioMax(2 / 3);
gallery.value.style.height = heightMin(3 / 2);
break;
default:
gallery.value.style.aspectRatio = '';
default: {
const maxHeight = Math.max(64, (container.value ? container.value.clientHeight : getBodyScrollHeight()) * 0.5 || 360);
if (width === 0 || !maxHeight) return;
const imgResizeRatio = width / img.properties.width;
const imgDrawHeight = img.properties.height * imgResizeRatio;
gallery.value.style.height = `${Math.max(64, Math.min(imgDrawHeight, maxHeight))}px`;
gallery.value.style.minHeight = 'initial';
gallery.value.style.maxHeight = 'initial';
break;
}
}
gallery.value.style.aspectRatio = 'initial';
}
watch([defaultStore.reactiveState.mediaListWithOneImageAppearance, gallery], () => calcAspectRatio());
onMounted(() => {
const lightbox = new PhotoSwipeLightbox({
calcAspectRatio();
lightbox = new PhotoSwipeLightbox({
dataSource: props.mediaList
.filter(media => {
if (media.type === 'image/svg+xml') return true; // svgのwebpublicはpngなのでtrue
@@ -161,12 +228,7 @@ onMounted(() => {
lightbox.init();
window.addEventListener('popstate', () => {
if (lightbox.pswp && lightbox.pswp.isOpen === true) {
lightbox.pswp.close();
return;
}
});
window.addEventListener('popstate', popstateHandler);
lightbox.on('beforeOpen', () => {
history.pushState(null, '', '#pswp');
@@ -179,6 +241,12 @@ onMounted(() => {
});
});
onUnmounted(() => {
window.removeEventListener('popstate', popstateHandler);
lightbox?.destroy();
lightbox = null;
});
const previewable = (file: misskey.entities.DriveFile): boolean => {
if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue
// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
@@ -203,7 +271,7 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
&.n1 {
grid-template-rows: 1fr;
// default (expand)
// default but fallback (expand)
min-height: 64px;
max-height: clamp(
64px,
@@ -212,20 +280,20 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
);
&.n116_9 {
min-height: none;
max-height: none;
min-height: initial;
max-height: initial;
aspect-ratio: 16 / 9; // fallback
}
&.n11_1{
min-height: none;
max-height: none;
min-height: initial;
max-height: initial;
aspect-ratio: 1 / 1; // fallback
}
&.n12_3 {
min-height: none;
max-height: none;
min-height: initial;
max-height: initial;
aspect-ratio: 2 / 3; // fallback
}
}

View File

@@ -431,6 +431,7 @@ defineExpose({
margin: auto;
padding: 32px;
display: flex;
overflow: auto;
@media (max-width: 500px) {
padding: 16px;

View File

@@ -341,6 +341,7 @@ function reply(viaKeyboard = false): void {
pleaseLogin();
os.post({
reply: appearNote,
channel: appearNote.channel,
animation: !viaKeyboard,
}, () => {
focus();
@@ -758,6 +759,7 @@ function showReactions(): void {
padding: 16px;
border: dashed 1px var(--renote);
border-radius: 8px;
overflow: clip;
}
.channel {

View File

@@ -320,6 +320,7 @@ function reply(viaKeyboard = false): void {
showMovedDialog();
os.post({
reply: appearNote,
channel: appearNote.channel,
animation: !viaKeyboard,
}, () => {
focus();
@@ -595,6 +596,7 @@ if (appearNote.replyId) {
padding: 16px;
border: dashed 1px var(--renote);
border-radius: 8px;
overflow: clip;
}
.channel {

View File

@@ -37,7 +37,6 @@ const showContent = $ref(false);
display: flex;
margin: 0;
padding: 0;
overflow: clip;
font-size: 0.95em;
}

View File

@@ -541,7 +541,7 @@ function onCompositionEnd(ev: CompositionEvent) {
}
async function onPaste(ev: ClipboardEvent) {
for (const { item, i } of Array.from(ev.clipboardData.items).map((item, i) => ({ item, i }))) {
for (const { item, i } of Array.from(ev.clipboardData.items, (item, i) => ({ item, i }))) {
if (item.kind === 'file') {
const file = item.getAsFile();
const lio = file.name.lastIndexOf('.');
@@ -908,7 +908,6 @@ defineExpose({
display: flex;
flex-wrap: nowrap;
gap: 4px;
margin-bottom: -10px;
}
.headerLeft {
@@ -1026,7 +1025,7 @@ defineExpose({
}
.targetNote {
padding: 10px 20px 16px 20px;
padding: 0 20px 16px 20px;
}
.withQuote {

View File

@@ -16,8 +16,8 @@ import tinycolor from 'tinycolor2';
const loaded = !!window.TagCanvas;
const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
const computedStyle = getComputedStyle(document.documentElement);
const idForCanvas = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
const idForTags = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
const idForCanvas = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
const idForTags = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
let available = $ref(false);
let rootEl = $shallowRef<HTMLElement | null>(null);
let canvasEl = $shallowRef<HTMLCanvasElement | null>(null);

View File

@@ -38,14 +38,6 @@ const prepend = note => {
}
};
const onUserAdded = () => {
tlComponent.pagingComponent?.reload();
};
const onUserRemoved = () => {
tlComponent.pagingComponent?.reload();
};
let endpoint;
let query;
let connection;
@@ -136,8 +128,6 @@ if (props.src === 'antenna') {
listId: props.list,
});
connection.on('note', prepend);
connection.on('userAdded', onUserAdded);
connection.on('userRemoved', onUserRemoved);
} else if (props.src === 'channel') {
endpoint = 'channels/timeline';
query = {

View File

@@ -110,7 +110,7 @@ const selectAll = () => {
if (selectedEmojis.value.length > 0) {
selectedEmojis.value = [];
} else {
selectedEmojis.value = emojisPaginationComponent.value.items.map(item => item.id);
selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id);
}
};

View File

@@ -166,7 +166,7 @@ const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(computed(() => page ? {
title: computed(() => page.title || page.name),
title: page.title || page.name,
avatar: page.user,
path: `/@${page.user.username}/pages/${page.name}`,
share: {

View File

@@ -258,6 +258,7 @@ watch([
showGapBetweenNotesInTimeline,
instanceTicker,
overridedDeviceKind,
mediaListWithOneImageAppearance,
], async () => {
await reloadAsk();
});

View File

@@ -32,7 +32,7 @@
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted } from 'vue';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue';
@@ -93,15 +93,6 @@ const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
'darkTheme',
'syncDeviceDarkMode',
'plugins',
'mediaVolume',
'sound_masterVolume',
'sound_note',
'sound_noteMy',
'sound_notification',
'sound_chat',
'sound_chatBg',
'sound_antenna',
'sound_channel',
];
const scope = ['clientPreferencesProfiles'];
@@ -125,18 +116,18 @@ type Profile = {
const connection = $i && useStream().useChannel('main');
let profiles = $ref<Record<string, Profile> | null>(null);
const profiles = ref<Record<string, Profile> | null>(null);
os.api('i/registry/get-all', { scope })
.then(res => {
profiles = res || {};
profiles.value = res || {};
});
function isObject(value: unknown): value is Record<string, unknown> {
return value != null && typeof value === 'object' && !Array.isArray(value);
}
function validate(profile: unknown): void {
function validate(profile: any): void {
if (!isObject(profile)) throw new Error('not an object');
// Check if unnecessary properties exist
@@ -147,9 +138,9 @@ function validate(profile: unknown): void {
// Check if createdAt and updatedAt is Date
// https://zenn.dev/lollipop_onl/articles/eoz-judge-js-invalid-date
if (!profile.createdAt || Number.isNaN(new Date(profile.createdAt).getTime())) throw new Error('createdAt is falsy or not Date');
if (!profile.createdAt || Number.isNaN(new Date(profile.createdAt as any).getTime())) throw new Error('createdAt is falsy or not Date');
if (profile.updatedAt) {
if (Number.isNaN(new Date(profile.updatedAt).getTime())) {
if (Number.isNaN(new Date(profile.updatedAt as any).getTime())) {
throw new Error('updatedAt is not Date');
}
} else if (profile.updatedAt !== null) {
@@ -181,14 +172,14 @@ function getSettings(): Profile['settings'] {
}
async function saveNew(): Promise<void> {
if (!profiles) return;
if (!profiles.value) return;
const { canceled, result: name } = await os.inputText({
title: ts._preferencesBackups.inputName,
});
if (canceled) return;
if (Object.values(profiles).some(x => x.name === name)) {
if (Object.values(profiles.value).some(x => x.name === name)) {
return os.alert({
title: ts._preferencesBackups.cannotSave,
text: t('_preferencesBackups.nameAlreadyExists', { name }),
@@ -212,7 +203,7 @@ function loadFile(): void {
input.type = 'file';
input.multiple = false;
input.onchange = async () => {
if (!profiles) return;
if (!profiles.value) return;
if (!input.files || input.files.length === 0) return;
const file = input.files[0];
@@ -233,7 +224,7 @@ function loadFile(): void {
return os.alert({
type: 'error',
title: ts._preferencesBackups.cannotLoad,
text: err?.message,
text: (err as any)?.message ?? '',
});
}
@@ -252,9 +243,9 @@ function loadFile(): void {
}
async function applyProfile(id: string): Promise<void> {
if (!profiles) return;
if (!profiles.value) return;
const profile = profiles[id];
const profile = profiles.value[id];
const { canceled: cancel1 } = await os.confirm({
type: 'warning',
@@ -312,23 +303,23 @@ async function applyProfile(id: string): Promise<void> {
}
async function deleteProfile(id: string): Promise<void> {
if (!profiles) return;
if (!profiles.value) return;
const { canceled } = await os.confirm({
type: 'info',
title: ts.delete,
text: t('deleteAreYouSure', { x: profiles[id].name }),
text: t('deleteAreYouSure', { x: profiles.value[id].name }),
});
if (canceled) return;
await os.apiWithDialog('i/registry/remove', { scope, key: id });
delete profiles[id];
delete profiles.value[id];
}
async function save(id: string): Promise<void> {
if (!profiles) return;
if (!profiles.value) return;
const { name, createdAt } = profiles[id];
const { name, createdAt } = profiles.value[id];
const { canceled } = await os.confirm({
type: 'info',
@@ -349,25 +340,25 @@ async function save(id: string): Promise<void> {
}
async function rename(id: string): Promise<void> {
if (!profiles) return;
if (!profiles.value) return;
const { canceled: cancel1, result: name } = await os.inputText({
title: ts._preferencesBackups.inputName,
});
if (cancel1 || profiles[id].name === name) return;
if (cancel1 || profiles.value[id].name === name) return;
if (Object.values(profiles).some(x => x.name === name)) {
if (Object.values(profiles.value).some(x => x.name === name)) {
return os.alert({
title: ts._preferencesBackups.cannotSave,
text: t('_preferencesBackups.nameAlreadyExists', { name }),
});
}
const registry = Object.assign({}, { ...profiles[id] });
const registry = Object.assign({}, { ...profiles.value[id] });
const { canceled: cancel2 } = await os.confirm({
type: 'info',
title: ts._preferencesBackups.rename,
title: ts.rename,
text: t('_preferencesBackups.renameConfirm', { old: registry.name, new: name }),
});
if (cancel2) return;
@@ -377,7 +368,7 @@ async function rename(id: string): Promise<void> {
}
function menu(ev: MouseEvent, profileId: string) {
if (!profiles) return;
if (!profiles.value) return;
return os.popupMenu([{
text: ts._preferencesBackups.apply,
@@ -387,8 +378,8 @@ function menu(ev: MouseEvent, profileId: string) {
type: 'a',
text: ts.download,
icon: 'ti ti-download',
href: URL.createObjectURL(new Blob([JSON.stringify(profiles[profileId], null, 2)], { type: 'application/json' })),
download: `${profiles[profileId].name}.json`,
href: URL.createObjectURL(new Blob([JSON.stringify(profiles.value[profileId], null, 2)], { type: 'application/json' })),
download: `${profiles.value[profileId].name}.json`,
}, null, {
text: ts.rename,
icon: 'ti ti-forms',
@@ -402,16 +393,16 @@ function menu(ev: MouseEvent, profileId: string) {
icon: 'ti ti-trash',
action: () => deleteProfile(profileId),
danger: true,
}], ev.currentTarget ?? ev.target);
}], (ev.currentTarget ?? ev.target ?? undefined) as unknown as HTMLElement | undefined);
}
onMounted(() => {
// streamingのuser storage updateイベントを監視して更新
connection?.on('registryUpdated', ({ scope: recievedScope, key, value }) => {
if (!recievedScope || recievedScope.length !== scope.length || recievedScope[0] !== scope[0]) return;
if (!profiles) return;
if (!profiles.value) return;
profiles[key] = value;
profiles.value[key] = value;
});
});

View File

@@ -11,7 +11,6 @@ export function createAiScriptEnv(opts) {
USER_NAME: $i ? values.STR($i.name) : values.NULL,
USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value),
CURRENT_URL: values.STR(window.location.href),
'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
await os.alert({
type: type ? type.value : 'info',
@@ -48,5 +47,8 @@ export function createAiScriptEnv(opts) {
utils.assertString(key);
return utils.jsToVal(JSON.parse(miLocalStorage.getItem(`aiscript:${opts.storageKey}:${key.value}`)));
}),
'Mk:url': values.FN_NATIVE(() => {
return values.STR(window.location.href);
}),
};
}

View File

@@ -2,7 +2,7 @@ const twemojiSvgBase = '/twemoji';
const fluentEmojiPngBase = '/fluent-emoji';
export function char2twemojiFilePath(char: string): string {
let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16));
let codes = Array.from(char, x => x.codePointAt(0)?.toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
codes = codes.filter(x => x && x.length);
const fileName = codes.join('-');
@@ -10,7 +10,7 @@ export function char2twemojiFilePath(char: string): string {
}
export function char2fluentEmojiFilePath(char: string): string {
let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16));
let codes = Array.from(char, x => x.codePointAt(0)?.toString(16));
// Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25
if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char);
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');

View File

@@ -12,7 +12,8 @@ export function chooseFileFromPc(multiple: boolean, keepOriginal = false): Promi
input.type = 'file';
input.multiple = multiple;
input.onchange = () => {
const promises = Array.from(input.files).map(file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal));
if (!input.files) return res([]);
const promises = Array.from(input.files, file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal));
Promise.all(promises).then(driveFiles => {
res(driveFiles);