merge: upstream

This commit is contained in:
Mar0xy 2023-10-31 19:33:24 +01:00
commit 4dd23a3793
No known key found for this signature in database
GPG key ID: 56569BBE47D2C828
217 changed files with 6773 additions and 2275 deletions

View file

@ -9,6 +9,7 @@ import { $i } from '@/account.js';
import { miLocalStorage } from '@/local-storage.js';
import { customEmojis } from '@/custom-emojis.js';
import { url, lang } from '@/config.js';
import { nyaize } from '@/scripts/nyaize.js';
export function createAiScriptEnv(opts) {
return {
@ -71,5 +72,9 @@ export function createAiScriptEnv(opts) {
'Mk:url': values.FN_NATIVE(() => {
return values.STR(window.location.href);
}),
'Mk:nyaize': values.FN_NATIVE(([text]) => {
utils.assertString(text);
return values.STR(nyaize(text.value));
}),
};
}

View file

@ -0,0 +1,31 @@
import { setWasm, setCDN, Highlighter, getHighlighter as _getHighlighter } from 'shiki';
setWasm('/assets/shiki/dist/onig.wasm');
setCDN('/assets/shiki/');
let _highlighter: Highlighter | null = null;
export async function getHighlighter(): Promise<Highlighter> {
if (!_highlighter) {
return await initHighlighter();
}
return _highlighter;
}
export async function initHighlighter() {
const highlighter = await _getHighlighter({
theme: 'dark-plus',
langs: ['js'],
});
await highlighter.loadLanguage({
path: 'languages/aiscript.tmLanguage.json',
id: 'aiscript',
scopeName: 'source.aiscript',
aliases: ['is', 'ais'],
});
_highlighter = highlighter;
return highlighter;
}

View file

@ -56,6 +56,7 @@ function copyUrl(file: Misskey.entities.DriveFile) {
copyToClipboard(file.url);
os.success();
}
/*
function addApp() {
alert('not implemented yet');

View file

@ -114,6 +114,12 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
return !confirm.canceled;
}
async function userInfoUpdate() {
os.apiWithDialog('federation/update-remote-user', {
userId: user.id,
});
}
async function invalidateFollow() {
if (!await getConfirmed(i18n.ts.breakFollowConfirm)) return;
@ -330,6 +336,14 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
}]);
}
if (user.host !== null) {
menu = menu.concat([null, {
icon: 'ph-arrows-counter-clockwise ph-bold ph-lg',
text: i18n.ts.updateRemoteUser,
action: userInfoUpdate,
}]);
}
if (defaultStore.state.devMode) {
menu = menu.concat([null, {
icon: 'ph-identification-card ph-bold ph-lg',

View file

@ -0,0 +1,129 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { defineAsyncComponent } from 'vue';
import { compareVersions } from 'compare-versions';
import { v4 as uuid } from 'uuid';
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
import type { Plugin } from '@/store.js';
import { ColdDeviceStorage } from '@/store.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
export type AiScriptPluginMeta = {
name: string;
version: string;
author: string;
description?: string;
permissions?: string[];
config?: Record<string, any>;
};
const parser = new Parser();
export function savePlugin({ id, meta, src, token }: {
id: string;
meta: AiScriptPluginMeta;
src: string;
token: string;
}) {
ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
...meta,
id,
active: true,
configData: {},
token: token,
src: src,
} as Plugin));
}
export function isSupportedAiScriptVersion(version: string): boolean {
try {
return (compareVersions(version, '0.12.0') >= 0);
} catch (err) {
return false;
}
}
export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta> {
if (!code) {
throw new Error('code is required');
}
const lv = utils.getLangVersion(code);
if (lv == null) {
throw new Error('No language version annotation found');
} else if (!isSupportedAiScriptVersion(lv)) {
throw new Error(`Aiscript version '${lv}' is not supported`);
}
let ast;
try {
ast = parser.parse(code);
} catch (err) {
throw new Error('Aiscript syntax error');
}
const meta = Interpreter.collectMetadata(ast);
if (meta == null) {
throw new Error('Meta block not found');
}
const metadata = meta.get(null);
if (metadata == null) {
throw new Error('Metadata not found');
}
const { name, version, author, description, permissions, config } = metadata;
if (name == null || version == null || author == null) {
throw new Error('Required property not found');
}
return {
name,
version,
author,
description,
permissions,
config,
};
}
export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
if (!code) return;
let realMeta: AiScriptPluginMeta;
if (!meta) {
realMeta = await parsePluginMeta(code);
} else {
realMeta = meta;
}
const token = realMeta.permissions == null || realMeta.permissions.length === 0 ? null : await new Promise((res, rej) => {
os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
title: i18n.ts.tokenRequested,
information: i18n.ts.pluginTokenRequestedDescription,
initialName: realMeta.name,
initialPermissions: realMeta.permissions,
}, {
done: async result => {
const { name, permissions } = result;
const { token } = await os.api('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
});
res(token);
},
}, 'closed');
});
savePlugin({
id: uuid(),
meta: realMeta,
token,
src: code,
});
}

View file

@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import JSON5 from 'json5';
import { addTheme, getThemes } from '@/theme-store.js';
import { Theme, applyTheme, validateTheme } from '@/scripts/theme.js';
export function parseThemeCode(code: string): Theme {
let theme;
try {
theme = JSON5.parse(code);
} catch (err) {
throw new Error('Failed to parse theme json');
}
if (!validateTheme(theme)) {
throw new Error('This theme is invaild');
}
if (getThemes().some(t => t.id === theme.id)) {
throw new Error('This theme is already installed');
}
return theme;
}
export function previewTheme(code: string): void {
const theme = parseThemeCode(code);
if (theme) applyTheme(theme, false);
}
export async function installTheme(code: string): Promise<void> {
const theme = parseThemeCode(code);
if (!theme) return;
await addTheme(theme);
}

View file

@ -6,12 +6,41 @@
import { lang } from '@/config.js';
export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP');
export const dateTimeFormat = new Intl.DateTimeFormat(versatileLang, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
});
export const numberFormat = new Intl.NumberFormat(versatileLang);
let _dateTimeFormat: Intl.DateTimeFormat;
try {
_dateTimeFormat = new Intl.DateTimeFormat(versatileLang, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
});
} catch (err) {
console.warn(err);
if (_DEV_) console.log('[Intl] Fallback to en-US');
// Fallback to en-US
_dateTimeFormat = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
});
}
export const dateTimeFormat = _dateTimeFormat;
let _numberFormat: Intl.NumberFormat;
try {
_numberFormat = new Intl.NumberFormat(versatileLang);
} catch (err) {
console.warn(err);
if (_DEV_) console.log('[Intl] Fallback to en-US');
// Fallback to en-US
_numberFormat = new Intl.NumberFormat('en-US');
}
export const numberFormat = _numberFormat;

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function nyaize(text: string): string {
return text
// ja-JP
.replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
// en-US
.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya')
.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan')
.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan')
// ko-KR
.replace(/[나-낳]/g, match => String.fromCharCode(
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
))
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');
}

View file

@ -46,6 +46,7 @@ export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, o
};
function removeListener() { container.removeEventListener('scroll', onScroll); }
container.addEventListener('scroll', onScroll, { passive: true });
return removeListener;
}
@ -71,6 +72,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
function removeListener() {
containerOrWindow.removeEventListener('scroll', onScroll);
}
containerOrWindow.addEventListener('scroll', onScroll, { passive: true });
return removeListener;
}

View file

@ -12,15 +12,17 @@ import * as os from '@/os.js';
export function useNoteCapture(props: {
rootEl: Ref<HTMLElement>;
note: Ref<Misskey.entities.Note>;
pureNote: Ref<Misskey.entities.Note>;
isDeletedRef: Ref<boolean>;
}) {
const note = props.note;
const pureNote = props.pureNote !== undefined ? props.pureNote : props.note;
const connection = $i ? useStream() : null;
async function onStreamNoteUpdated(noteData): void {
const { type, id, body } = noteData;
if (id !== note.value.id) return;
if ((id !== note.value.id) && (id !== pureNote.value.id)) return;
switch (type) {
case 'reacted': {
@ -98,6 +100,7 @@ export function useNoteCapture(props: {
if (connection) {
// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: note.value.id });
if (pureNote.value.id !== note.value.id) connection.send('s', { id: pureNote.value.id });
if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
}
}
@ -107,6 +110,11 @@ export function useNoteCapture(props: {
connection.send('un', {
id: note.value.id,
});
if (pureNote.value.id !== note.value.id) {
connection.send('un', {
id: pureNote.value.id,
});
}
if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated);
}
}

View file

@ -71,9 +71,11 @@ export class WorkerMultiDispatch<POST = any, RETURN = any> {
public isTerminated() {
return this.terminated;
}
public getWorkers() {
return this.workers;
}
public getSymbol() {
return this.symbol;
}