Compare commits
10 Commits
KisaragiEf
...
frontend-d
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9cd7ea77ff | ||
![]() |
74c93fcebe | ||
![]() |
8be624aa44 | ||
![]() |
3fe7e37f10 | ||
![]() |
7fe3035059 | ||
![]() |
06855f769f | ||
![]() |
3e85052754 | ||
![]() |
b6fdd71957 | ||
![]() |
36dff66883 | ||
![]() |
255c8bd1b9 |
1
.github/ISSUE_TEMPLATE/01_bug-report.yml
vendored
1
.github/ISSUE_TEMPLATE/01_bug-report.yml
vendored
@@ -9,7 +9,6 @@ body:
|
||||
Thanks for reporting!
|
||||
First, in order to avoid duplicate Issues, please search to see if the problem you found has already been reported.
|
||||
Also, If you are NOT owner/admin of server, PLEASE DONT REPORT SERVER SPECIFIC ISSUES TO HERE! (e.g. feature XXX is not working in misskey.example) Please try with another misskey servers, and if your issue is only reproducible with specific server, contact your server's owner/admin first.
|
||||
Finally, this is not place to ask trouble shooting. Please use Duscussions for it.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
3
.github/workflows/lint.yml
vendored
3
.github/workflows/lint.yml
vendored
@@ -78,6 +78,7 @@ jobs:
|
||||
matrix:
|
||||
workspace:
|
||||
- backend
|
||||
- sw
|
||||
- misskey-js
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
@@ -92,7 +93,7 @@ jobs:
|
||||
- run: corepack enable
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: pnpm --filter misskey-js run build
|
||||
if: ${{ matrix.workspace == 'backend' }}
|
||||
if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'sw' }}
|
||||
- run: pnpm --filter misskey-reversi run build
|
||||
if: ${{ matrix.workspace == 'backend' }}
|
||||
- run: pnpm --filter ${{ matrix.workspace }} run typecheck
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,6 +44,7 @@ compose.yml
|
||||
/build
|
||||
built
|
||||
built-test
|
||||
js-built
|
||||
/data
|
||||
/.cache-loader
|
||||
/db
|
||||
|
@@ -5,6 +5,7 @@
|
||||
|
||||
### Client
|
||||
- サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
|
||||
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
|
||||
|
||||
### Server
|
||||
- ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
|
||||
|
@@ -144,7 +144,9 @@ export interface Schema extends OfSchema {
|
||||
readonly type?: TypeStringef;
|
||||
readonly nullable?: boolean;
|
||||
readonly optional?: boolean;
|
||||
readonly prefixItems?: ReadonlyArray<Schema>;
|
||||
readonly items?: Schema;
|
||||
readonly unevaluatedItems?: Schema | boolean;
|
||||
readonly properties?: Obj;
|
||||
readonly required?: ReadonlyArray<Extract<keyof NonNullable<this['properties']>, string>>;
|
||||
readonly description?: string;
|
||||
@@ -198,6 +200,7 @@ type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X
|
||||
//type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
|
||||
type UnionObjType<s extends Obj, a extends readonly any[], X extends ReadonlyArray<keyof s> = a[number]> = X extends any ? ObjType<s, X> : never;
|
||||
type ArrayUnion<T> = T extends any ? Array<T> : never;
|
||||
type ArrayToTuple<X extends ReadonlyArray<Schema>> = { [K in keyof X]: SchemaType<X[K]> };
|
||||
|
||||
type ObjectSchemaTypeDef<p extends Schema> =
|
||||
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
|
||||
@@ -232,6 +235,12 @@ export type SchemaTypeDef<p extends Schema> =
|
||||
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
|
||||
never
|
||||
) :
|
||||
p['prefixItems'] extends ReadonlyArray<Schema> ? (
|
||||
p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] :
|
||||
p['items'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||
p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||
[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
|
||||
) :
|
||||
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
|
||||
any[]
|
||||
) :
|
||||
|
@@ -85,7 +85,7 @@ export type MiNotification = {
|
||||
/**
|
||||
* アプリ通知のbody
|
||||
*/
|
||||
customBody: string | null;
|
||||
customBody: string;
|
||||
|
||||
/**
|
||||
* アプリ通知のheader
|
||||
|
@@ -3,6 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
|
||||
import { notificationTypes } from '@/types.js';
|
||||
|
||||
const baseSchema = {
|
||||
@@ -294,6 +295,7 @@ export const packedNotificationSchema = {
|
||||
achievement: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ACHIEVEMENT_TYPES,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
@@ -311,11 +313,11 @@ export const packedNotificationSchema = {
|
||||
},
|
||||
header: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
icon: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
|
@@ -21,16 +21,15 @@ export const meta = {
|
||||
items: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
},
|
||||
prefixItems: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
unevaluatedItems: false,
|
||||
},
|
||||
example: [[
|
||||
'example.com',
|
||||
|
@@ -21,16 +21,15 @@ export const meta = {
|
||||
items: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
},
|
||||
prefixItems: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
unevaluatedItems: false,
|
||||
},
|
||||
example: [[
|
||||
'example.com',
|
||||
|
@@ -166,7 +166,7 @@
|
||||
|
||||
if (!errorsElement) {
|
||||
document.body.innerHTML = `
|
||||
<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 9v2m0 4v.01"></path>
|
||||
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
||||
@@ -178,7 +178,7 @@
|
||||
<p><b>The following actions may solve the problem. / 以下を行うと解決する可能性があります。</b></p>
|
||||
<p>Update your os and browser / ブラウザおよびOSを最新バージョンに更新する</p>
|
||||
<p>Disable an adblocker / アドブロッカーを無効にする</p>
|
||||
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
|
||||
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
|
||||
<p>(Tor Browser) Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
|
||||
<details style="color: #86b300;">
|
||||
<summary>Other options / その他のオプション</summary>
|
||||
@@ -320,6 +320,6 @@
|
||||
#errorInfo {
|
||||
width: 50%;
|
||||
}
|
||||
}`)
|
||||
}`);
|
||||
}
|
||||
})();
|
||||
|
@@ -36,8 +36,6 @@ html
|
||||
link(rel='prefetch' href=serverErrorImageUrl)
|
||||
link(rel='prefetch' href=infoImageUrl)
|
||||
link(rel='prefetch' href=notFoundImageUrl)
|
||||
//- https://github.com/misskey-dev/misskey/issues/9842
|
||||
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v3.3.0')
|
||||
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
|
||||
|
||||
if !config.clientManifestExists
|
||||
|
@@ -3,9 +3,13 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
|
||||
// https://vitejs.dev/config/build-options.html#build-modulepreload
|
||||
import 'vite/modulepreload-polyfill';
|
||||
|
||||
import '@tabler/icons-webfont/dist/tabler-icons.scss';
|
||||
|
||||
import '@/style.scss';
|
||||
import { mainBoot } from '@/boot/main-boot.js';
|
||||
import { subBoot } from '@/boot/sub-boot.js';
|
||||
|
@@ -3,11 +3,6 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// devモードで起動される際(index.htmlを使うとき)はrouterが暴発してしまってうまく読み込めない。
|
||||
// よって、devモードとして起動されるときはビルド時に組み込む形としておく。
|
||||
// (pnpm start時はpugファイルの中で静的リソースとして読み込むようになっており、この問題は起こっていない)
|
||||
import '@tabler/icons-webfont/dist/tabler-icons.scss';
|
||||
|
||||
await main();
|
||||
|
||||
import('@/_boot_.js');
|
||||
|
@@ -245,7 +245,7 @@ const submitText = computed((): string => {
|
||||
});
|
||||
|
||||
const textLength = computed((): number => {
|
||||
return (text.value + imeText.value).trim().length;
|
||||
return (text.value + imeText.value).length;
|
||||
});
|
||||
|
||||
const maxTextLength = computed((): number => {
|
||||
|
@@ -36,7 +36,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XChart from './overview.queue.chart.vue';
|
||||
import type { ApQueueDomain } from '@/pages/admin/queue.vue';
|
||||
import number from '@/filters/number.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
|
||||
@@ -52,10 +54,10 @@ const chartDelayed = shallowRef<InstanceType<typeof XChart>>();
|
||||
const chartWaiting = shallowRef<InstanceType<typeof XChart>>();
|
||||
|
||||
const props = defineProps<{
|
||||
domain: string;
|
||||
domain: ApQueueDomain;
|
||||
}>();
|
||||
|
||||
const onStats = (stats) => {
|
||||
function onStats(stats: Misskey.entities.QueueStats) {
|
||||
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
|
||||
active.value = stats[props.domain].active;
|
||||
delayed.value = stats[props.domain].delayed;
|
||||
@@ -65,13 +67,13 @@ const onStats = (stats) => {
|
||||
chartActive.value.pushData(stats[props.domain].active);
|
||||
chartDelayed.value.pushData(stats[props.domain].delayed);
|
||||
chartWaiting.value.pushData(stats[props.domain].waiting);
|
||||
};
|
||||
}
|
||||
|
||||
const onStatsLog = (statsLog) => {
|
||||
const dataProcess = [];
|
||||
const dataActive = [];
|
||||
const dataDelayed = [];
|
||||
const dataWaiting = [];
|
||||
function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
|
||||
const dataProcess: Misskey.entities.QueueStats[ApQueueDomain]['activeSincePrevTick'][] = [];
|
||||
const dataActive: Misskey.entities.QueueStats[ApQueueDomain]['active'][] = [];
|
||||
const dataDelayed: Misskey.entities.QueueStats[ApQueueDomain]['delayed'][] = [];
|
||||
const dataWaiting: Misskey.entities.QueueStats[ApQueueDomain]['waiting'][] = [];
|
||||
|
||||
for (const stats of [...statsLog].reverse()) {
|
||||
dataProcess.push(stats[props.domain].activeSincePrevTick);
|
||||
@@ -84,7 +86,7 @@ const onStatsLog = (statsLog) => {
|
||||
chartActive.value.setData(dataActive);
|
||||
chartDelayed.value.setData(dataDelayed);
|
||||
chartWaiting.value.setData(dataWaiting);
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
connection.on('stats', onStats);
|
||||
|
@@ -49,7 +49,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XChart from './queue.chart.chart.vue';
|
||||
import type { ApQueueDomain } from '@/pages/admin/queue.vue';
|
||||
import number from '@/filters/number.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
@@ -62,17 +64,17 @@ const activeSincePrevTick = ref(0);
|
||||
const active = ref(0);
|
||||
const delayed = ref(0);
|
||||
const waiting = ref(0);
|
||||
const jobs = ref<(string | number)[][]>([]);
|
||||
const jobs = ref<Misskey.Endpoints[`admin/queue/${ApQueueDomain}-delayed`]['res']>([]);
|
||||
const chartProcess = shallowRef<InstanceType<typeof XChart>>();
|
||||
const chartActive = shallowRef<InstanceType<typeof XChart>>();
|
||||
const chartDelayed = shallowRef<InstanceType<typeof XChart>>();
|
||||
const chartWaiting = shallowRef<InstanceType<typeof XChart>>();
|
||||
|
||||
const props = defineProps<{
|
||||
domain: string;
|
||||
domain: ApQueueDomain;
|
||||
}>();
|
||||
|
||||
const onStats = (stats) => {
|
||||
function onStats(stats: Misskey.entities.QueueStats) {
|
||||
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
|
||||
active.value = stats[props.domain].active;
|
||||
delayed.value = stats[props.domain].delayed;
|
||||
@@ -82,13 +84,13 @@ const onStats = (stats) => {
|
||||
chartActive.value.pushData(stats[props.domain].active);
|
||||
chartDelayed.value.pushData(stats[props.domain].delayed);
|
||||
chartWaiting.value.pushData(stats[props.domain].waiting);
|
||||
};
|
||||
}
|
||||
|
||||
const onStatsLog = (statsLog) => {
|
||||
const dataProcess = [];
|
||||
const dataActive = [];
|
||||
const dataDelayed = [];
|
||||
const dataWaiting = [];
|
||||
function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
|
||||
const dataProcess: Misskey.entities.QueueStats[ApQueueDomain]['activeSincePrevTick'][] = [];
|
||||
const dataActive: Misskey.entities.QueueStats[ApQueueDomain]['active'][] = [];
|
||||
const dataDelayed: Misskey.entities.QueueStats[ApQueueDomain]['delayed'][] = [];
|
||||
const dataWaiting: Misskey.entities.QueueStats[ApQueueDomain]['waiting'][] = [];
|
||||
|
||||
for (const stats of [...statsLog].reverse()) {
|
||||
dataProcess.push(stats[props.domain].activeSincePrevTick);
|
||||
@@ -101,14 +103,12 @@ const onStatsLog = (statsLog) => {
|
||||
chartActive.value.setData(dataActive);
|
||||
chartDelayed.value.setData(dataDelayed);
|
||||
chartWaiting.value.setData(dataWaiting);
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.domain === 'inbox' || props.domain === 'deliver') {
|
||||
misskeyApi(`admin/queue/${props.domain}-delayed`).then(result => {
|
||||
jobs.value = result;
|
||||
});
|
||||
}
|
||||
misskeyApi(`admin/queue/${props.domain}-delayed`).then(result => {
|
||||
jobs.value = result;
|
||||
});
|
||||
|
||||
connection.on('stats', onStats);
|
||||
connection.on('statsLog', onStatsLog);
|
||||
|
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, type Ref } from 'vue';
|
||||
import XQueue from './queue.chart.vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import * as os from '@/os.js';
|
||||
@@ -25,7 +25,9 @@ import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
const tab = ref('deliver');
|
||||
export type ApQueueDomain = 'deliver' | 'inbox';
|
||||
|
||||
const tab: Ref<ApQueueDomain> = ref('deliver');
|
||||
|
||||
function clear() {
|
||||
os.confirm({
|
||||
|
@@ -137,7 +137,6 @@ export class I18n<T extends ILocale> {
|
||||
return this.tsxCache = new Proxy(this.locale, new Handler()) as unknown as Tsx<T>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (this.tsxCache) {
|
||||
return this.tsxCache;
|
||||
}
|
||||
@@ -244,51 +243,3 @@ export class I18n<T extends ILocale> {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.vitest) {
|
||||
const { describe, expect, it } = import.meta.vitest;
|
||||
|
||||
describe('i18n', () => {
|
||||
it('t', () => {
|
||||
const i18n = new I18n({
|
||||
foo: 'foo',
|
||||
bar: {
|
||||
baz: 'baz',
|
||||
qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
|
||||
quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
|
||||
},
|
||||
});
|
||||
|
||||
expect(i18n.t('foo')).toBe('foo');
|
||||
expect(i18n.t('bar.baz')).toBe('baz');
|
||||
expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge');
|
||||
expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga');
|
||||
});
|
||||
it('ts', () => {
|
||||
const i18n = new I18n({
|
||||
foo: 'foo',
|
||||
bar: {
|
||||
baz: 'baz',
|
||||
qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
|
||||
quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
|
||||
},
|
||||
});
|
||||
|
||||
expect(i18n.ts.foo).toBe('foo');
|
||||
expect(i18n.ts.bar.baz).toBe('baz');
|
||||
});
|
||||
it('tsx', () => {
|
||||
const i18n = new I18n({
|
||||
foo: 'foo',
|
||||
bar: {
|
||||
baz: 'baz',
|
||||
qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
|
||||
quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
|
||||
},
|
||||
});
|
||||
|
||||
expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge');
|
||||
expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
23
packages/frontend/src/services/AccountService.ts
Normal file
23
packages/frontend/src/services/AccountService.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { inject, injectable, container } from 'tsyringe';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { defineAsyncComponent, reactive, ref } from 'vue';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
|
||||
type Account = Misskey.entities.MeDetailed & { token: string };
|
||||
|
||||
const accountData = miLocalStorage.getItem('account');
|
||||
|
||||
const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
||||
|
||||
@injectable()
|
||||
export class AccountService {
|
||||
constructor(
|
||||
) {}
|
||||
|
||||
public readonly i = $i;
|
||||
}
|
170
packages/frontend/src/services/UploaderService.ts
Normal file
170
packages/frontend/src/services/UploaderService.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { inject, injectable, container } from 'tsyringe';
|
||||
import { reactive, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
|
||||
import { getCompressionConfig } from './upload/compress-config.js';
|
||||
import { AccountService } from './AccountService.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { apiUrl } from '@/config.js';
|
||||
import { alert } from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
type Uploading = {
|
||||
id: string;
|
||||
name: string;
|
||||
progressMax: number | undefined;
|
||||
progressValue: number | undefined;
|
||||
img: string;
|
||||
};
|
||||
export const uploads = ref<Uploading[]>([]);
|
||||
|
||||
const mimeTypeMap = {
|
||||
'image/webp': 'webp',
|
||||
'image/jpeg': 'jpg',
|
||||
'image/png': 'png',
|
||||
} as const;
|
||||
|
||||
@injectable()
|
||||
export class Uploader {
|
||||
constructor(
|
||||
@inject('AccountService') private accountService: AccountService,
|
||||
@inject('ServerMetadataService') private serverMetadataService: ServerMetadataService,
|
||||
) {}
|
||||
|
||||
public uploadFile(
|
||||
file: File,
|
||||
folder?: any,
|
||||
name?: string,
|
||||
keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
|
||||
): Promise<Misskey.entities.DriveFile> {
|
||||
if (this.accountService.i == null) throw new Error('Not logged in');
|
||||
|
||||
if (folder && typeof folder === 'object') folder = folder.id;
|
||||
|
||||
return fetchServerMetadata().then((serverMetadata) => new Promise((resolve, reject) => {
|
||||
if (file.size > serverMetadata.maxFileSize) {
|
||||
alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.failedToUpload,
|
||||
text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
|
||||
});
|
||||
return reject();
|
||||
}
|
||||
|
||||
const id = uuid();
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (): Promise<void> => {
|
||||
const filename = name ?? file.name ?? 'untitled';
|
||||
const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
|
||||
|
||||
const ctx = reactive<Uploading>({
|
||||
id,
|
||||
name: defaultStore.state.keepOriginalFilename ? filename : id + extension,
|
||||
progressMax: undefined,
|
||||
progressValue: undefined,
|
||||
img: window.URL.createObjectURL(file),
|
||||
});
|
||||
|
||||
uploads.value.push(ctx);
|
||||
|
||||
const config = !keepOriginal ? await getCompressionConfig(file) : undefined;
|
||||
let resizedImage: Blob | undefined;
|
||||
if (config) {
|
||||
try {
|
||||
const resized = await readAndCompressImage(file, config);
|
||||
if (resized.size < file.size || file.type === 'image/webp') {
|
||||
// The compression may not always reduce the file size
|
||||
// (and WebP is not browser safe yet)
|
||||
resizedImage = resized;
|
||||
}
|
||||
if (_DEV_) {
|
||||
const saved = ((1 - resized.size / file.size) * 100).toFixed(2);
|
||||
console.log(`Image compression: before ${file.size} bytes, after ${resized.size} bytes, saved ${saved}%`);
|
||||
}
|
||||
|
||||
ctx.name = file.type !== config.mimeType ? `${ctx.name}.${mimeTypeMap[config.mimeType]}` : ctx.name;
|
||||
} catch (err) {
|
||||
console.error('Failed to resize image', err);
|
||||
}
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('i', this.accountService.i.token);
|
||||
formData.append('force', 'true');
|
||||
formData.append('file', resizedImage ?? file);
|
||||
formData.append('name', ctx.name);
|
||||
if (folder) formData.append('folderId', folder);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
||||
xhr.onload = ((ev: ProgressEvent<XMLHttpRequest>) => {
|
||||
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
|
||||
// TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい
|
||||
uploads.value = uploads.value.filter(x => x.id !== id);
|
||||
|
||||
if (xhr.status === 413) {
|
||||
alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.failedToUpload,
|
||||
text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
|
||||
});
|
||||
} else if (ev.target?.response) {
|
||||
const res = JSON.parse(ev.target.response);
|
||||
if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') {
|
||||
alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.failedToUpload,
|
||||
text: i18n.ts.cannotUploadBecauseInappropriate,
|
||||
});
|
||||
} else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') {
|
||||
alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.failedToUpload,
|
||||
text: i18n.ts.cannotUploadBecauseNoFreeSpace,
|
||||
});
|
||||
} else {
|
||||
alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.failedToUpload,
|
||||
text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
alert({
|
||||
type: 'error',
|
||||
title: 'Failed to upload',
|
||||
text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`,
|
||||
});
|
||||
}
|
||||
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
const driveFile = JSON.parse(ev.target.response);
|
||||
|
||||
resolve(driveFile);
|
||||
|
||||
uploads.value = uploads.value.filter(x => x.id !== id);
|
||||
}) as (ev: ProgressEvent<EventTarget>) => any;
|
||||
|
||||
xhr.upload.onprogress = ev => {
|
||||
if (ev.lengthComputable) {
|
||||
ctx.progressMax = ev.total;
|
||||
ctx.progressValue = ev.loaded;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}));
|
||||
}
|
||||
}
|
@@ -138,7 +138,7 @@ function onStats(connStats: Misskey.entities.ServerStats) {
|
||||
}
|
||||
|
||||
function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) {
|
||||
for (const revStats of statsLog.reverse()) {
|
||||
for (const revStats of statsLog.toReversed()) {
|
||||
onStats(revStats);
|
||||
}
|
||||
}
|
||||
|
@@ -111,7 +111,7 @@ function onStats(connStats: Misskey.entities.ServerStats) {
|
||||
}
|
||||
|
||||
function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) {
|
||||
for (const revStats of statsLog.reverse()) {
|
||||
for (const revStats of statsLog.toReversed()) {
|
||||
onStats(revStats);
|
||||
}
|
||||
}
|
||||
|
52
packages/frontend/test/i18n.test.ts
Normal file
52
packages/frontend/test/i18n.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { I18n } from '@/scripts/i18n.js';
|
||||
import { ParameterizedString } from '../../../locales/index.js';
|
||||
|
||||
describe('i18n', () => {
|
||||
it('t', () => {
|
||||
const i18n = new I18n({
|
||||
foo: 'foo',
|
||||
bar: {
|
||||
baz: 'baz',
|
||||
qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
|
||||
quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
|
||||
},
|
||||
});
|
||||
|
||||
expect(i18n.t('foo')).toBe('foo');
|
||||
expect(i18n.t('bar.baz')).toBe('baz');
|
||||
expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge');
|
||||
expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga');
|
||||
});
|
||||
it('ts', () => {
|
||||
const i18n = new I18n({
|
||||
foo: 'foo',
|
||||
bar: {
|
||||
baz: 'baz',
|
||||
qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
|
||||
quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
|
||||
},
|
||||
});
|
||||
|
||||
expect(i18n.ts.foo).toBe('foo');
|
||||
expect(i18n.ts.bar.baz).toBe('baz');
|
||||
});
|
||||
it('tsx', () => {
|
||||
const i18n = new I18n({
|
||||
foo: 'foo',
|
||||
bar: {
|
||||
baz: 'baz',
|
||||
qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
|
||||
quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
|
||||
},
|
||||
});
|
||||
|
||||
expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge');
|
||||
expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga');
|
||||
});
|
||||
});
|
@@ -4245,15 +4245,16 @@ export type components = {
|
||||
/** @enum {string} */
|
||||
type: 'roleAssigned';
|
||||
role: components['schemas']['Role'];
|
||||
} | {
|
||||
} | ({
|
||||
/** Format: id */
|
||||
id: string;
|
||||
/** Format: date-time */
|
||||
createdAt: string;
|
||||
/** @enum {string} */
|
||||
type: 'achievementEarned';
|
||||
achievement: string;
|
||||
} | {
|
||||
/** @enum {string} */
|
||||
achievement: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted' | 'bubbleGameExplodingHead' | 'bubbleGameDoubleExplodingHead';
|
||||
}) | ({
|
||||
/** Format: id */
|
||||
id: string;
|
||||
/** Format: date-time */
|
||||
@@ -4261,9 +4262,9 @@ export type components = {
|
||||
/** @enum {string} */
|
||||
type: 'app';
|
||||
body: string;
|
||||
header: string;
|
||||
icon: string;
|
||||
} | {
|
||||
header: string | null;
|
||||
icon: string | null;
|
||||
}) | {
|
||||
/** Format: id */
|
||||
id: string;
|
||||
/** Format: date-time */
|
||||
@@ -8217,7 +8218,7 @@ export type operations = {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': ((string | number)[])[];
|
||||
'application/json': [string, number][];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
@@ -8263,7 +8264,7 @@ export type operations = {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': ((string | number)[])[];
|
||||
'application/json': [string, number][];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
|
@@ -8,10 +8,10 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as esbuild from 'esbuild';
|
||||
import locales from '../../locales/index.js';
|
||||
import meta from '../../package.json' with { type: "json" };
|
||||
import meta from '../../package.json' with { type: 'json' };
|
||||
const watch = process.argv[2]?.includes('watch');
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
console.log('Starting SW building...');
|
||||
|
||||
|
@@ -41,11 +41,10 @@ export async function createNotification<K extends keyof PushNotificationDataMap
|
||||
|
||||
async function composeNotification(data: PushNotificationDataMap[keyof PushNotificationDataMap]): Promise<[string, NotificationOptions] | null> {
|
||||
const i18n = await (swLang.i18n ?? swLang.fetchLocale());
|
||||
const { t } = i18n;
|
||||
switch (data.type) {
|
||||
/*
|
||||
case 'driveFileCreated': // TODO (Server Side)
|
||||
return [t('_notification.fileUploaded'), {
|
||||
return [i18n.ts._notification.fileUploaded, {
|
||||
body: body.name,
|
||||
icon: body.url,
|
||||
data
|
||||
@@ -58,52 +57,52 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||
const account = await getAccountFromId(data.userId);
|
||||
if (!account) return null;
|
||||
const userDetail = await cli.request('users/show', { userId: data.body.userId }, account.token);
|
||||
return [t('_notification.youWereFollowed'), {
|
||||
return [i18n.ts._notification.youWereFollowed, {
|
||||
body: getUserName(data.body.user),
|
||||
icon: data.body.user.avatarUrl,
|
||||
icon: data.body.user.avatarUrl ?? undefined,
|
||||
badge: iconUrl('user-plus'),
|
||||
data,
|
||||
actions: userDetail.isFollowing ? [] : [
|
||||
{
|
||||
action: 'follow',
|
||||
title: t('_notification._actions.followBack'),
|
||||
title: i18n.ts._notification._actions.followBack,
|
||||
},
|
||||
],
|
||||
}];
|
||||
}
|
||||
|
||||
case 'mention':
|
||||
return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), {
|
||||
return [i18n.tsx._notification.youGotMention({ name: getUserName(data.body.user) }), {
|
||||
body: data.body.note.text ?? '',
|
||||
icon: data.body.user.avatarUrl,
|
||||
icon: data.body.user.avatarUrl ?? undefined,
|
||||
badge: iconUrl('at'),
|
||||
data,
|
||||
actions: [
|
||||
{
|
||||
action: 'reply',
|
||||
title: t('_notification._actions.reply'),
|
||||
title: i18n.ts._notification._actions.reply,
|
||||
},
|
||||
],
|
||||
}];
|
||||
|
||||
case 'reply':
|
||||
return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), {
|
||||
return [i18n.tsx._notification.youGotReply({ name: getUserName(data.body.user) }), {
|
||||
body: data.body.note.text ?? '',
|
||||
icon: data.body.user.avatarUrl,
|
||||
icon: data.body.user.avatarUrl ?? undefined,
|
||||
badge: iconUrl('arrow-back-up'),
|
||||
data,
|
||||
actions: [
|
||||
{
|
||||
action: 'reply',
|
||||
title: t('_notification._actions.reply'),
|
||||
title: i18n.ts._notification._actions.reply,
|
||||
},
|
||||
],
|
||||
}];
|
||||
|
||||
case 'renote':
|
||||
return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), {
|
||||
return [i18n.tsx._notification.youRenoted({ name: getUserName(data.body.user) }), {
|
||||
body: data.body.note.text ?? '',
|
||||
icon: data.body.user.avatarUrl,
|
||||
icon: data.body.user.avatarUrl ?? undefined,
|
||||
badge: iconUrl('repeat'),
|
||||
data,
|
||||
actions: [
|
||||
@@ -115,29 +114,29 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||
}];
|
||||
|
||||
case 'quote':
|
||||
return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), {
|
||||
return [i18n.tsx._notification.youGotQuote({ name: getUserName(data.body.user) }), {
|
||||
body: data.body.note.text ?? '',
|
||||
icon: data.body.user.avatarUrl,
|
||||
icon: data.body.user.avatarUrl ?? undefined,
|
||||
badge: iconUrl('quote'),
|
||||
data,
|
||||
actions: [
|
||||
{
|
||||
action: 'reply',
|
||||
title: t('_notification._actions.reply'),
|
||||
title: i18n.ts._notification._actions.reply,
|
||||
},
|
||||
...((data.body.note.visibility === 'public' || data.body.note.visibility === 'home') ? [
|
||||
{
|
||||
action: 'renote',
|
||||
title: t('_notification._actions.renote'),
|
||||
title: i18n.ts._notification._actions.renote,
|
||||
},
|
||||
] : []),
|
||||
],
|
||||
}];
|
||||
|
||||
case 'note':
|
||||
return [t('_notification.newNote') + ': ' + getUserName(data.body.user), {
|
||||
return [i18n.ts._notification.newNote + ': ' + getUserName(data.body.user), {
|
||||
body: data.body.note.text ?? '',
|
||||
icon: data.body.user.avatarUrl,
|
||||
icon: data.body.user.avatarUrl ?? undefined,
|
||||
data,
|
||||
}];
|
||||
|
||||
@@ -164,7 +163,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||
const tag = `reaction:${data.body.note.id}`;
|
||||
return [`${reaction} ${getUserName(data.body.user)}`, {
|
||||
body: data.body.note.text ?? '',
|
||||
icon: data.body.user.avatarUrl,
|
||||
icon: data.body.user.avatarUrl ?? undefined,
|
||||
tag,
|
||||
badge,
|
||||
data,
|
||||
@@ -178,41 +177,41 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||
}
|
||||
|
||||
case 'receiveFollowRequest':
|
||||
return [t('_notification.youReceivedFollowRequest'), {
|
||||
return [i18n.ts._notification.youReceivedFollowRequest, {
|
||||
body: getUserName(data.body.user),
|
||||
icon: data.body.user.avatarUrl,
|
||||
icon: data.body.user.avatarUrl ?? undefined,
|
||||
badge: iconUrl('user-plus'),
|
||||
data,
|
||||
actions: [
|
||||
{
|
||||
action: 'accept',
|
||||
title: t('accept'),
|
||||
title: i18n.ts.accept,
|
||||
},
|
||||
{
|
||||
action: 'reject',
|
||||
title: t('reject'),
|
||||
title: i18n.ts.reject,
|
||||
},
|
||||
],
|
||||
}];
|
||||
|
||||
case 'followRequestAccepted':
|
||||
return [t('_notification.yourFollowRequestAccepted'), {
|
||||
return [i18n.ts._notification.yourFollowRequestAccepted, {
|
||||
body: getUserName(data.body.user),
|
||||
icon: data.body.user.avatarUrl,
|
||||
icon: data.body.user.avatarUrl ?? undefined,
|
||||
badge: iconUrl('circle-check'),
|
||||
data,
|
||||
}];
|
||||
|
||||
case 'achievementEarned':
|
||||
return [t('_notification.achievementEarned'), {
|
||||
body: t(`_achievements._types._${data.body.achievement}.title`),
|
||||
return [i18n.ts._notification.achievementEarned, {
|
||||
body: i18n.ts._achievements._types[`_${data.body.achievement}`].title,
|
||||
badge: iconUrl('medal'),
|
||||
data,
|
||||
tag: `achievement:${data.body.achievement}`,
|
||||
}];
|
||||
|
||||
case 'pollEnded':
|
||||
return [t('_notification.pollEnded'), {
|
||||
return [i18n.ts._notification.pollEnded, {
|
||||
body: data.body.note.text ?? '',
|
||||
badge: iconUrl('chart-arrows'),
|
||||
data,
|
||||
@@ -226,8 +225,8 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||
}];
|
||||
|
||||
case 'test':
|
||||
return [t('_notification.testNotification'), {
|
||||
body: t('_notification.notificationWillBeDisplayedLikeThis'),
|
||||
return [i18n.ts._notification.testNotification, {
|
||||
body: i18n.ts._notification.notificationWillBeDisplayedLikeThis,
|
||||
badge: iconUrl('bell'),
|
||||
data,
|
||||
}];
|
||||
@@ -236,9 +235,9 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||
return null;
|
||||
}
|
||||
case 'unreadAntennaNote':
|
||||
return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), {
|
||||
return [i18n.tsx._notification.unreadAntennaNote({ name: data.body.antenna.name }), {
|
||||
body: `${getUserName(data.body.note.user)}: ${data.body.note.text ?? ''}`,
|
||||
icon: data.body.note.user.avatarUrl,
|
||||
icon: data.body.note.user.avatarUrl ?? undefined,
|
||||
badge: iconUrl('antenna'),
|
||||
tag: `antenna:${data.body.antenna.id}`,
|
||||
data,
|
||||
@@ -252,7 +251,6 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||
export async function createEmptyNotification(): Promise<void> {
|
||||
return new Promise<void>(async res => {
|
||||
const i18n = await (swLang.i18n ?? swLang.fetchLocale());
|
||||
const { t } = i18n;
|
||||
|
||||
await globalThis.registration.showNotification(
|
||||
(new URL(origin)).host,
|
||||
@@ -264,11 +262,11 @@ export async function createEmptyNotification(): Promise<void> {
|
||||
actions: [
|
||||
{
|
||||
action: 'markAllAsRead',
|
||||
title: t('markAllAsRead'),
|
||||
title: i18n.ts.markAllAsRead,
|
||||
},
|
||||
{
|
||||
action: 'settings',
|
||||
title: t('notificationSettings'),
|
||||
title: i18n.ts.notificationSettings,
|
||||
},
|
||||
],
|
||||
data: {},
|
||||
|
@@ -4,9 +4,10 @@
|
||||
*/
|
||||
|
||||
import { get } from 'idb-keyval';
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
export async function getAccountFromId(id: string): Promise<{ token: string; id: string } | void> {
|
||||
const accounts = await get<{ token: string; id: string }[]>('accounts');
|
||||
export async function getAccountFromId(id: string): Promise<Pick<Misskey.entities.SignupResponse, 'id' | 'token'> | undefined> {
|
||||
const accounts = await get<Pick<Misskey.entities.SignupResponse, 'id' | 'token'>[]>('accounts');
|
||||
if (!accounts) {
|
||||
console.log('Accounts are not recorded');
|
||||
return;
|
||||
|
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export type Locale = { [key: string]: string | Locale };
|
||||
|
||||
export class I18n<T extends Locale = Locale> {
|
||||
public ts: T;
|
||||
|
||||
constructor(locale: T) {
|
||||
this.ts = locale;
|
||||
|
||||
//#region BIND
|
||||
this.t = this.t.bind(this);
|
||||
//#endregion
|
||||
}
|
||||
|
||||
// string にしているのは、ドット区切りでのパス指定を許可するため
|
||||
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
|
||||
public t(key: string, args?: Record<string, string>): string {
|
||||
try {
|
||||
let str = key.split('.').reduce<Locale | Locale[keyof Locale]>((o, i) => o[i], this.ts);
|
||||
if (typeof str !== 'string') throw new Error();
|
||||
|
||||
if (args) {
|
||||
for (const [k, v] of Object.entries(args)) {
|
||||
str = str.replace(`{${k}}`, v);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
} catch (err) {
|
||||
console.warn(`missing localization '${key}'`);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,7 +7,8 @@
|
||||
* Language manager for SW
|
||||
*/
|
||||
import { get, set } from 'idb-keyval';
|
||||
import { I18n, type Locale } from '@/scripts/i18n.js';
|
||||
import { I18n } from '../../../frontend/src/scripts/i18n.js';
|
||||
import type { Locale } from '../../../../locales/index.js';
|
||||
|
||||
class SwLang {
|
||||
public cacheName = `mk-cache-${_VERSION_}`;
|
||||
@@ -23,7 +24,7 @@ class SwLang {
|
||||
return this.fetchLocale();
|
||||
}
|
||||
|
||||
public i18n: Promise<I18n> | null = null;
|
||||
public i18n: Promise<I18n<Locale>> | null = null;
|
||||
|
||||
public fetchLocale(): Promise<I18n<Locale>> {
|
||||
return (this.i18n = this._fetch());
|
||||
|
@@ -14,15 +14,22 @@ import { getUrlWithLoginId } from '@/scripts/login-id.js';
|
||||
|
||||
export const cli = new Misskey.api.APIClient({ origin, fetch: (...args): Promise<Response> => fetch(...args) });
|
||||
|
||||
export async function api<E extends keyof Misskey.Endpoints, O extends Misskey.Endpoints[E]['req']>(endpoint: E, userId?: string, options?: O): Promise<void | ReturnType<typeof cli.request<E, O>>> {
|
||||
let account: { token: string; id: string } | void = undefined;
|
||||
export async function api<
|
||||
E extends keyof Misskey.Endpoints,
|
||||
P extends Misskey.Endpoints[E]['req']
|
||||
>(endpoint: E, userId?: string, params?: P): Promise<Misskey.api.SwitchCaseResponseType<E, P> | undefined> {
|
||||
let account: Pick<Misskey.entities.SignupResponse, 'id' | 'token'> | undefined;
|
||||
|
||||
if (userId) {
|
||||
account = await getAccountFromId(userId);
|
||||
if (!account) return;
|
||||
}
|
||||
|
||||
return cli.request(endpoint, options, account?.token);
|
||||
return (cli.request as <E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
) => Promise<Misskey.api.SwitchCaseResponseType<E, P>>)(endpoint, params, account?.token);
|
||||
}
|
||||
|
||||
// mark-all-as-read送出を1秒間隔に制限する
|
||||
@@ -33,7 +40,7 @@ export function sendMarkAllAsRead(userId: string): Promise<null | undefined | vo
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
readBlockingStatus.set(userId, false);
|
||||
api('notifications/mark-all-as-read', userId).then(resolve, resolve);
|
||||
(api('notifications/mark-all-as-read', userId) as Promise<void>).then(resolve, resolve);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
@@ -6,7 +6,8 @@
|
||||
import { get } from 'idb-keyval';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { PushNotificationDataMap } from '@/types.js';
|
||||
import type { I18n, Locale } from '@/scripts/i18n.js';
|
||||
import type { I18n } from '../../frontend/src/scripts/i18n.js';
|
||||
import type { Locale } from '../../../locales/index.js';
|
||||
import { createEmptyNotification, createNotification } from '@/scripts/create-notification.js';
|
||||
import { swLang } from '@/scripts/lang.js';
|
||||
import * as swos from '@/scripts/operations.js';
|
||||
@@ -30,8 +31,8 @@ globalThis.addEventListener('activate', ev => {
|
||||
async function offlineContentHTML() {
|
||||
const i18n = await (swLang.i18n ?? swLang.fetchLocale()) as Partial<I18n<Locale>>;
|
||||
const messages = {
|
||||
title: i18n.ts?._offlineScreen?.title ?? 'Offline - Could not connect to server',
|
||||
header: i18n.ts?._offlineScreen?.header ?? 'Could not connect to server',
|
||||
title: i18n.ts?._offlineScreen.title ?? 'Offline - Could not connect to server',
|
||||
header: i18n.ts?._offlineScreen.header ?? 'Could not connect to server',
|
||||
reload: i18n.ts?.reload ?? 'Reload',
|
||||
};
|
||||
|
||||
@@ -159,8 +160,8 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
|
||||
case 'markAllAsRead':
|
||||
await globalThis.registration.getNotifications()
|
||||
.then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close()));
|
||||
await get('accounts').then(accounts => {
|
||||
return Promise.all(accounts.map(async account => {
|
||||
await get<Pick<Misskey.entities.SignupResponse, 'id' | 'token'>[]>('accounts').then(accounts => {
|
||||
return Promise.all((accounts ?? []).map(async account => {
|
||||
await swos.sendMarkAllAsRead(account.id);
|
||||
}));
|
||||
});
|
||||
|
@@ -2,6 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"noEmitOnError": false,
|
||||
"noImplicitAny": false,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedParameters": false,
|
||||
"noUnusedLocals": true,
|
||||
|
Reference in New Issue
Block a user