Merge tag '13.14.1' into merge-upstream

This commit is contained in:
まっちゃとーにゅ
2023-07-23 03:08:40 +09:00
560 changed files with 12755 additions and 8764 deletions

View File

@@ -11,6 +11,7 @@ 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',

View File

@@ -510,7 +510,7 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
// Ui:root.update({ children: [...] }) の糖衣構文
'Ui:render': values.FN_NATIVE(([children], opts) => {
utils.assertArray(children);
rootComponent.value.children = children.value.map(v => {
utils.assertObject(v);
return v.value.get('id').value;

View File

@@ -78,8 +78,9 @@ export function maximum(xs: number[]): number {
export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
const groups = [] as T[][];
for (const x of xs) {
if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) {
groups[groups.length - 1].push(x);
const lastGroup = groups.at(-1);
if (lastGroup !== undefined && f(lastGroup[0], x)) {
lastGroup.push(x);
} else {
groups.push([x]);
}

View File

@@ -65,7 +65,7 @@ export class Autocomplete {
*/
private onInput() {
const caretPos = this.textarea.selectionStart;
const text = this.text.substr(0, caretPos).split('\n').pop()!;
const text = this.text.substring(0, caretPos).split('\n').pop()!;
const mentionIndex = text.lastIndexOf('@');
const hashtagIndex = text.lastIndexOf('#');
@@ -91,7 +91,7 @@ export class Autocomplete {
let opened = false;
if (isMention) {
const username = text.substr(mentionIndex + 1);
const username = text.substring(mentionIndex + 1);
if (username !== '' && username.match(/^[a-zA-Z0-9_]+$/)) {
this.open('user', username);
opened = true;
@@ -102,7 +102,7 @@ export class Autocomplete {
}
if (isHashtag && !opened) {
const hashtag = text.substr(hashtagIndex + 1);
const hashtag = text.substring(hashtagIndex + 1);
if (!hashtag.includes(' ')) {
this.open('hashtag', hashtag);
opened = true;
@@ -110,7 +110,7 @@ export class Autocomplete {
}
if (isEmoji && !opened) {
const emoji = text.substr(emojiIndex + 1);
const emoji = text.substring(emojiIndex + 1);
if (!emoji.includes(' ')) {
this.open('emoji', emoji);
opened = true;
@@ -118,7 +118,7 @@ export class Autocomplete {
}
if (isMfmTag && !opened) {
const mfmTag = text.substr(mfmTagIndex + 1);
const mfmTag = text.substring(mfmTagIndex + 1);
if (!mfmTag.includes(' ')) {
this.open('mfmTag', mfmTag.replace('[', ''));
opened = true;
@@ -208,9 +208,9 @@ export class Autocomplete {
if (type === 'user') {
const source = this.text;
const before = source.substr(0, caret);
const before = source.substring(0, caret);
const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
const after = source.substr(caret);
const after = source.substring(caret);
const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
@@ -226,9 +226,9 @@ export class Autocomplete {
} else if (type === 'hashtag') {
const source = this.text;
const before = source.substr(0, caret);
const before = source.substring(0, caret);
const trimmedBefore = before.substring(0, before.lastIndexOf('#'));
const after = source.substr(caret);
const after = source.substring(caret);
// 挿入
this.text = `${trimmedBefore}#${value} ${after}`;
@@ -242,9 +242,9 @@ export class Autocomplete {
} else if (type === 'emoji') {
const source = this.text;
const before = source.substr(0, caret);
const before = source.substring(0, caret);
const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
const after = source.substr(caret);
const after = source.substring(caret);
// 挿入
this.text = trimmedBefore + value + after;
@@ -258,9 +258,9 @@ export class Autocomplete {
} else if (type === 'mfmTag') {
const source = this.text;
const before = source.substr(0, caret);
const before = source.substring(0, caret);
const trimmedBefore = before.substring(0, before.lastIndexOf('$'));
const after = source.substr(caret);
const after = source.substring(caret);
// 挿入
this.text = `${trimmedBefore}$[${value} ]${after}`;

View File

@@ -1,7 +1,8 @@
import { ref } from "vue";
export class Cache<T> {
private cachedAt: number | null = null;
private value: T | undefined;
public value = ref<T | undefined>();
private lifetime: number;
constructor(lifetime: Cache<never>['lifetime']) {
@@ -10,21 +11,20 @@ export class Cache<T> {
public set(value: T): void {
this.cachedAt = Date.now();
this.value = value;
this.value.value = value;
}
public get(): T | undefined {
private get(): T | undefined {
if (this.cachedAt == null) return undefined;
if ((Date.now() - this.cachedAt) > this.lifetime) {
this.value = undefined;
this.value.value = undefined;
this.cachedAt = null;
return undefined;
}
return this.value;
return this.value.value;
}
public delete() {
this.value = undefined;
this.cachedAt = null;
}

View File

@@ -1,5 +1,7 @@
// structredCloneが遅いため
// SEE: http://var.blog.jp/archives/86038606.html
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];

View File

@@ -0,0 +1,19 @@
import * as mfm from 'mfm-js';
import * as misskey from 'misskey-js';
import { extractUrlFromMfm } from './extract-url-from-mfm';
export function shouldCollapsed(note: misskey.entities.Note): boolean {
const urls = note.text ? extractUrlFromMfm(mfm.parse(note.text)) : null;
const collapsed = note.cw == null && note.text != null && (
(note.text.includes('$[x2')) ||
(note.text.includes('$[x3')) ||
(note.text.includes('$[x4')) ||
(note.text.includes('$[scale')) ||
(note.text.split('\n').length > 9) ||
(note.text.length > 500) ||
(note.files.length >= 5) ||
(!!urls && urls.length >= 4)
);
return collapsed;
}

View File

@@ -1,3 +1,4 @@
type EnumItem = string | {label: string; value: string;};
export type FormItem = {
label?: string;
type: 'string';
@@ -20,7 +21,7 @@ export type FormItem = {
type: 'enum';
default: string | null;
hidden?: boolean;
enum: string[];
enum: EnumItem[];
} | {
label?: string;
type: 'radio';

View File

@@ -5,7 +5,7 @@ export async function genSearchQuery(v: any, q: string) {
let host: string;
let userId: string;
if (q.split(' ').some(x => x.startsWith('@'))) {
for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substr(1))) {
for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substring(1))) {
if (at.includes('.')) {
if (at === localHost || at === '.') {
host = null;

View File

@@ -3,6 +3,8 @@ import { defineAsyncComponent } from 'vue';
import { i18n } from '@/i18n';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import * as os from '@/os';
import { MenuItem } from '@/types/menu';
import { defaultStore } from '@/store';
function rename(file: Misskey.entities.DriveFile) {
os.inputText({
@@ -66,8 +68,10 @@ async function deleteFile(file: Misskey.entities.DriveFile) {
});
}
export function getDriveFileMenu(file: Misskey.entities.DriveFile) {
return [{
export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] {
const isImage = file.type.startsWith('image/');
let menu;
menu = [{
text: i18n.ts.rename,
icon: 'ti ti-forms',
action: () => rename(file),
@@ -79,7 +83,14 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile) {
text: i18n.ts.describeFile,
icon: 'ti ti-text-caption',
action: () => describe(file),
}, null, {
}, ...isImage ? [{
text: i18n.ts.cropImage,
icon: 'ti ti-crop',
action: () => os.cropImage(file, {
aspectRatio: NaN,
uploadFolder: folder ? folder.id : folder
}),
}] : [], null, {
text: i18n.ts.createNoteFromTheFile,
icon: 'ti ti-pencil',
action: () => os.post({
@@ -102,4 +113,16 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile) {
danger: true,
action: () => deleteFile(file),
}];
if (defaultStore.state.devMode) {
menu = menu.concat([null, {
icon: 'ti ti-id',
text: i18n.ts.copyFileId,
action: () => {
copyToClipboard(file.id);
},
}]);
}
return menu;
}

View File

@@ -1,14 +1,15 @@
import { toUnicode } from 'punycode';
import { defineAsyncComponent } from 'vue';
import * as misskey from 'misskey-js';
import { i18n } from '@/i18n';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import { host } from '@/config';
import { host, url } from '@/config';
import * as os from '@/os';
import { defaultStore, userActions } from '@/store';
import { $i, iAmModerator } from '@/account';
import { mainRouter } from '@/router';
import { Router } from '@/nirax';
import { rolesCache, userListsCache } from '@/cache';
import { antennasCache, rolesCache, userListsCache } from '@/cache';
export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) {
const meId = $i ? $i.id : null;
@@ -137,6 +138,13 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
action: () => {
copyToClipboard(`${user.host ?? host}/@${user.username}.atom`);
},
}, {
icon: 'ti ti-share',
text: i18n.ts.copyProfileUrl,
action: () => {
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
copyToClipboard(`${url}/${canonical}`);
},
}, {
icon: 'ti ti-mail',
text: i18n.ts.sendMessage,
@@ -158,11 +166,39 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
return lists.map(list => ({
text: list.name,
action: () => {
os.apiWithDialog('users/lists/push', {
action: async () => {
await os.apiWithDialog('users/lists/push', {
listId: list.id,
userId: user.id,
});
userListsCache.delete();
},
}));
},
}, {
type: 'parent',
icon: 'ti ti-antenna',
text: i18n.ts.addToAntenna,
children: async () => {
const antennas = await antennasCache.fetch(() => os.api('antennas/list'));
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
return antennas.filter((a) => a.src === 'users').map(antenna => ({
text: antenna.name,
action: async () => {
await os.apiWithDialog('antennas/update', {
antennaId: antenna.id,
name: antenna.name,
keywords: antenna.keywords,
excludeKeywords: antenna.excludeKeywords,
src: antenna.src,
userListId: antenna.userListId,
users: [...antenna.users, canonical],
caseSensitive: antenna.caseSensitive,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
notify: antenna.notify,
});
antennasCache.delete();
},
}));
},
@@ -196,7 +232,7 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
default: 'indefinitely',
});
if (canceled) return;
const expiresAt = period === 'indefinitely' ? null
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)

View File

@@ -1,3 +1,20 @@
const requestIdleCallback: typeof globalThis.requestIdleCallback = globalThis.requestIdleCallback ?? ((callback) => {
const start = performance.now();
const timeoutId = setTimeout(() => {
callback({
didTimeout: false, // polyfill でタイムアウト発火することはない
timeRemaining() {
const diff = performance.now() - start;
return Math.max(0, 50 - diff); // <https://www.w3.org/TR/requestidlecallback/#idle-periods>
},
});
});
return timeoutId;
});
const cancelIdleCallback: typeof globalThis.cancelIdleCallback = globalThis.cancelIdleCallback ?? ((timeoutId) => {
clearTimeout(timeoutId);
});
class IdlingRenderScheduler {
#renderers: Set<FrameRequestCallback>;
#rafId: number;

View File

@@ -0,0 +1,11 @@
import * as misskey from 'misskey-js';
import { $i } from '@/account';
export function isFfVisibleForMe(user: misskey.entities.UserDetailed): boolean {
if ($i && $i.id === user.id) return true;
if (user.ffVisibility === 'private') return false;
if (user.ffVisibility === 'followers' && !user.isFollowing) return false;
return true;
}

View File

@@ -6,18 +6,19 @@ import { Router } from '@/nirax';
export async function lookup(router?: Router) {
const _router = router ?? mainRouter;
const { canceled, result: query } = await os.inputText({
const { canceled, result: temp } = await os.inputText({
title: i18n.ts.lookup,
});
const query = temp ? temp.trim() : '';
if (canceled) return;
if (query.startsWith('@') && !query.includes(' ')) {
_router.push(`/${query}`);
return;
}
if (query.startsWith('#')) {
_router.push(`/tags/${encodeURIComponent(query.substr(1))}`);
_router.push(`/tags/${encodeURIComponent(query.substring(1))}`);
return;
}

View File

@@ -193,9 +193,7 @@ export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notifica
}
export function playFile(file: string, volume: number) {
const masterVolume = soundConfigStore.state.sound_masterVolume;
if (masterVolume === 0) return;
const audio = setVolume(getAudio(file), volume);
if (audio.volume === 0) return;
audio.play();
}

View File

@@ -35,7 +35,7 @@ export const fromThemeString = (str?: string) : ThemeValue => {
} else if (str.startsWith('"')) {
return {
type: 'css',
value: str.substr(1).trim(),
value: str.substring(1).trim(),
};
} else {
return str;

View File

@@ -98,7 +98,7 @@ function compile(theme: Theme): Record<string, string> {
function getColor(val: string): tinycolor.Instance {
// ref (prop)
if (val[0] === '@') {
return getColor(theme.props[val.substr(1)]);
return getColor(theme.props[val.substring(1)]);
}
// ref (const)
@@ -109,7 +109,7 @@ function compile(theme: Theme): Record<string, string> {
// func
else if (val[0] === ':') {
const parts = val.split('<');
const func = parts.shift().substr(1);
const func = parts.shift().substring(1);
const arg = parseFloat(parts.shift());
const color = getColor(parts.join('<'));

View File

@@ -1,7 +1,15 @@
import isAnimated from 'is-file-animated';
import { isWebpSupported } from './isWebpSupported';
import type { BrowserImageResizerConfig } from 'browser-image-resizer';
const compressTypeMap = {
'image/jpeg': { quality: 0.90, mimeType: 'image/webp' },
'image/png': { quality: 1, mimeType: 'image/webp' },
'image/webp': { quality: 0.90, mimeType: 'image/webp' },
'image/svg+xml': { quality: 1, mimeType: 'image/webp' },
} as const;
const compressTypeMapFallback = {
'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' },
'image/png': { quality: 1, mimeType: 'image/png' },
'image/webp': { quality: 0.85, mimeType: 'image/jpeg' },
@@ -9,7 +17,7 @@ const compressTypeMap = {
} as const;
export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfig | undefined> {
const imgConfig = compressTypeMap[file.type];
const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type];
if (!imgConfig || await isAnimated(file)) {
return;
}

View File

@@ -0,0 +1,10 @@
let isWebpSupportedCache: boolean | undefined;
export function isWebpSupported() {
if (isWebpSupportedCache === undefined) {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
isWebpSupportedCache = canvas.toDataURL('image/webp').startsWith('data:image/webp');
}
return isWebpSupportedCache;
}

View File

@@ -2,7 +2,7 @@
* 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)

View File

@@ -93,12 +93,12 @@ export function useNoteCapture(props: {
function onStreamConnected() {
capture(false);
}
capture(true);
if (connection) {
connection.on('_connected_', onStreamConnected);
}
onUnmounted(() => {
decapture(true);
if (connection) {