merge: upstream
This commit is contained in:
commit
4dd23a3793
217 changed files with 6773 additions and 2275 deletions
|
|
@ -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));
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
31
packages/frontend/src/scripts/code-highlighter.ts
Normal file
31
packages/frontend/src/scripts/code-highlighter.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -56,6 +56,7 @@ function copyUrl(file: Misskey.entities.DriveFile) {
|
|||
copyToClipboard(file.url);
|
||||
os.success();
|
||||
}
|
||||
|
||||
/*
|
||||
function addApp() {
|
||||
alert('not implemented yet');
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
129
packages/frontend/src/scripts/install-plugin.ts
Normal file
129
packages/frontend/src/scripts/install-plugin.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
37
packages/frontend/src/scripts/install-theme.ts
Normal file
37
packages/frontend/src/scripts/install-theme.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
20
packages/frontend/src/scripts/nyaize.ts
Normal file
20
packages/frontend/src/scripts/nyaize.ts
Normal 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, '냥');
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue