Merge tag '2023.9.0' into merge-upstream

This commit is contained in:
riku6460
2023-09-25 12:43:07 +09:00
1235 changed files with 19016 additions and 13835 deletions

View File

@@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as os from '@/os';
import { $i } from '@/account';
import * as os from '@/os.js';
import { $i } from '@/account.js';
export const ACHIEVEMENT_TYPES = [
'notes1',
@@ -81,6 +81,7 @@ export const ACHIEVEMENT_TYPES = [
'setNameToSyuilo',
'cookieClicked',
'brainDiver',
'smashTestNotificationButton',
] as const;
export const ACHIEVEMENT_BADGES = {
@@ -454,6 +455,11 @@ export const ACHIEVEMENT_BADGES = {
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
frame: 'bronze',
},
'smashTestNotificationButton': {
img: '/fluent-emoji/1f514.png',
bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
frame: 'bronze',
},
/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
img: string;

View File

@@ -4,18 +4,19 @@
*/
import { utils, values } from '@syuilo/aiscript';
import * as os from '@/os';
import { $i } from '@/account';
import { miLocalStorage } from '@/local-storage';
import { customEmojis } from '@/custom-emojis';
import * as os from '@/os.js';
import { $i } from '@/account.js';
import { miLocalStorage } from '@/local-storage.js';
import { customEmojis } from '@/custom-emojis.js';
import { lang } from '@/config.js';
export function createAiScriptEnv(opts) {
let apiRequests = 0;
return {
USER_ID: $i ? values.STR($i.id) : values.NULL,
USER_NAME: $i ? values.STR($i.name) : values.NULL,
USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value),
LOCALE: values.STR(lang),
'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
await os.alert({
type: type ? type.value : 'info',
@@ -33,15 +34,19 @@ export function createAiScriptEnv(opts) {
return confirm.canceled ? values.FALSE : values.TRUE;
}),
'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => {
utils.assertString(ep);
if (ep.value.includes('://')) throw new Error('invalid endpoint');
if (token) {
utils.assertString(token);
// バグがあればundefinedもあり得るため念のため
if (typeof token.value !== 'string') throw new Error('invalid token');
}
apiRequests++;
if (apiRequests > 16) return values.NULL;
const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token ?? null));
return utils.jsToVal(res);
const actualToken: string|null = token?.value ?? opts.token ?? null;
return os.api(ep.value, utils.valToJs(param), actualToken).then(res => {
return utils.jsToVal(res);
}, err => {
return values.ERROR('request_failed', utils.jsToVal(err));
});
}),
'Mk:save': values.FN_NATIVE(([key, value]) => {
utils.assertString(key);

View File

@@ -124,7 +124,14 @@ export type AsUiPostFormButton = AsUiComponentBase & {
};
};
export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton;
export type AsUiPostForm = AsUiComponentBase & {
type: 'postForm';
form?: {
text: string;
};
};
export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton | AsUiPostForm;
export function patch(id: string, def: values.Value, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
// TODO
@@ -462,6 +469,27 @@ function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: valu
};
}
function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiPostForm, 'id' | 'type'> {
utils.assertObject(def);
const form = def.value.get('form');
if (form) utils.assertObject(form);
const getForm = () => {
const text = form!.value.get('text');
utils.assertString(text);
return {
text: text.value,
};
};
return {
form: form ? getForm() : {
text: '',
},
};
}
export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: Ref<AsUiRoot>) => void) {
const instances = {};
@@ -523,51 +551,55 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
}),
'Ui:C:container': values.FN_NATIVE(([def, id], opts) => {
return createComponentInstance('container', def, id, getContainerOptions, opts.call);
return createComponentInstance('container', def, id, getContainerOptions, opts.topCall);
}),
'Ui:C:text': values.FN_NATIVE(([def, id], opts) => {
return createComponentInstance('text', def, id, getTextOptions, opts.call);
return createComponentInstance('text', def, id, getTextOptions, opts.topCall);
}),
'Ui:C:mfm': values.FN_NATIVE(([def, id], opts) => {
return createComponentInstance('mfm', def, id, getMfmOptions, opts.call);
return createComponentInstance('mfm', def, id, getMfmOptions, opts.topCall);
}),
'Ui:C:textarea': values.FN_NATIVE(([def, id], opts) => {
return createComponentInstance('textarea', def, id, getTextareaOptions, opts.call);
return createComponentInstance('textarea', def, id, getTextareaOptions, opts.topCall);
}),
'Ui:C:textInput': values.FN_NATIVE(([def, id], opts) => {
return createComponentInstance('textInput', def, id, getTextInputOptions, opts.call);
return createComponentInstance('textInput', def, id, getTextInputOptions, opts.topCall);
}),
'Ui:C:numberInput': values.FN_NATIVE(([def, id], opts) => {
return createComponentInstance('numberInput', def, id, getNumberInputOptions, opts.call);
return createComponentInstance('numberInput', def, id, getNumberInputOptions, opts.topCall);
}),
'Ui:C:button': values.FN_NATIVE(([def, id], opts) => {
return createComponentInstance('button', def, id, getButtonOptions, opts.call);
return createComponentInstance('button', def, id, getButtonOptions, opts.topCall);
}),
'Ui:C:buttons': values.FN_NATIVE(([def, id], opts) => {
return createComponentInstance('buttons', def, id, getButtonsOptions, opts.call);
return createComponentInstance('buttons', def, id, getButtonsOptions, opts.topCall);
}),
'Ui:C:switch': values.FN_NATIVE(([def, id], opts) => {
return createComponentInstance('switch', def, id, getSwitchOptions, opts.call);
return createComponentInstance('switch', def, id, getSwitchOptions, opts.topCall);
}),
'Ui:C:select': values.FN_NATIVE(([def, id], opts) => {
return createComponentInstance('select', def, id, getSelectOptions, opts.call);
return createComponentInstance('select', def, id, getSelectOptions, opts.topCall);
}),
'Ui:C:folder': values.FN_NATIVE(([def, id], opts) => {
return createComponentInstance('folder', def, id, getFolderOptions, opts.call);
return createComponentInstance('folder', def, id, getFolderOptions, opts.topCall);
}),
'Ui:C:postFormButton': values.FN_NATIVE(([def, id], opts) => {
return createComponentInstance('postFormButton', def, id, getPostFormButtonOptions, opts.call);
return createComponentInstance('postFormButton', def, id, getPostFormButtonOptions, opts.topCall);
}),
'Ui:C:postForm': values.FN_NATIVE(([def, id], opts) => {
return createComponentInstance('postForm', def, id, getPostFormOptions, opts.topCall);
}),
};
}

