Merge tag '13.11.0' into io
# Conflicts: # packages/backend/src/server/ServerService.ts # packages/backend/src/server/api/endpoints/notes/timeline.ts
This commit is contained in:
@@ -443,11 +443,14 @@ export const ACHIEVEMENT_BADGES = {
|
||||
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
|
||||
frame: 'bronze',
|
||||
},
|
||||
/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
|
||||
} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
|
||||
img: string;
|
||||
bg: string | null;
|
||||
frame: 'bronze' | 'silver' | 'gold' | 'platinum';
|
||||
}>;
|
||||
*/
|
||||
} as const;
|
||||
|
||||
export const claimedAchievements: typeof ACHIEVEMENT_TYPES[number][] = ($i && $i.achievements) ? $i.achievements.map(x => x.name) : [];
|
||||
|
||||
|
@@ -471,7 +471,7 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
|
||||
components.push(component);
|
||||
const instance = values.OBJ(new Map([
|
||||
['id', values.STR(_id)],
|
||||
['update', values.FN_NATIVE(async ([def], opts) => {
|
||||
['update', values.FN_NATIVE(([def], opts) => {
|
||||
utils.assertObject(def);
|
||||
const updates = getOptions(def, call);
|
||||
for (const update of def.value.keys()) {
|
||||
@@ -491,13 +491,13 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
|
||||
return {
|
||||
'Ui:root': rootInstance,
|
||||
|
||||
'Ui:patch': values.FN_NATIVE(async ([id, val], opts) => {
|
||||
'Ui:patch': values.FN_NATIVE(([id, val], opts) => {
|
||||
utils.assertString(id);
|
||||
utils.assertArray(val);
|
||||
patch(id.value, val.value, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:get': values.FN_NATIVE(async ([id], opts) => {
|
||||
'Ui:get': values.FN_NATIVE(([id], opts) => {
|
||||
utils.assertString(id);
|
||||
const instance = instances[id.value];
|
||||
if (instance) {
|
||||
@@ -508,7 +508,7 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
|
||||
}),
|
||||
|
||||
// Ui:root.update({ children: [...] }) の糖衣構文
|
||||
'Ui:render': values.FN_NATIVE(async ([children], opts) => {
|
||||
'Ui:render': values.FN_NATIVE(([children], opts) => {
|
||||
utils.assertArray(children);
|
||||
|
||||
rootComponent.value.children = children.value.map(v => {
|
||||
@@ -517,51 +517,51 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
|
||||
});
|
||||
}),
|
||||
|
||||
'Ui:C:container': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
'Ui:C:container': values.FN_NATIVE(([def, id], opts) => {
|
||||
return createComponentInstance('container', def, id, getContainerOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:text': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
'Ui:C:text': values.FN_NATIVE(([def, id], opts) => {
|
||||
return createComponentInstance('text', def, id, getTextOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:mfm': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
'Ui:C:mfm': values.FN_NATIVE(([def, id], opts) => {
|
||||
return createComponentInstance('mfm', def, id, getMfmOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:textarea': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
'Ui:C:textarea': values.FN_NATIVE(([def, id], opts) => {
|
||||
return createComponentInstance('textarea', def, id, getTextareaOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:textInput': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
'Ui:C:textInput': values.FN_NATIVE(([def, id], opts) => {
|
||||
return createComponentInstance('textInput', def, id, getTextInputOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:numberInput': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
'Ui:C:numberInput': values.FN_NATIVE(([def, id], opts) => {
|
||||
return createComponentInstance('numberInput', def, id, getNumberInputOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:button': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
'Ui:C:button': values.FN_NATIVE(([def, id], opts) => {
|
||||
return createComponentInstance('button', def, id, getButtonOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:buttons': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
'Ui:C:buttons': values.FN_NATIVE(([def, id], opts) => {
|
||||
return createComponentInstance('buttons', def, id, getButtonsOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:switch': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
'Ui:C:switch': values.FN_NATIVE(([def, id], opts) => {
|
||||
return createComponentInstance('switch', def, id, getSwitchOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:select': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
'Ui:C:select': values.FN_NATIVE(([def, id], opts) => {
|
||||
return createComponentInstance('select', def, id, getSelectOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:folder': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
'Ui:C:folder': values.FN_NATIVE(([def, id], opts) => {
|
||||
return createComponentInstance('folder', def, id, getFolderOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:postFormButton': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
'Ui:C:postFormButton': values.FN_NATIVE(([def, id], opts) => {
|
||||
return createComponentInstance('postFormButton', def, id, getPostFormButtonOptions, opts.call);
|
||||
}),
|
||||
};
|
||||
|
@@ -5,7 +5,7 @@ import { $i } from '@/account';
|
||||
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): Promise<Endpoints[E]['res']> {
|
||||
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']> {
|
||||
pendingApiRequestsCount.value++;
|
||||
|
||||
const onFinally = () => {
|
||||
@@ -26,6 +26,7 @@ export function api<E extends keyof Endpoints, P extends Endpoints[E]['req']>(en
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal,
|
||||
}).then(async (res) => {
|
||||
const body = res.status === 204 ? null : await res.json();
|
||||
|
||||
|
80
packages/frontend/src/scripts/cache.ts
Normal file
80
packages/frontend/src/scripts/cache.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
|
||||
export class Cache<T> {
|
||||
private cachedAt: number | null = null;
|
||||
private value: T | undefined;
|
||||
private lifetime: number;
|
||||
|
||||
constructor(lifetime: Cache<never>['lifetime']) {
|
||||
this.lifetime = lifetime;
|
||||
}
|
||||
|
||||
public set(value: T): void {
|
||||
this.cachedAt = Date.now();
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public get(): T | undefined {
|
||||
if (this.cachedAt == null) return undefined;
|
||||
if ((Date.now() - this.cachedAt) > this.lifetime) {
|
||||
this.value = undefined;
|
||||
this.cachedAt = null;
|
||||
return undefined;
|
||||
}
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public delete() {
|
||||
this.value = undefined;
|
||||
this.cachedAt = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||
*/
|
||||
public async fetch(fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
|
||||
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();
|
||||
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;
|
||||
}
|
||||
}
|
93
packages/frontend/src/scripts/get-drive-file-menu.ts
Normal file
93
packages/frontend/src/scripts/get-drive-file-menu.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
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';
|
||||
|
||||
function rename(file: Misskey.entities.DriveFile) {
|
||||
os.inputText({
|
||||
title: i18n.ts.renameFile,
|
||||
placeholder: i18n.ts.inputNewFileName,
|
||||
default: file.name,
|
||||
}).then(({ canceled, result: name }) => {
|
||||
if (canceled) return;
|
||||
os.api('drive/files/update', {
|
||||
fileId: file.id,
|
||||
name: name,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function describe(file: Misskey.entities.DriveFile) {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
|
||||
default: file.comment != null ? file.comment : '',
|
||||
file: file,
|
||||
}, {
|
||||
done: caption => {
|
||||
os.api('drive/files/update', {
|
||||
fileId: file.id,
|
||||
comment: caption.length === 0 ? null : caption,
|
||||
});
|
||||
},
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
function toggleSensitive(file: Misskey.entities.DriveFile) {
|
||||
os.api('drive/files/update', {
|
||||
fileId: file.id,
|
||||
isSensitive: !file.isSensitive,
|
||||
});
|
||||
}
|
||||
|
||||
function copyUrl(file: Misskey.entities.DriveFile) {
|
||||
copyToClipboard(file.url);
|
||||
os.success();
|
||||
}
|
||||
/*
|
||||
function addApp() {
|
||||
alert('not implemented yet');
|
||||
}
|
||||
*/
|
||||
async function deleteFile(file: Misskey.entities.DriveFile) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('driveFileDeleteConfirm', { name: file.name }),
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
os.api('drive/files/delete', {
|
||||
fileId: file.id,
|
||||
});
|
||||
}
|
||||
|
||||
export function getDriveFileMenu(file: Misskey.entities.DriveFile) {
|
||||
return [{
|
||||
text: i18n.ts.rename,
|
||||
icon: 'ti ti-forms',
|
||||
action: () => rename(file),
|
||||
}, {
|
||||
text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
|
||||
icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-off',
|
||||
action: () => toggleSensitive(file),
|
||||
}, {
|
||||
text: i18n.ts.describeFile,
|
||||
icon: 'ti ti-text-caption',
|
||||
action: () => describe(file),
|
||||
}, null, {
|
||||
text: i18n.ts.copyUrl,
|
||||
icon: 'ti ti-link',
|
||||
action: () => copyUrl(file),
|
||||
}, {
|
||||
type: 'a',
|
||||
href: file.url,
|
||||
target: '_blank',
|
||||
text: i18n.ts.download,
|
||||
icon: 'ti ti-download',
|
||||
download: file.name,
|
||||
}, null, {
|
||||
text: i18n.ts.delete,
|
||||
icon: 'ti ti-trash',
|
||||
danger: true,
|
||||
action: () => deleteFile(file),
|
||||
}];
|
||||
}
|
@@ -10,6 +10,81 @@ import { url } from '@/config';
|
||||
import { noteActions } from '@/store';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
import { getUserMenu } from '@/scripts/get-user-menu';
|
||||
import { clipsCache } from '@/cache';
|
||||
|
||||
export async function getNoteClipMenu(props: {
|
||||
note: misskey.entities.Note;
|
||||
isDeleted: Ref<boolean>;
|
||||
currentClip?: misskey.entities.Clip;
|
||||
}) {
|
||||
const isRenote = (
|
||||
props.note.renote != null &&
|
||||
props.note.text == null &&
|
||||
props.note.fileIds.length === 0 &&
|
||||
props.note.poll == null
|
||||
);
|
||||
|
||||
const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
|
||||
|
||||
const clips = await clipsCache.fetch(() => os.api('clips/list'));
|
||||
return [...clips.map(clip => ({
|
||||
text: clip.name,
|
||||
action: () => {
|
||||
claimAchievement('noteClipped1');
|
||||
os.promiseDialog(
|
||||
os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
|
||||
null,
|
||||
async (err) => {
|
||||
if (err.id === '734806c4-542c-463a-9311-15c512803965') {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }),
|
||||
});
|
||||
if (!confirm.canceled) {
|
||||
os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
|
||||
if (props.currentClip?.id === clip.id) props.isDeleted.value = true;
|
||||
}
|
||||
} else {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err.message + '\n' + err.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
})), null, {
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.createNew,
|
||||
action: async () => {
|
||||
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
|
||||
name: {
|
||||
type: 'string',
|
||||
label: i18n.ts.name,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
multiline: true,
|
||||
label: i18n.ts.description,
|
||||
},
|
||||
isPublic: {
|
||||
type: 'boolean',
|
||||
label: i18n.ts.public,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const clip = await os.apiWithDialog('clips/create', result);
|
||||
|
||||
clipsCache.delete();
|
||||
|
||||
claimAchievement('noteClipped1');
|
||||
os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
export function getNoteMenu(props: {
|
||||
note: misskey.entities.Note;
|
||||
@@ -17,7 +92,7 @@ export function getNoteMenu(props: {
|
||||
translation: Ref<any>;
|
||||
translating: Ref<boolean>;
|
||||
isDeleted: Ref<boolean>;
|
||||
currentClipPage?: Ref<misskey.entities.Clip>;
|
||||
currentClip?: misskey.entities.Clip;
|
||||
}) {
|
||||
const isRenote = (
|
||||
props.note.renote != null &&
|
||||
@@ -101,7 +176,7 @@ export function getNoteMenu(props: {
|
||||
}
|
||||
|
||||
async function unclip(): Promise<void> {
|
||||
os.apiWithDialog('clips/remove-note', { clipId: props.currentClipPage.value.id, noteId: appearNote.id });
|
||||
os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id });
|
||||
props.isDeleted.value = true;
|
||||
}
|
||||
|
||||
@@ -155,7 +230,7 @@ export function getNoteMenu(props: {
|
||||
|
||||
menu = [
|
||||
...(
|
||||
props.currentClipPage?.value.userId === $i.id ? [{
|
||||
props.currentClip?.userId === $i.id ? [{
|
||||
icon: 'ti ti-backspace',
|
||||
text: i18n.ts.unclip,
|
||||
danger: true,
|
||||
@@ -208,64 +283,7 @@ export function getNoteMenu(props: {
|
||||
type: 'parent',
|
||||
icon: 'ti ti-paperclip',
|
||||
text: i18n.ts.clip,
|
||||
children: async () => {
|
||||
const clips = await os.api('clips/list');
|
||||
return [{
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.createNew,
|
||||
action: async () => {
|
||||
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
|
||||
name: {
|
||||
type: 'string',
|
||||
label: i18n.ts.name,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
multiline: true,
|
||||
label: i18n.ts.description,
|
||||
},
|
||||
isPublic: {
|
||||
type: 'boolean',
|
||||
label: i18n.ts.public,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const clip = await os.apiWithDialog('clips/create', result);
|
||||
|
||||
claimAchievement('noteClipped1');
|
||||
os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
|
||||
},
|
||||
}, null, ...clips.map(clip => ({
|
||||
text: clip.name,
|
||||
action: () => {
|
||||
claimAchievement('noteClipped1');
|
||||
os.promiseDialog(
|
||||
os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
|
||||
null,
|
||||
async (err) => {
|
||||
if (err.id === '734806c4-542c-463a-9311-15c512803965') {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }),
|
||||
});
|
||||
if (!confirm.canceled) {
|
||||
os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
|
||||
if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true;
|
||||
}
|
||||
} else {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err.message + '\n' + err.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
}))];
|
||||
},
|
||||
children: () => getNoteClipMenu(props),
|
||||
},
|
||||
statePromise.then(state => state.isMutedThread ? {
|
||||
icon: 'ti ti-message-off',
|
||||
@@ -276,7 +294,7 @@ export function getNoteMenu(props: {
|
||||
text: i18n.ts.muteThread,
|
||||
action: () => toggleThreadMute(true),
|
||||
}),
|
||||
appearNote.userId === $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? {
|
||||
appearNote.userId === $i.id ? ($i.pinnedNoteIds ?? []).includes(appearNote.id) ? {
|
||||
icon: 'ti ti-pinned-off',
|
||||
text: i18n.ts.unpin,
|
||||
action: () => togglePin(false),
|
||||
|
@@ -8,6 +8,7 @@ import { userActions } from '@/store';
|
||||
import { $i, iAmModerator } from '@/account';
|
||||
import { mainRouter } from '@/router';
|
||||
import { Router } from '@/nirax';
|
||||
import { rolesCache, userListsCache } from '@/cache';
|
||||
|
||||
export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) {
|
||||
const meId = $i ? $i.id : null;
|
||||
@@ -53,6 +54,14 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleRenoteMute() {
|
||||
os.apiWithDialog(user.isRenoteMuted ? 'renote-mute/delete' : 'renote-mute/create', {
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
user.isRenoteMuted = !user.isRenoteMuted;
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleBlock() {
|
||||
if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return;
|
||||
|
||||
@@ -111,14 +120,14 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
|
||||
icon: 'ti ti-mail',
|
||||
text: i18n.ts.sendMessage,
|
||||
action: () => {
|
||||
os.post({ specified: user });
|
||||
os.post({ specified: user, initialText: `@${user.username} ` });
|
||||
},
|
||||
}, null, {
|
||||
type: 'parent',
|
||||
icon: 'ti ti-list',
|
||||
text: i18n.ts.addToList,
|
||||
children: async () => {
|
||||
const lists = await os.api('users/lists/list');
|
||||
const lists = await userListsCache.fetch(() => os.api('users/lists/list'));
|
||||
|
||||
return lists.map(list => ({
|
||||
text: list.name,
|
||||
@@ -139,7 +148,7 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
|
||||
icon: 'ti ti-badges',
|
||||
text: i18n.ts.roles,
|
||||
children: async () => {
|
||||
const roles = await os.api('admin/roles/list');
|
||||
const roles = await rolesCache.fetch(() => os.api('admin/roles/list'));
|
||||
|
||||
return roles.filter(r => r.target === 'manual').map(r => ({
|
||||
text: r.name,
|
||||
@@ -179,6 +188,10 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
|
||||
icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off',
|
||||
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
|
||||
action: toggleMute,
|
||||
}, {
|
||||
icon: user.isRenoteMuted ? 'ti ti-repeat' : 'ti ti-repeat-off',
|
||||
text: user.isRenoteMuted ? i18n.ts.renoteUnmute : i18n.ts.renoteMute,
|
||||
action: toggleRenoteMute,
|
||||
}, {
|
||||
icon: 'ti ti-ban',
|
||||
text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block,
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import { ref, Ref, unref } from 'vue';
|
||||
import { collectPageVars } from '../collect-page-vars';
|
||||
import { initHpmlLib } from './lib';
|
||||
@@ -51,7 +50,6 @@ export class Hpml {
|
||||
this.eval();
|
||||
}
|
||||
|
||||
@autobind
|
||||
public eval() {
|
||||
try {
|
||||
this.vars.value = this.evaluateVars();
|
||||
@@ -60,7 +58,6 @@ export class Hpml {
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public interpolate(str: string) {
|
||||
if (str == null) return null;
|
||||
return str.replace(/{(.+?)}/g, match => {
|
||||
@@ -69,12 +66,10 @@ export class Hpml {
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
public registerCanvas(id: string, canvas: any) {
|
||||
this.canvases[id] = canvas;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public updatePageVar(name: string, value: any) {
|
||||
const pageVar = this.pageVars.find(v => v.name === name);
|
||||
if (pageVar !== undefined) {
|
||||
@@ -84,13 +79,11 @@ export class Hpml {
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public updateRandomSeed(seed: string) {
|
||||
this.opts.randomSeed = seed;
|
||||
this.envVars.SEED = seed;
|
||||
}
|
||||
|
||||
@autobind
|
||||
private _interpolateScope(str: string, scope: HpmlScope) {
|
||||
return str.replace(/{(.+?)}/g, match => {
|
||||
const v = scope.getState(match.slice(1, -1).trim());
|
||||
@@ -98,7 +91,6 @@ export class Hpml {
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
public evaluateVars(): Record<string, any> {
|
||||
const values: Record<string, any> = {};
|
||||
|
||||
@@ -117,7 +109,6 @@ export class Hpml {
|
||||
return values;
|
||||
}
|
||||
|
||||
@autobind
|
||||
private evaluate(expr: Expr, scope: HpmlScope): any {
|
||||
if (isLiteralValue(expr)) {
|
||||
if (expr.type === null) {
|
||||
|
@@ -2,7 +2,6 @@
|
||||
* Hpml
|
||||
*/
|
||||
|
||||
import autobind from 'autobind-decorator';
|
||||
import { Hpml } from './evaluator';
|
||||
import { funcDefs } from './lib';
|
||||
|
||||
@@ -61,7 +60,6 @@ export class HpmlScope {
|
||||
this.name = name ?? 'anonymous';
|
||||
}
|
||||
|
||||
@autobind
|
||||
public createChildScope(states: Record<string, any>, name?: HpmlScope['name']): HpmlScope {
|
||||
const layer = [states, ...this.layerdStates];
|
||||
return new HpmlScope(layer, name);
|
||||
@@ -71,7 +69,6 @@ export class HpmlScope {
|
||||
* 指定した名前の変数の値を取得します
|
||||
* @param name 変数名
|
||||
*/
|
||||
@autobind
|
||||
public getState(name: string): any {
|
||||
for (const later of this.layerdStates) {
|
||||
const state = later[name];
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import { isLiteralValue } from './expr';
|
||||
import { funcDefs } from './lib';
|
||||
import { envVarsDef } from '.';
|
||||
@@ -23,7 +22,6 @@ export class HpmlTypeChecker {
|
||||
this.pageVars = pageVars;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public typeCheck(v: Expr): TypeError | null {
|
||||
if (isLiteralValue(v)) return null;
|
||||
|
||||
@@ -61,7 +59,6 @@ export class HpmlTypeChecker {
|
||||
return null;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getExpectedType(v: Expr, slot: number): Type {
|
||||
const def = funcDefs[v.type ?? ''];
|
||||
if (def == null) {
|
||||
@@ -89,7 +86,6 @@ export class HpmlTypeChecker {
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public infer(v: Expr): Type {
|
||||
if (v.type === null) return null;
|
||||
if (v.type === 'text') return 'string';
|
||||
@@ -144,7 +140,6 @@ export class HpmlTypeChecker {
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getVarByName(name: string): Variable {
|
||||
const v = this.variables.find(x => x.name === name);
|
||||
if (v !== undefined) {
|
||||
@@ -154,25 +149,21 @@ export class HpmlTypeChecker {
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getVarsByType(type: Type): Variable[] {
|
||||
if (type == null) return this.variables;
|
||||
return this.variables.filter(x => (this.infer(x) === null) || (this.infer(x) === type));
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getEnvVarsByType(type: Type): string[] {
|
||||
if (type == null) return Object.keys(envVarsDef);
|
||||
return Object.entries(envVarsDef).filter(([k, v]) => v === null || type === v).map(([k, v]) => k);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getPageVarsByType(type: Type): string[] {
|
||||
if (type == null) return this.pageVars.map(v => v.name);
|
||||
return this.pageVars.filter(v => type === v.type).map(v => v.name);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public isUsedName(name: string) {
|
||||
if (this.variables.some(v => v.name === name)) {
|
||||
return true;
|
||||
|
41
packages/frontend/src/scripts/lookup.ts
Normal file
41
packages/frontend/src/scripts/lookup.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { mainRouter } from '@/router';
|
||||
import { Router } from '@/nirax';
|
||||
|
||||
export async function lookup(router?: Router) {
|
||||
const _router = router ?? mainRouter;
|
||||
|
||||
const { canceled, result: query } = await os.inputText({
|
||||
title: i18n.ts.lookup,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
if (query.startsWith('@') && !query.includes(' ')) {
|
||||
_router.push(`/${query}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (query.startsWith('#')) {
|
||||
_router.push(`/tags/${encodeURIComponent(query.substr(1))}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (query.startsWith('https://')) {
|
||||
const promise = os.api('ap/show', {
|
||||
uri: query,
|
||||
});
|
||||
|
||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||
|
||||
const res = await promise;
|
||||
|
||||
if (res.type === 'User') {
|
||||
_router.push(`/@${res.object.username}@${res.object.host}`);
|
||||
} else if (res.type === 'Note') {
|
||||
_router.push(`/notes/${res.object.id}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
@@ -10,7 +10,10 @@ export function getProxiedImageUrl(imageUrl: string, type?: 'preview', mustOrigi
|
||||
imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
|
||||
}
|
||||
|
||||
return `${mustOrigin ? localProxy : instance.mediaProxy}/image.webp?${query({
|
||||
return `${mustOrigin ? localProxy : instance.mediaProxy}/${
|
||||
type === 'preview' ? 'preview.webp'
|
||||
: 'image.webp'
|
||||
}?${query({
|
||||
url: imageUrl,
|
||||
fallback: '1',
|
||||
...(type ? { [type]: '1' } : {}),
|
||||
|
@@ -133,8 +133,8 @@ export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioEle
|
||||
return audio;
|
||||
}
|
||||
|
||||
export function play(type: string) {
|
||||
const sound = ColdDeviceStorage.get('sound_' + type as any);
|
||||
export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') {
|
||||
const sound = ColdDeviceStorage.get(`sound_${type}`);
|
||||
if (sound.type == null) return;
|
||||
playFile(sound.type, sound.volume);
|
||||
}
|
||||
|
6
packages/frontend/src/scripts/test-utils.ts
Normal file
6
packages/frontend/src/scripts/test-utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <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);
|
||||
}
|
Reference in New Issue
Block a user