View File

@@ -3,21 +3,21 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Endpoints } from 'misskey-js/built/api.types';
import * as Misskey from 'misskey-js';
import { ref } from 'vue';
import { apiUrl } from '@/config';
import { $i } from '@/account';
import { apiUrl } from '@/config.js';
import { $i } from '@/account.js';
export const pendingApiRequestsCount = ref(0);
// Implements Misskey.api.ApiClient.request
export function api<E extends keyof Endpoints, P extends Endpoints[E]['req']>(endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal): Promise<Endpoints[E]['res']> {
export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal): Promise<Misskey.Endpoints[E]['res']> {
pendingApiRequestsCount.value++;
const onFinally = () => {
pendingApiRequestsCount.value--;
};
const promise = new Promise<Endpoints[E]['res'] | void>((resolve, reject) => {
const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => {
// Append a credential
if ($i) (data as any).i = $i.token;
if (token !== undefined) (data as any).i = token;
@@ -51,7 +51,7 @@ export function api<E extends keyof Endpoints, P extends Endpoints[E]['req']>(en
}
// Implements Misskey.api.ApiClient.request
export function apiGet <E extends keyof Endpoints, P extends Endpoints[E]['req']>(endpoint: E, data: P = {} as any): Promise<Endpoints[E]['res']> {
export function apiGet <E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(endpoint: E, data: P = {} as any): Promise<Misskey.Endpoints[E]['res']> {
pendingApiRequestsCount.value++;
const onFinally = () => {
@@ -60,7 +60,7 @@ export function apiGet <E extends keyof Endpoints, P extends Endpoints[E]['req']
const query = new URLSearchParams(data as any);
const promise = new Promise<Endpoints[E]['res'] | void>((resolve, reject) => {
const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => {
// Send request
window.fetch(`${apiUrl}/${endpoint}?${query}`, {
method: 'GET',

View File

@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { EndoRelation, Predicate } from './relation';
type EndoRelation<T> = (a: T, b: T) => boolean;
type Predicate<T> = (x: T) => boolean;
/**
* Count the number of elements that satisfy the predicate

View File

@@ -6,7 +6,7 @@
import { nextTick, Ref, ref, defineAsyncComponent } from 'vue';
import getCaretCoordinates from 'textarea-caret';
import { toASCII } from 'punycode/';
import { popup } from '@/os';
import { popup } from '@/os.js';
export class Autocomplete {
private suggestion: {

View File

@@ -9,9 +9,11 @@ export class Cache<T> {
private cachedAt: number | null = null;
public value = ref<T | undefined>();
private lifetime: number;
private fetcher: () => Promise<T>;
constructor(lifetime: Cache<never>['lifetime']) {
constructor(lifetime: Cache<never>['lifetime'], fetcher: () => Promise<T>) {
this.lifetime = lifetime;
this.fetcher = fetcher;
}
public set(value: T): void {
@@ -35,51 +37,17 @@ export class Cache<T> {
/**
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
public async fetch(fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
public async fetch(): Promise<T> {
const cachedValue = this.get();
if (cachedValue !== undefined) {
if (validator) {
if (validator(cachedValue)) {
// Cache HIT
return cachedValue;
}
} else {
// Cache HIT
return cachedValue;
}
// Cache HIT
return cachedValue;
}
// Cache MISS
const value = await fetcher();
const value = await this.fetcher();
this.set(value);
return value;
}
/**
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
public async fetchMaybe(fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
const cachedValue = this.get();
if (cachedValue !== undefined) {
if (validator) {
if (validator(cachedValue)) {
// Cache HIT
return cachedValue;
}
} else {
// Cache HIT
return cachedValue;
}
}
// Cache MISS
const value = await fetcher();
if (value !== undefined) {
this.set(value);
}
return value;
}
}

View File

@@ -4,7 +4,7 @@
*/
import { ref, computed } from 'vue';
import * as os from '@/os';
import * as os from '@/os.js';
type SaveData = {
gameVersion: number;

View File

@@ -4,10 +4,10 @@
*/
import * as mfm from 'mfm-js';
import * as misskey from 'misskey-js';
import { extractUrlFromMfm } from './extract-url-from-mfm';
import * as Misskey from 'misskey-js';
import { extractUrlFromMfm } from './extract-url-from-mfm.js';
export function shouldCollapsed(note: misskey.entities.Note): boolean {
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')) ||

View File

@@ -4,7 +4,7 @@
*/
import _confetti from 'canvas-confetti';
import * as os from '@/os';
import * as os from '@/os.js';
export function confetti(options: { duration?: number; } = {}) {
const duration = options.duration ?? 1000 * 4;

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { defaultStore } from '@/store';
import { defaultStore } from '@/store.js';
await defaultStore.ready;

View File

@@ -4,7 +4,7 @@
*/
import * as mfm from 'mfm-js';
import { unique } from '@/scripts/array';
import { unique } from '@/scripts/array.js';
// unique without hash
// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ]

View File

@@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Acct from 'misskey-js/built/acct';
import { host as localHost } from '@/config';
import * as Misskey from 'misskey-js';
import { host as localHost } from '@/config.js';
export async function genSearchQuery(v: any, q: string) {
let host: string;
@@ -18,7 +18,7 @@ export async function genSearchQuery(v: any, q: string) {
host = at;
}
} else {
const user = await v.os.api('users/show', Acct.parse(at)).catch(x => null);
const user = await v.os.api('users/show', Misskey.acct.parse(at)).catch(x => null);
if (user) {
userId = user.id;
} else {

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { get } from '@/scripts/idb-proxy';
import { get } from '@/scripts/idb-proxy.js';
export async function getAccountFromId(id: string) {
const accounts = await get('accounts') as { token: string; id: string; }[];

View File

@@ -5,11 +5,11 @@
import * as Misskey from 'misskey-js';
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';
import { i18n } from '@/i18n.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import * as os from '@/os.js';
import { MenuItem } from '@/types/menu.js';
import { defaultStore } from '@/store.js';
function rename(file: Misskey.entities.DriveFile) {
os.inputText({

View File

@@ -4,23 +4,24 @@
*/
import { defineAsyncComponent, Ref } from 'vue';
import * as misskey from 'misskey-js';
import { claimAchievement } from './achievements';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
import * as os from '@/os';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import { url } from '@/config';
import { defaultStore, noteActions } from '@/store';
import { miLocalStorage } from '@/local-storage';
import { getUserMenu } from '@/scripts/get-user-menu';
import { clipsCache } from '@/cache';
import * as Misskey from 'misskey-js';
import { claimAchievement } from './achievements.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js';
import { defaultStore, noteActions } from '@/store.js';
import { miLocalStorage } from '@/local-storage.js';
import { getUserMenu } from '@/scripts/get-user-menu.js';
import { clipsCache } from '@/cache.js';
import { MenuItem } from '@/types/menu.js';
export async function getNoteClipMenu(props: {
note: misskey.entities.Note;
note: Misskey.entities.Note;
isDeleted: Ref<boolean>;
currentClip?: misskey.entities.Clip;
currentClip?: Misskey.entities.Clip;
}) {
const isRenote = (
props.note.renote != null &&
@@ -29,9 +30,9 @@ export async function getNoteClipMenu(props: {
props.note.poll == null
);
const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
const clips = await clipsCache.fetch(() => os.api('clips/list'));
const clips = await clipsCache.fetch();
return [...clips.map(clip => ({
text: clip.name,
action: () => {
@@ -91,13 +92,38 @@ export async function getNoteClipMenu(props: {
}];
}
export function getAbuseNoteMenu(note: misskey.entities.Note, text: string): MenuItem {
return {
icon: 'ti ti-exclamation-circle',
text,
action: (): void => {
const u = note.url ?? note.uri ?? `${url}/notes/${note.id}`;
os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
user: note.user,
initialComment: `Note: ${u}\n-----\n`,
}, {}, 'closed');
},
};
}
export function getCopyNoteLinkMenu(note: misskey.entities.Note, text: string): MenuItem {
return {
icon: 'ti ti-link',
text,
action: (): void => {
copyToClipboard(`${url}/notes/${note.id}`);
os.success();
},
};
}
export function getNoteMenu(props: {
note: misskey.entities.Note;
note: Misskey.entities.Note;
menuButton: Ref<HTMLElement>;
translation: Ref<any>;
translating: Ref<boolean>;
isDeleted: Ref<boolean>;
currentClip?: misskey.entities.Clip;
currentClip?: Misskey.entities.Clip;
}) {
const isRenote = (
props.note.renote != null &&
@@ -106,7 +132,9 @@ export function getNoteMenu(props: {
props.note.poll == null
);
const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
const cleanups = [] as (() => void)[];
function del(): void {
os.confirm({
@@ -210,18 +238,6 @@ export function getNoteMenu(props: {
os.pageWindow(`/notes/${appearNote.id}`);
}
function showReactions(): void {
os.popup(defineAsyncComponent(() => import('@/components/MkReactedUsersDialog.vue')), {
noteId: appearNote.id,
}, {}, 'closed');
}
function showRenotes(): void {
os.popup(defineAsyncComponent(() => import('@/components/MkRenotedUsersDialog.vue')), {
noteId: appearNote.id,
}, {}, 'closed');
}
async function translate(): Promise<void> {
if (props.translation.value != null) return;
props.translating.value = true;
@@ -233,7 +249,7 @@ export function getNoteMenu(props: {
props.translation.value = res;
}
let menu;
let menu: MenuItem[];
if ($i) {
const statePromise = os.api('notes/state', {
noteId: appearNote.id,
@@ -251,23 +267,12 @@ export function getNoteMenu(props: {
icon: 'ti ti-info-circle',
text: i18n.ts.details,
action: openDetail,
}, {
icon: 'ti ti-repeat',
text: i18n.ts.renotesList,
action: showRenotes,
}, {
icon: 'ti ti-icons',
text: i18n.ts.reactionsList,
action: showReactions,
}, {
icon: 'ti ti-copy',
text: i18n.ts.copyContent,
action: copyContent,
}, {
icon: 'ti ti-link',
text: i18n.ts.copyLink,
action: copyLink,
}, (appearNote.url || appearNote.uri) ? {
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)
, (appearNote.url || appearNote.uri) ? {
icon: 'ti ti-external-link',
text: i18n.ts.showOnRemote,
action: () => {
@@ -295,7 +300,7 @@ export function getNoteMenu(props: {
action: () => toggleFavorite(true),
}),
{
type: 'parent',
type: 'parent' as const,
icon: 'ti ti-paperclip',
text: i18n.ts.clip,
children: () => getNoteClipMenu(props),
@@ -318,15 +323,17 @@ export function getNoteMenu(props: {
text: i18n.ts.pin,
action: () => togglePin(true),
} : undefined,
appearNote.userId !== $i.id ? {
type: 'parent',
{
type: 'parent' as const,
icon: 'ti ti-user',
text: i18n.ts.user,
children: async () => {
const user = await os.api('users/show', { userId: appearNote.userId });
return getUserMenu(user);
const user = appearNote.userId === $i?.id ? $i : await os.api('users/show', { userId: appearNote.userId });
const { menu, cleanup } = getUserMenu(user);
cleanups.push(cleanup);
return menu;
},
} : undefined,
},
/*
...($i.isModerator || $i.isAdmin ? [
null,
@@ -339,17 +346,8 @@ export function getNoteMenu(props: {
),*/
...(appearNote.userId !== $i.id ? [
null,
{
icon: 'ti ti-exclamation-circle',
text: i18n.ts.reportAbuse,
action: () => {
const u = appearNote.url ?? appearNote.uri ?? `${url}/notes/${appearNote.id}`;
os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
user: appearNote.user,
initialComment: `Note: ${u}\n-----\n`,
}, {}, 'closed');
},
}]
appearNote.userId !== $i.id ? getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse) : undefined,
]
: []
),
...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
@@ -377,11 +375,8 @@ export function getNoteMenu(props: {
icon: 'ti ti-copy',
text: i18n.ts.copyContent,
action: copyContent,
}, {
icon: 'ti ti-link',
text: i18n.ts.copyLink,
action: copyLink,
}, (appearNote.url || appearNote.uri) ? {
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)
, (appearNote.url || appearNote.uri) ? {
icon: 'ti ti-external-link',
text: i18n.ts.showOnRemote,
action: () => {
@@ -411,5 +406,15 @@ export function getNoteMenu(props: {
}]);
}
return menu;
const cleanup = () => {
if (_DEV_) console.log('note menu cleanup', cleanups);
for (const cl of cleanups) {
cl();
}
};
return {
menu,
cleanup,
};
}

View File

@@ -3,14 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as misskey from 'misskey-js';
import { i18n } from '@/i18n';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
/**
* 投稿を表す文字列を取得します。
* @param {*} note (packされた)投稿
*/
export const getNoteSummary = (note: misskey.entities.Note): string => {
export const getNoteSummary = (note: Misskey.entities.Note): string => {
if (note.deletedAt) {
return `(${i18n.ts.deletedNote})`;
}

View File

@@ -4,21 +4,23 @@
*/
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, 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 { antennasCache, rolesCache, userListsCache } from '@/cache';
import { defineAsyncComponent, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { host, url } from '@/config.js';
import * as os from '@/os.js';
import { defaultStore, userActions } from '@/store.js';
import { $i, iAmModerator } from '@/account.js';
import { mainRouter } from '@/router.js';
import { Router } from '@/nirax.js';
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) {
export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router = mainRouter) {
const meId = $i ? $i.id : null;
const cleanups = [] as (() => void)[];
async function toggleMute() {
if (user.isMuted) {
os.apiWithDialog('mute/delete', {
@@ -78,6 +80,15 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
});
}
async function toggleNotify() {
os.apiWithDialog('following/update', {
userId: user.id,
notify: user.notify === 'normal' ? 'none' : 'normal',
}).then(() => {
user.notify = user.notify === 'normal' ? 'none' : 'normal';
});
}
function reportAbuse() {
os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
user: user,
@@ -131,13 +142,13 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
action: () => {
copyToClipboard(`@${user.username}@${user.host ?? host}`);
},
}, {
icon: 'ti ti-info-circle',
text: i18n.ts.info,
}, ...(iAmModerator ? [{
icon: 'ti ti-user-exclamation',
text: i18n.ts.moderation,
action: () => {
router.push(`/user-info/${user.id}`);
router.push(`/admin/user/${user.id}`);
},
}, {
}] : []), {
icon: 'ti ti-rss',
text: i18n.ts.copyRSS,
action: () => {
@@ -154,7 +165,8 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
icon: 'ti ti-mail',
text: i18n.ts.sendMessage,
action: () => {
os.post({ specified: user, initialText: `@${user.username} ` });
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`;
os.post({ specified: user, initialText: `${canonical} ` });
},
}, null, {
icon: 'ti ti-pencil',
@@ -167,25 +179,40 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
icon: 'ti ti-list',
text: i18n.ts.addToList,
children: async () => {
const lists = await userListsCache.fetch(() => os.api('users/lists/list'));
const lists = await userListsCache.fetch();
return lists.map(list => {
const isListed = ref(list.userIds.includes(user.id));
cleanups.push(watch(isListed, () => {
if (isListed.value) {
os.apiWithDialog('users/lists/push', {
listId: list.id,
userId: user.id,
}).then(() => {
list.userIds.push(user.id);
});
} else {
os.apiWithDialog('users/lists/pull', {
listId: list.id,
userId: user.id,
}).then(() => {
list.userIds.splice(list.userIds.indexOf(user.id), 1);
});
}
}));
return lists.map(list => ({
text: list.name,
action: async () => {
await os.apiWithDialog('users/lists/push', {
listId: list.id,
userId: user.id,
});
userListsCache.delete();
},
}));
return {
type: 'switch',
text: list.name,
ref: isListed,
};
});
},
}, {
type: 'parent',
icon: 'ti ti-antenna',
text: i18n.ts.addToAntenna,
children: async () => {
const antennas = await antennasCache.fetch(() => os.api('antennas/list'));
const antennas = await antennasCache.fetch();
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
return antennas.filter((a) => a.src === 'users').map(antenna => ({
text: antenna.name,
@@ -216,7 +243,7 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
icon: 'ti ti-badges',
text: i18n.ts.roles,
children: async () => {
const roles = await rolesCache.fetch(() => os.api('admin/roles/list'));
const roles = await rolesCache.fetch();
return roles.filter(r => r.target === 'manual').map(r => ({
text: r.name,
@@ -252,6 +279,15 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
}]);
}
// フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため
//if (user.isFollowing) {
menu = menu.concat([{
icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off',
text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes,
action: toggleNotify,
}]);
//}
menu = menu.concat([null, {
icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off',
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
@@ -311,5 +347,15 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
}))]);
}
return menu;
const cleanup = () => {
if (_DEV_) console.log('user menu cleanup', cleanups);
for (const cl of cleanups) {
cl();
}
};
return {
menu,
cleanup,
};
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import keyCode from './keycode';
import keyCode from './keycode.js';
type Callback = (ev: KeyboardEvent) => void;

View File

@@ -24,7 +24,7 @@ import {
import gradient from 'chartjs-plugin-gradient';
import zoomPlugin from 'chartjs-plugin-zoom';
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
import { defaultStore } from '@/store';
import { defaultStore } from '@/store.js';
import 'chartjs-adapter-date-fns';
export function initChart() {

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { lang } from '@/config';
import { lang } from '@/config.js';
export async function initializeSw() {
if (!('serviceWorker' in navigator)) return;

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { lang } from '@/config';
import { lang } from '@/config.js';
export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP');
export const dateTimeFormat = new Intl.DateTimeFormat(versatileLang, {

View File

@@ -3,10 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as misskey from 'misskey-js';
import { $i } from '@/account';
import * as Misskey from 'misskey-js';
import { $i } from '@/account.js';
export function isFfVisibleForMe(user: misskey.entities.UserDetailed): boolean {
export function isFfVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
if ($i && $i.id === user.id) return true;
if (user.ffVisibility === 'private') return false;

View File

@@ -3,9 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Acct from 'misskey-js/built/acct';
import { i18n } from '@/i18n';
import * as os from '@/os';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
export async function lookupUser() {
const { canceled, result } = await os.inputText({
@@ -14,10 +14,10 @@ export async function lookupUser() {
if (canceled) return;
const show = (user) => {
os.pageWindow(`/user-info/${user.id}`);
os.pageWindow(`/admin/user/${user.id}`);
};
const usernamePromise = os.api('users/show', Acct.parse(result));
const usernamePromise = os.api('users/show', Misskey.acct.parse(result));
const idPromise = os.api('users/show', { userId: result });
let _notFound = false;
const notFound = () => {

View File

@@ -3,10 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as os from '@/os';
import { i18n } from '@/i18n';
import { mainRouter } from '@/router';
import { Router } from '@/nirax';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { mainRouter } from '@/router.js';
import { Router } from '@/nirax.js';
export async function lookup(router?: Router) {
const _router = router ?? mainRouter;

View File

@@ -3,9 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { query } from '@/scripts/url';
import { url } from '@/config';
import { instance } from '@/instance';
import { query } from '@/scripts/url.js';
import { url } from '@/config.js';
import { instance } from '@/instance.js';
export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string {
const localProxy = `${url}/proxy`;

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as misskey from 'misskey-js';
import * as Misskey from 'misskey-js';
import { ComputedRef, inject, isRef, onActivated, onMounted, provide, ref, Ref } from 'vue';
export const setPageMetadata = Symbol('setPageMetadata');
@@ -13,8 +13,8 @@ export type PageMetadata = {
title: string;
subtitle?: string;
icon?: string | null;
avatar?: misskey.entities.User | null;
userName?: misskey.entities.User | null;
avatar?: Misskey.entities.User | null;
userName?: Misskey.entities.User | null;
};
export function definePageMetadata(metadata: PageMetadata | null | Ref<PageMetadata | null> | ComputedRef<PageMetadata | null>): void {

View File

@@ -4,9 +4,9 @@
*/
import { defineAsyncComponent } from 'vue';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import { popup } from '@/os';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { popup } from '@/os.js';
export function pleaseLogin(path?: string) {
if ($i) return;

View File

@@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { appendQuery } from './url';
import * as config from '@/config';
import { appendQuery } from './url.js';
import * as config from '@/config.js';
export function popout(path: string, w?: HTMLElement) {
let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path;

View File

@@ -4,7 +4,7 @@
*/
import { defineAsyncComponent, Ref, ref } from 'vue';
import { popup } from '@/os';
import { popup } from '@/os.js';
class ReactionPicker {
private src: Ref<HTMLElement | null> = ref(null);

View File

@@ -30,7 +30,7 @@ export function getScrollPosition(el: HTMLElement | null): number {
export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) {
// とりあえず評価してみる
if (isTopVisible(el)) {
if (el.isConnected && isTopVisible(el)) {
cb();
if (once) return null;
}
@@ -54,7 +54,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
const container = getScrollContainer(el);
// とりあえず評価してみる
if (isBottomVisible(el, tolerance, container)) {
if (el.isConnected && isBottomVisible(el, tolerance, container)) {
cb();
if (once) return null;
}

View File

@@ -4,14 +4,14 @@
*/
import { ref } from 'vue';
import { DriveFile } from 'misskey-js/built/entities';
import * as os from '@/os';
import { useStream } from '@/stream';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
import { uploadFile } from '@/scripts/upload';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { uploadFile } from '@/scripts/upload.js';
export function chooseFileFromPc(multiple: boolean, keepOriginal = false): Promise<DriveFile[]> {
export function chooseFileFromPc(multiple: boolean, keepOriginal = false): Promise<Misskey.entities.DriveFile[]> {
return new Promise((res, rej) => {
const input = document.createElement('input');
input.type = 'file';
@@ -38,7 +38,7 @@ export function chooseFileFromPc(multiple: boolean, keepOriginal = false): Promi
});
}
export function chooseFileFromDrive(multiple: boolean): Promise<DriveFile[]> {
export function chooseFileFromDrive(multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
return new Promise((res, rej) => {
os.selectDriveFile(multiple).then(files => {
res(files);
@@ -46,7 +46,7 @@ export function chooseFileFromDrive(multiple: boolean): Promise<DriveFile[]> {
});
}
export function chooseFileFromUrl(): Promise<DriveFile> {
export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
return new Promise((res, rej) => {
os.inputText({
title: i18n.ts.uploadFromUrl,
@@ -79,7 +79,7 @@ export function chooseFileFromUrl(): Promise<DriveFile> {
});
}
function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile[]> {
function select(src: any, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
return new Promise((res, rej) => {
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
@@ -106,10 +106,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
});
}
export function selectFile(src: any, label: string | null = null): Promise<DriveFile> {
export function selectFile(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile> {
return select(src, label, false).then(files => files[0]);
}
export function selectFiles(src: any, label: string | null = null): Promise<DriveFile[]> {
export function selectFiles(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
return select(src, label, true);
}

View File

@@ -3,9 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as os from '@/os';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import * as os from '@/os.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
export function showMovedDialog() {
if (!$i) return;

View File

@@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as os from '@/os';
import { i18n } from '@/i18n';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
export function showSuspendedDialog() {
return os.alert({

View File

@@ -4,7 +4,7 @@
*/
import { markRaw } from 'vue';
import { Storage } from '@/pizzax';
import { Storage } from '@/pizzax.js';
export const soundConfigStore = markRaw(new Storage('sound', {
mediaVolume: {

View File

@@ -3,8 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
/// <reference types="@testing-library/jest-dom"/>
export async function tick(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
await new Promise((globalThis.requestIdleCallback ?? setTimeout) as never);

View File

@@ -5,7 +5,7 @@
import { v4 as uuid } from 'uuid';
import { themeProps, Theme } from './theme';
import { themeProps, Theme } from './theme.js';
export type Default = null;
export type Color = string;

View File

@@ -19,7 +19,7 @@ export type Theme = {
import lightTheme from '@/themes/_light.json5';
import darkTheme from '@/themes/_dark.json5';
import { deepClone } from './clone';
import { miLocalStorage } from '@/local-storage';
import { miLocalStorage } from '@/local-storage.js';
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { deviceKind } from '@/scripts/device-kind';
import { deviceKind } from '@/scripts/device-kind.js';
const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;

View File

@@ -6,12 +6,12 @@
import { reactive, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { readAndCompressImage } from 'browser-image-resizer';
import { getCompressionConfig } from './upload/compress-config';
import { defaultStore } from '@/store';
import { apiUrl } from '@/config';
import { $i } from '@/account';
import { alert } from '@/os';
import { i18n } from '@/i18n';
import { getCompressionConfig } from './upload/compress-config.js';
import { defaultStore } from '@/store.js';
import { apiUrl } from '@/config.js';
import { $i } from '@/account.js';
import { alert } from '@/os.js';
import { i18n } from '@/i18n.js';
type Uploading = {
id: string;

View File

@@ -4,7 +4,7 @@
*/
import { onUnmounted, onDeactivated, ref } from 'vue';
import * as os from '@/os';
import * as os from '@/os.js';
import MkChartTooltip from '@/components/MkChartTooltip.vue';
export function useChartTooltip(opts: { position: 'top' | 'middle' } = { position: 'top' }) {

View File

@@ -4,13 +4,13 @@
*/
import { onUnmounted, Ref } from 'vue';
import * as misskey from 'misskey-js';
import { useStream } from '@/stream';
import { $i } from '@/account';
import * as Misskey from 'misskey-js';
import { useStream } from '@/stream.js';
import { $i } from '@/account.js';
export function useNoteCapture(props: {
rootEl: Ref<HTMLElement>;
note: Ref<misskey.entities.Note>;
note: Ref<Misskey.entities.Note>;
isDeletedRef: Ref<boolean>;
}) {
const note = props.note;