Merge branch 'develop' into fix-msg-room

This commit is contained in:
tamaina 2022-02-01 23:35:51 +09:00
commit 927317b5bb
36 changed files with 454 additions and 356 deletions

View file

@ -10,13 +10,19 @@
## 12.x.x (unreleased) ## 12.x.x (unreleased)
### Improvements ### Improvements
- 連合インスタンスページからインスタンス情報再取得を行えるように
### Bugfixes ### Bugfixes
- 投稿のNSFW画像を表示したあとにリアクションが更新されると画像が非表示になる問題を修正 - 投稿のNSFW画像を表示したあとにリアクションが更新されると画像が非表示になる問題を修正
- 「クリップ」ページが開かない問題を修正 - 「クリップ」ページが開かない問題を修正
- トレンドウィジェットが動作しないのを修正 - トレンドウィジェットが動作しないのを修正
- フェデレーションウィジェットが動作しないのを修正
- リアクション設定で絵文字ピッカーが開かないのを修正 - リアクション設定で絵文字ピッカーが開かないのを修正
- DMページでメンションが含まれる問題を修正 - DMページでメンションが含まれる問題を修正
- 投稿フォームのハッシュタグ保持フィールドが動作しない問題を修正
- Add `img-src` and `media-src` directives to `Content-Security-Policy` for
files and media proxy
- ensure that specified users does not get duplicates
## 12.102.1 (2022/01/27) ## 12.102.1 (2022/01/27)
### Bugfixes ### Bugfixes

View file

@ -3,7 +3,7 @@ We're glad you're interested in contributing Misskey! In this document you will
** Important:** This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.** ** Important:** This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.**
Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\ Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\
The accuracy of translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language. The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language.
It will also allow the reader to use the translation tool of their preference if necessary. It will also allow the reader to use the translation tool of their preference if necessary.
## Issues ## Issues

View file

@ -176,3 +176,7 @@ describe('After user singed in', () => {
cy.contains('Hello, Misskey!'); cy.contains('Hello, Misskey!');
}); });
}); });
// TODO: 投稿フォームの公開範囲指定のテスト
// TODO: 投稿フォームのファイル添付のテスト
// TODO: 投稿フォームのハッシュタグ保持フィールドのテスト

View file

@ -235,6 +235,8 @@ resetAreYouSure: "リセットしますか?"
saved: "保存しました" saved: "保存しました"
messaging: "チャット" messaging: "チャット"
upload: "アップロード" upload: "アップロード"
keepOriginalUploading: "オリジナル画像を保持"
keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。"
fromDrive: "ドライブから" fromDrive: "ドライブから"
fromUrl: "URLから" fromUrl: "URLから"
uploadFromUrl: "URLアップロード" uploadFromUrl: "URLアップロード"

View file

@ -122,7 +122,7 @@
"langmap": "0.0.16", "langmap": "0.0.16",
"mfm-js": "0.21.0", "mfm-js": "0.21.0",
"mime-types": "2.1.34", "mime-types": "2.1.34",
"misskey-js": "0.0.13", "misskey-js": "0.0.14",
"mocha": "8.4.0", "mocha": "8.4.0",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"multer": "1.4.4", "multer": "1.4.4",

View file

@ -32,7 +32,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => {
// Authentication // Authentication
authenticate(body['i']).then(([user, app]) => { authenticate(body['i']).then(([user, app]) => {
// API invoking // API invoking
call(endpoint.name, user, app, body, (ctx as any).file).then((res: any) => { call(endpoint.name, user, app, body, ctx).then((res: any) => {
reply(res); reply(res);
}).catch((e: ApiError) => { }).catch((e: ApiError) => {
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e); reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);

View file

@ -1,3 +1,4 @@
import * as Koa from 'koa';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { limiter } from './limiter'; import { limiter } from './limiter';
import { User } from '@/models/entities/user'; import { User } from '@/models/entities/user';
@ -12,7 +13,7 @@ const accessDenied = {
id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e', id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e',
}; };
export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, file?: any) => { export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => {
const isSecure = user != null && token == null; const isSecure = user != null && token == null;
const ep = endpoints.find(e => e.name === endpoint); const ep = endpoints.find(e => e.name === endpoint);
@ -76,9 +77,20 @@ export default async (endpoint: string, user: User | null | undefined, token: Ac
}); });
} }
// Cast non JSON input
if (ep.meta.requireFile && ep.meta.params) {
const body = (ctx!.request as any).body;
for (const k of Object.keys(ep.meta.params)) {
const param = ep.meta.params[k];
if (['Boolean', 'Number'].includes(param.validator.name) && typeof body[k] === 'string') {
body[k] = JSON.parse(body[k]);
}
}
}
// API invoking // API invoking
const before = performance.now(); const before = performance.now();
return await ep.exec(data, user, token, file).catch((e: Error) => { return await ep.exec(data, user, token, ctx!.file).catch((e: Error) => {
if (e instanceof ApiError) { if (e instanceof ApiError) {
throw e; throw e;
} else { } else {

View file

@ -39,15 +39,13 @@ export const meta = {
}, },
isSensitive: { isSensitive: {
validator: $.optional.either($.bool, $.str), validator: $.optional.bool,
default: false, default: false,
transform: (v: any): boolean => v === true || v === 'true',
}, },
force: { force: {
validator: $.optional.either($.bool, $.str), validator: $.optional.bool,
default: false, default: false,
transform: (v: any): boolean => v === true || v === 'true',
}, },
}, },

View file

@ -18,7 +18,7 @@ const _dirname = dirname(_filename);
const app = new Koa(); const app = new Koa();
app.use(cors()); app.use(cors());
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`); ctx.set('Content-Security-Policy', `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`);
await next(); await next();
}); });

View file

@ -11,7 +11,7 @@ import { proxyMedia } from './proxy-media';
const app = new Koa(); const app = new Koa();
app.use(cors()); app.use(cors());
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`); ctx.set('Content-Security-Policy', `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`);
await next(); await next();
}); });

View file

@ -4967,10 +4967,10 @@ minizlib@^2.0.0, minizlib@^2.1.1:
minipass "^3.0.0" minipass "^3.0.0"
yallist "^4.0.0" yallist "^4.0.0"
misskey-js@0.0.13: misskey-js@0.0.14:
version "0.0.13" version "0.0.14"
resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.13.tgz#03a4e469186e28752d599dc4093519eb64647970" resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d"
integrity sha512-kBdJdfe281gtykzzsrN3IAxWUQIimzPiJGyKWf863ggWJlWYVPmP9hTFlX2z8oPOaypgVBPEPHyw/jNUdc2DbQ== integrity sha512-bvLx6U3OwQwqHfp/WKwIVwdvNYAAPk0+YblXyxmSG3dwlzCgBRRLcB8o6bNruUDyJgh3t73pLDcOz3myxcUmww==
dependencies: dependencies:
autobind-decorator "^2.4.0" autobind-decorator "^2.4.0"
eventemitter3 "^4.0.7" eventemitter3 "^4.0.7"

View file

@ -18,6 +18,7 @@ module.exports = {
// data の禁止理由: 抽象的すぎるため // data の禁止理由: 抽象的すぎるため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
"id-denylist": ["error", "window", "data", "e"], "id-denylist": ["error", "window", "data", "e"],
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
"vue/attributes-order": ["error", { "vue/attributes-order": ["error", {
"alphabetical": false "alphabetical": false
}], }],

View file

@ -69,7 +69,7 @@
"langmap": "0.0.16", "langmap": "0.0.16",
"matter-js": "0.18.0", "matter-js": "0.18.0",
"mfm-js": "0.21.0", "mfm-js": "0.21.0",
"misskey-js": "0.0.13", "misskey-js": "0.0.14",
"mocha": "8.4.0", "mocha": "8.4.0",
"ms": "2.1.3", "ms": "2.1.3",
"nested-property": "4.0.0", "nested-property": "4.0.0",

View file

@ -0,0 +1,51 @@
<template>
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" @closed="emit('closed')">
<div v-if="title" class="qpcyisrl">
<div class="title">{{ title }}</div>
<div v-for="x in series" class="series">
<span class="color" :style="{ background: x.backgroundColor, borderColor: x.borderColor }"></span>
<span>{{ x.text }}</span>
</div>
</div>
</MkTooltip>
</template>
<script lang="ts" setup>
import { } from 'vue';
import MkTooltip from './ui/tooltip.vue';
const props = defineProps<{
showing: boolean;
x: number;
y: number;
title: string;
series: {
backgroundColor: string;
borderColor: string;
text: string;
}[];
}>();
const emit = defineEmits<{
(ev: 'closed'): void;
}>();
</script>
<style lang="scss" scoped>
.qpcyisrl {
> .title {
margin-bottom: 4px;
}
> .series {
> .color {
display: inline-block;
width: 8px;
height: 8px;
border-width: 1px;
border-style: solid;
margin-right: 8px;
}
}
}
</style>

View file

@ -8,7 +8,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted, ref, watch, PropType } from 'vue'; import { defineComponent, onMounted, ref, watch, PropType, onUnmounted, shallowRef } from 'vue';
import { import {
Chart, Chart,
ArcElement, ArcElement,
@ -31,6 +31,7 @@ import { enUS } from 'date-fns/locale';
import zoomPlugin from 'chartjs-plugin-zoom'; import zoomPlugin from 'chartjs-plugin-zoom';
import * as os from '@/os'; import * as os from '@/os';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import MkChartTooltip from '@/components/chart-tooltip.vue';
Chart.register( Chart.register(
ArcElement, ArcElement,
@ -137,6 +138,43 @@ export default defineComponent({
})); }));
}; };
const tooltipShowing = ref(false);
const tooltipX = ref(0);
const tooltipY = ref(0);
const tooltipTitle = ref(null);
const tooltipSeries = ref(null);
let disposeTooltipComponent;
os.popup(MkChartTooltip, {
showing: tooltipShowing,
x: tooltipX,
y: tooltipY,
title: tooltipTitle,
series: tooltipSeries,
}, {}).then(({ dispose }) => {
disposeTooltipComponent = dispose;
});
function externalTooltipHandler(context) {
if (context.tooltip.opacity === 0) {
tooltipShowing.value = false;
return;
}
tooltipTitle.value = context.tooltip.title[0];
tooltipSeries.value = context.tooltip.body.map((b, i) => ({
backgroundColor: context.tooltip.labelColors[i].backgroundColor,
borderColor: context.tooltip.labelColors[i].borderColor,
text: b.lines[0],
}));
const rect = context.chart.canvas.getBoundingClientRect();
tooltipShowing.value = true;
tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX;
tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY;
}
const render = () => { const render = () => {
if (chartInstance) { if (chartInstance) {
chartInstance.destroy(); chartInstance.destroy();
@ -222,10 +260,12 @@ export default defineComponent({
}, },
}, },
tooltip: { tooltip: {
enabled: false,
mode: 'index', mode: 'index',
animation: { animation: {
duration: 0, duration: 0,
}, },
external: externalTooltipHandler,
}, },
zoom: { zoom: {
pan: { pan: {
@ -684,6 +724,10 @@ export default defineComponent({
fetchAndRender(); fetchAndRender();
}); });
onUnmounted(() => {
if (disposeTooltipComponent) disposeTooltipComponent();
});
return { return {
chartEl, chartEl,
fetching, fetching,

View file

@ -117,7 +117,7 @@ export default defineComponent({
text: computed(() => { text: computed(() => {
return props.textConverter(finalValue.value); return props.textConverter(finalValue.value);
}), }),
source: thumbEl, targetElement: thumbEl,
}, {}, 'closed'); }, {}, 'closed');
const style = document.createElement('style'); const style = document.createElement('style');

View file

@ -20,45 +20,33 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, ref, toRefs } from 'vue'; import { toRefs, Ref } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import Ripple from '@/components/ripple.vue'; import Ripple from '@/components/ripple.vue';
export default defineComponent({ const props = defineProps<{
props: { modelValue: boolean | Ref<boolean>;
modelValue: { disabled?: boolean;
type: Boolean, }>();
default: false
},
disabled: {
type: Boolean,
default: false
}
},
setup(props, context) { const emit = defineEmits<{
const button = ref<HTMLElement>(); (e: 'update:modelValue', v: boolean): void;
}>();
let button = $ref<HTMLElement>();
const checked = toRefs(props).modelValue; const checked = toRefs(props).modelValue;
const toggle = () => { const toggle = () => {
if (props.disabled) return; if (props.disabled) return;
context.emit('update:modelValue', !checked.value); emit('update:modelValue', !checked.value);
if (!checked.value) { if (!checked.value) {
const rect = button.value.getBoundingClientRect(); const rect = button.getBoundingClientRect();
const x = rect.left + (button.value.offsetWidth / 2); const x = rect.left + (button.offsetWidth / 2);
const y = rect.top + (button.value.offsetHeight / 2); const y = rect.top + (button.offsetHeight / 2);
os.popup(Ripple, { x, y, particle: false }, {}, 'end'); os.popup(Ripple, { x, y, particle: false }, {}, 'end');
} }
}; };
return {
button,
checked,
toggle,
};
},
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -153,7 +153,7 @@ export default defineComponent({
showing, showing,
reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction, reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
emojis: props.notification.note.emojis, emojis: props.notification.note.emojis,
source: reactionRef.value.$el, targetElement: reactionRef.value.$el,
}, {}, 'closed'); }, {}, 'closed');
}); });

View file

@ -135,7 +135,10 @@ let showPreview = $ref(false);
let cw = $ref<string | null>(null); let cw = $ref<string | null>(null);
let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly); let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly);
let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof misskey.noteVisibilities[number]); let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof misskey.noteVisibilities[number]);
let visibleUsers = $ref(props.initialVisibleUsers ?? []); let visibleUsers = $ref([]);
if (props.initialVisibleUsers) {
props.initialVisibleUsers.forEach(pushVisibleUser);
}
let autocomplete = $ref(null); let autocomplete = $ref(null);
let draghover = $ref(false); let draghover = $ref(false);
let quoteId = $ref(null); let quoteId = $ref(null);
@ -262,12 +265,12 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
os.api('users/show', { os.api('users/show', {
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId) userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId)
}).then(users => { }).then(users => {
visibleUsers.push(...users); users.forEach(pushVisibleUser);
}); });
if (props.reply.userId !== $i.id) { if (props.reply.userId !== $i.id) {
os.api('users/show', { userId: props.reply.userId }).then(user => { os.api('users/show', { userId: props.reply.userId }).then(user => {
visibleUsers.push(user); pushVisibleUser(user);
}); });
} }
} }
@ -275,7 +278,7 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
if (props.specified) { if (props.specified) {
visibility = 'specified'; visibility = 'specified';
visibleUsers.push(props.specified); pushVisibleUser(props.specified);
} }
// keep cw when reply // keep cw when reply
@ -397,9 +400,15 @@ function setVisibility() {
}, 'closed'); }, 'closed');
} }
function pushVisibleUser(user) {
if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) {
visibleUsers.push(user);
}
}
function addVisibleUser() { function addVisibleUser() {
os.selectUser().then(user => { os.selectUser().then(user => {
visibleUsers.push(user); pushVisibleUser(user);
}); });
} }
@ -540,8 +549,8 @@ async function post() {
}; };
if (withHashtags && hashtags && hashtags.trim() !== '') { if (withHashtags && hashtags && hashtags.trim() !== '') {
const hashtags = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); const hashtags_ = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
data.text = data.text ? `${data.text} ${hashtags}` : hashtags; data.text = data.text ? `${data.text} ${hashtags_}` : hashtags_;
} }
// plugin // plugin
@ -565,9 +574,9 @@ async function post() {
deleteDraft(); deleteDraft();
emit('posted'); emit('posted');
if (data.text && data.text != '') { if (data.text && data.text != '') {
const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); const hashtags_ = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); localStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
} }
posting = false; posting = false;
postAccount = null; postAccount = null;

View file

@ -1,5 +1,5 @@
<template> <template>
<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')"> <MkTooltip ref="tooltip" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
<div class="beeadbfb"> <div class="beeadbfb">
<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> <XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
<div class="name">{{ reaction.replace('@.', '') }}</div> <div class="name">{{ reaction.replace('@.', '') }}</div>
@ -15,11 +15,11 @@ import XReactionIcon from './reaction-icon.vue';
const props = defineProps<{ const props = defineProps<{
reaction: string; reaction: string;
emojis: any[]; // TODO emojis: any[]; // TODO
source: any; // TODO targetElement: HTMLElement;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')"> <MkTooltip ref="tooltip" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
<div class="bqxuuuey"> <div class="bqxuuuey">
<div class="reaction"> <div class="reaction">
<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> <XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
@ -26,11 +26,11 @@ const props = defineProps<{
users: any[]; // TODO users: any[]; // TODO
count: number; count: number;
emojis: any[]; // TODO emojis: any[]; // TODO
source: any; // TODO targetElement: HTMLElement;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="emit('closed')"> <MkTooltip ref="tooltip" :target-element="targetElement" :max-width="250" @closed="emit('closed')">
<div class="beaffaef"> <div class="beaffaef">
<div v-for="u in users" :key="u.id" class="user"> <div v-for="u in users" :key="u.id" class="user">
<MkAvatar class="avatar" :user="u"/> <MkAvatar class="avatar" :user="u"/>
@ -17,11 +17,11 @@ import MkTooltip from './ui/tooltip.vue';
const props = defineProps<{ const props = defineProps<{
users: any[]; // TODO users: any[]; // TODO
count: number; count: number;
source: any; // TODO targetElement: HTMLElement;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
</script> </script>

View file

@ -1,53 +1,37 @@
<template> <template>
<transition :name="$store.state.animation ? 'fade' : ''" appear> <transition :name="$store.state.animation ? 'fade' : ''" appear>
<div class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> <div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
<MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/> <MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/>
</div> </div>
</transition> </transition>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { onMounted, onBeforeUnmount } from 'vue';
import contains from '@/scripts/contains'; import contains from '@/scripts/contains';
import MkMenu from './menu.vue'; import MkMenu from './menu.vue';
import { MenuItem } from './types/menu.vue';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ const props = defineProps<{
components: { items: MenuItem[];
MkMenu, ev: MouseEvent;
}, }>();
props: {
items: {
type: Array,
required: true
},
ev: {
required: true
},
viaKeyboard: {
type: Boolean,
required: false
},
},
emits: ['closed'],
data() {
return {
zIndex: os.claimZIndex('high'),
};
},
computed: {
keymap(): any {
return {
'esc': () => this.$emit('closed'),
};
},
},
mounted() {
let left = this.ev.pageX + 1; // + 1
let top = this.ev.pageY + 1; // + 1
const width = this.$el.offsetWidth; const emit = defineEmits<{
const height = this.$el.offsetHeight; (e: 'closed'): void;
}>();
let rootEl = $ref<HTMLDivElement>();
let zIndex = $ref<number>(os.claimZIndex('high'));
onMounted(() => {
let left = props.ev.pageX + 1; // + 1
let top = props.ev.pageY + 1; // + 1
const width = rootEl.offsetWidth;
const height = rootEl.offsetHeight;
if (left + width - window.pageXOffset > window.innerWidth) { if (left + width - window.pageXOffset > window.innerWidth) {
left = window.innerWidth - width + window.pageXOffset; left = window.innerWidth - width + window.pageXOffset;
@ -65,24 +49,23 @@ export default defineComponent({
left = 0; left = 0;
} }
this.$el.style.top = top + 'px'; rootEl.style.top = `${top}px`;
this.$el.style.left = left + 'px'; rootEl.style.left = `${left}px`;
for (const el of Array.from(document.querySelectorAll('body *'))) { for (const el of Array.from(document.querySelectorAll('body *'))) {
el.addEventListener('mousedown', this.onMousedown); el.addEventListener('mousedown', onMousedown);
}
},
beforeUnmount() {
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.removeEventListener('mousedown', this.onMousedown);
}
},
methods: {
onMousedown(e) {
if (!contains(this.$el, e.target) && (this.$el != e.target)) this.$emit('closed');
},
} }
}); });
onBeforeUnmount(() => {
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.removeEventListener('mousedown', onMousedown);
}
});
function onMousedown(e: Event) {
if (!contains(rootEl, e.target) && (rootEl != e.target)) emit('closed');
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,8 +1,8 @@
<template> <template>
<div ref="items" v-hotkey="keymap" <div ref="itemsEl" v-hotkey="keymap"
class="rrevdjwt" class="rrevdjwt"
:class="{ center: align === 'center', asDrawer }" :class="{ center: align === 'center', asDrawer }"
:style="{ width: (width && !asDrawer) ? width + 'px' : null, maxHeight: maxHeight ? maxHeight + 'px' : null }" :style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
@contextmenu.self="e => e.preventDefault()" @contextmenu.self="e => e.preventDefault()"
> >
<template v-for="(item, i) in items2"> <template v-for="(item, i) in items2">
@ -28,6 +28,9 @@
<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/> <MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> <span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</button> </button>
<span v-else-if="item.type === 'switch'" :tabindex="i" class="item">
<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
</span>
<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)"> <button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)">
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i> <i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> <MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
@ -41,114 +44,78 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, ref, unref } from 'vue'; import { nextTick, onMounted, watch } from 'vue';
import { focusPrev, focusNext } from '@/scripts/focus'; import { focusPrev, focusNext } from '@/scripts/focus';
import contains from '@/scripts/contains'; import FormSwitch from '@/components/form/switch.vue';
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
export default defineComponent({ const props = defineProps<{
props: { items: MenuItem[];
items: { viaKeyboard?: boolean;
type: Array, asDrawer?: boolean;
required: true align?: 'center' | string;
}, width?: number;
viaKeyboard: { maxHeight?: number;
type: Boolean, }>();
required: false
},
asDrawer: {
type: Boolean,
required: false
},
align: {
type: String,
requried: false
},
width: {
type: Number,
required: false
},
maxHeight: {
type: Number,
required: false
},
},
emits: ['close'],
data() {
return {
items2: [],
};
},
computed: {
keymap(): any {
return {
'up|k|shift+tab': this.focusUp,
'down|j|tab': this.focusDown,
'esc': this.close,
};
},
},
watch: {
items: {
handler() {
const items = ref(unref(this.items).filter(item => item !== undefined));
for (let i = 0; i < items.value.length; i++) { const emit = defineEmits<{
const item = items.value[i]; (e: 'close'): void;
}>();
if (item && item.then) { // if item is Promise let itemsEl = $ref<HTMLDivElement>();
items.value[i] = { type: 'pending' };
let items2: InnerMenuItem[] = $ref([]);
let keymap = $computed(() => ({
'up|k|shift+tab': focusUp,
'down|j|tab': focusDown,
'esc': close,
}));
watch(() => props.items, () => {
const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item && 'then' in item) { // if item is Promise
items[i] = { type: 'pending' };
item.then(actualItem => { item.then(actualItem => {
items.value[i] = actualItem; items2[i] = actualItem;
}); });
} }
} }
this.items2 = items; items2 = items as InnerMenuItem[];
}, }, {
immediate: true immediate: true,
} });
},
mounted() { onMounted(() => {
if (this.viaKeyboard) { if (props.viaKeyboard) {
this.$nextTick(() => { nextTick(() => {
focusNext(this.$refs.items.children[0], true, false); focusNext(itemsEl.children[0], true, false);
}); });
} }
});
if (this.contextmenuEvent) { function clicked(fn: MenuAction, ev: MouseEvent) {
this.$el.style.top = this.contextmenuEvent.pageY + 'px';
this.$el.style.left = this.contextmenuEvent.pageX + 'px';
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.addEventListener('mousedown', this.onMousedown);
}
}
},
beforeUnmount() {
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.removeEventListener('mousedown', this.onMousedown);
}
},
methods: {
clicked(fn, ev) {
fn(ev); fn(ev);
this.close(); close();
}, }
close() {
this.$emit('close'); function close() {
}, emit('close');
focusUp() { }
focusPrev(document.activeElement);
}, function focusUp() {
focusDown() { focusPrev(document.activeElement);
focusNext(document.activeElement); }
},
onMousedown(e) { function focusDown() {
if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close(); focusNext(document.activeElement);
},
} }
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,44 +1,28 @@
<template> <template>
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="$refs.modal.close()" @closed="$emit('closed')"> <MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')">
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="$refs.modal.close()"/> <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
</MkModal> </MkModal>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { } from 'vue';
import MkModal from './modal.vue'; import MkModal from './modal.vue';
import MkMenu from './menu.vue'; import MkMenu from './menu.vue';
import { MenuItem } from '@/types/menu';
export default defineComponent({ defineProps<{
components: { items: MenuItem[];
MkModal, align?: 'center' | string;
MkMenu, width?: number;
}, viaKeyboard?: boolean;
src?: any;
}>();
props: { const emit = defineEmits<{
items: { (e: 'closed'): void;
type: Array, }>();
required: true
},
align: {
type: String,
required: false
},
width: {
type: Number,
required: false
},
viaKeyboard: {
type: Boolean,
required: false
},
src: {
required: false
},
},
emits: ['close', 'closed'], let modal = $ref<InstanceType<typeof MkModal>>();
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,51 +1,55 @@
<template> <template>
<transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="$emit('closed')"> <transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="emit('closed')">
<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }"> <div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
<slot>{{ text }}</slot> <slot>{{ text }}</slot>
</div> </div>
</transition> </transition>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, nextTick, onMounted, onUnmounted, ref } from 'vue'; import { nextTick, onMounted, onUnmounted, ref } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ const props = withDefaults(defineProps<{
props: { showing: boolean;
showing: { targetElement?: HTMLElement;
type: Boolean, x?: number;
required: true, y?: number;
}, text?: string;
source: { maxWidth?: number;
required: true, }>(), {
}, maxWidth: 250,
text: { });
type: String,
required: false
},
maxWidth: {
type: Number,
required: false,
default: 250,
},
},
emits: ['closed'], const emit = defineEmits<{
(ev: 'closed'): void;
}>();
setup(props, context) {
const el = ref<HTMLElement>(); const el = ref<HTMLElement>();
const zIndex = os.claimZIndex('high'); const zIndex = os.claimZIndex('high');
const setPosition = () => { const setPosition = () => {
if (el.value == null) return; if (el.value == null) return;
const rect = props.source.getBoundingClientRect();
const contentWidth = el.value.offsetWidth; const contentWidth = el.value.offsetWidth;
const contentHeight = el.value.offsetHeight; const contentHeight = el.value.offsetHeight;
let left = rect.left + window.pageXOffset + (props.source.offsetWidth / 2); let left: number;
let top = rect.top + window.pageYOffset - contentHeight; let top: number;
let rect: DOMRect;
if (props.targetElement) {
rect = props.targetElement.getBoundingClientRect();
left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2);
top = rect.top + window.pageYOffset - contentHeight;
el.value.style.transformOrigin = 'center bottom';
} else {
left = props.x;
top = props.y - contentHeight;
}
left -= (el.value.offsetWidth / 2); left -= (el.value.offsetWidth / 2);
@ -53,9 +57,14 @@ export default defineComponent({
left = window.innerWidth - contentWidth + window.pageXOffset - 1; left = window.innerWidth - contentWidth + window.pageXOffset - 1;
} }
//
if (top - window.pageYOffset < 0) { if (top - window.pageYOffset < 0) {
top = rect.top + window.pageYOffset + props.source.offsetHeight; if (props.targetElement) {
top = rect.top + window.pageYOffset + props.targetElement.offsetHeight;
el.value.style.transformOrigin = 'center top'; el.value.style.transformOrigin = 'center top';
} else {
top = props.y;
}
} }
el.value.style.left = left + 'px'; el.value.style.left = left + 'px';
@ -64,11 +73,6 @@ export default defineComponent({
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
if (props.source == null) {
context.emit('closed');
return;
}
setPosition(); setPosition();
let loopHandler; let loopHandler;
@ -87,13 +91,6 @@ export default defineComponent({
}); });
}); });
}); });
return {
el,
zIndex,
};
},
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -118,6 +115,6 @@ export default defineComponent({
border-radius: 4px; border-radius: 4px;
border: solid 0.5px var(--divider); border: solid 0.5px var(--divider);
pointer-events: none; pointer-events: none;
transform-origin: center bottom; transform-origin: center center;
} }
</style> </style>

View file

@ -48,7 +48,7 @@ export default {
popup(import('@/components/ui/tooltip.vue'), { popup(import('@/components/ui/tooltip.vue'), {
showing, showing,
text: self.text, text: self.text,
source: el targetElement: el,
}, {}, 'closed'); }, {}, 'closed');
self._close = () => { self._close = () => {
@ -56,8 +56,8 @@ export default {
}; };
}; };
el.addEventListener('selectstart', e => { el.addEventListener('selectstart', ev => {
e.preventDefault(); ev.preventDefault();
}); });
el.addEventListener(start, () => { el.addEventListener(start, () => {

View file

@ -7,8 +7,10 @@ import * as Misskey from 'misskey-js';
import { apiUrl, url } from '@/config'; import { apiUrl, url } from '@/config';
import MkPostFormDialog from '@/components/post-form-dialog.vue'; import MkPostFormDialog from '@/components/post-form-dialog.vue';
import MkWaitingDialog from '@/components/waiting-dialog.vue'; import MkWaitingDialog from '@/components/waiting-dialog.vue';
import { MenuItem } from '@/types/menu';
import { resolve } from '@/router'; import { resolve } from '@/router';
import { $i } from '@/account'; import { $i } from '@/account';
import { defaultStore } from '@/store';
export const pendingApiRequestsCount = ref(0); export const pendingApiRequestsCount = ref(0);
@ -470,7 +472,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
}); });
} }
export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement, options?: {
align?: string; align?: string;
width?: number; width?: number;
viaKeyboard?: boolean; viaKeyboard?: boolean;
@ -494,7 +496,7 @@ export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?
}); });
} }
export function contextMenu(items: any[], ev: MouseEvent) { export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent) {
ev.preventDefault(); ev.preventDefault();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let dispose; let dispose;
@ -541,7 +543,7 @@ export const uploads = ref<{
img: string; img: string;
}[]>([]); }[]>([]);
export function upload(file: File, folder?: any, name?: string): Promise<Misskey.entities.DriveFile> { export function upload(file: File, folder?: any, name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading): Promise<Misskey.entities.DriveFile> {
if (folder && typeof folder == 'object') folder = folder.id; if (folder && typeof folder == 'object') folder = folder.id;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -559,6 +561,8 @@ export function upload(file: File, folder?: any, name?: string): Promise<Misskey
uploads.value.push(ctx); uploads.value.push(ctx);
console.log(keepOriginal);
const data = new FormData(); const data = new FormData();
data.append('i', $i.token); data.append('i', $i.token);
data.append('force', 'true'); data.append('force', 'true');

View file

@ -29,6 +29,7 @@
<template #label>Moderation</template> <template #label>Moderation</template>
<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch> <FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch>
<FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch> <FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch>
<MkButton @click="refreshMetadata">Refresh metadata</MkButton>
</FormSection> </FormSection>
<FormSection> <FormSection>
@ -111,6 +112,7 @@ import MkChart from '@/components/chart.vue';
import MkObjectView from '@/components/object-view.vue'; import MkObjectView from '@/components/object-view.vue';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import MkLink from '@/components/link.vue'; import MkLink from '@/components/link.vue';
import MkButton from '@/components/ui/button.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import MkKeyValue from '@/components/key-value.vue'; import MkKeyValue from '@/components/key-value.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/form/select.vue';
@ -155,6 +157,15 @@ async function toggleSuspend(v) {
}); });
} }
function refreshMetadata() {
os.api('admin/federation/refresh-remote-instance-metadata', {
host: instance.host,
});
os.alert({
text: 'Refresh requested',
});
}
fetch(); fetch();
defineExpose({ defineExpose({

View file

@ -28,6 +28,7 @@
<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template> <template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
<template #suffixIcon><i class="fas fa-folder-open"></i></template> <template #suffixIcon><i class="fas fa-folder-open"></i></template>
</FormLink> </FormLink>
<FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ $ts.keepOriginalUploading }}<template #caption>{{ $ts.keepOriginalUploadingDescription }}</template></FormSwitch>
</FormSection> </FormSection>
</div> </div>
</template> </template>
@ -36,18 +37,21 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import * as tinycolor from 'tinycolor2'; import * as tinycolor from 'tinycolor2';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import MkKeyValue from '@/components/key-value.vue'; import MkKeyValue from '@/components/key-value.vue';
import FormSplit from '@/components/form/split.vue'; import FormSplit from '@/components/form/split.vue';
import * as os from '@/os'; import * as os from '@/os';
import bytes from '@/filters/bytes'; import bytes from '@/filters/bytes';
import * as symbols from '@/symbols'; import * as symbols from '@/symbols';
import { defaultStore } from '@/store';
// TODO: render chart // TODO: render chart
export default defineComponent({ export default defineComponent({
components: { components: {
FormLink, FormLink,
FormSwitch,
FormSection, FormSection,
MkKeyValue, MkKeyValue,
FormSplit, FormSplit,
@ -79,7 +83,8 @@ export default defineComponent({
l: 0.5 l: 0.5
}) })
}; };
} },
keepOriginalUploading: defaultStore.makeGetterSetter('keepOriginalUploading'),
}, },
async created() { async created() {

View file

@ -1,3 +1,4 @@
import { ref } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import { stream } from '@/stream'; import { stream } from '@/stream';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
@ -6,12 +7,14 @@ import { DriveFile } from 'misskey-js/built/entities';
function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> { function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
const chooseFileFromPc = () => { const chooseFileFromPc = () => {
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'file'; input.type = 'file';
input.multiple = multiple; input.multiple = multiple;
input.onchange = () => { input.onchange = () => {
const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder)); const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value));
Promise.all(promises).then(driveFiles => { Promise.all(promises).then(driveFiles => {
res(multiple ? driveFiles : driveFiles[0]); res(multiple ? driveFiles : driveFiles[0]);
@ -74,6 +77,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
text: label, text: label,
type: 'label' type: 'label'
} : undefined, { } : undefined, {
type: 'switch',
text: i18n.ts.keepOriginalUploading,
ref: keepOriginal
}, {
text: i18n.ts.upload, text: i18n.ts.upload,
icon: 'fas fa-upload', icon: 'fas fa-upload',
action: chooseFileFromPc action: chooseFileFromPc

View file

@ -43,6 +43,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'account', where: 'account',
default: 'yyyy-MM-dd HH-mm-ss [{{number}}]' default: 'yyyy-MM-dd HH-mm-ss [{{number}}]'
}, },
keepOriginalUploading: {
where: 'account',
default: false
},
memo: { memo: {
where: 'account', where: 'account',
default: null default: null

View file

@ -0,0 +1,20 @@
import * as Misskey from 'misskey-js';
import { Ref } from 'vue';
export type MenuAction = (ev: MouseEvent) => void;
export type MenuDivider = null;
export type MenuNull = undefined;
export type MenuLabel = { type: 'label', text: string };
export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean };
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean };
export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
export type MenuPending = { type: 'pending' };
type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton;
type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton>;
export type MenuItem = OuterMenuItem | OuterPromiseMenuItem;
export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton;

View file

@ -54,13 +54,13 @@ const charts = ref([]);
const fetching = ref(true); const fetching = ref(true);
const fetch = async () => { const fetch = async () => {
const instances = await os.api('federation/instances', { const fetchedInstances = await os.api('federation/instances', {
sort: '+lastCommunicatedAt', sort: '+lastCommunicatedAt',
limit: 5 limit: 5
}); });
const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' }))); const fetchedCharts = await Promise.all(fetchedInstances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
instances.value = instances; instances.value = fetchedInstances;
charts.value = charts; charts.value = fetchedCharts;
fetching.value = false; fetching.value = false;
}; };

View file

@ -4139,10 +4139,10 @@ minimist@^1.2.0, minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
misskey-js@0.0.13: misskey-js@0.0.14:
version "0.0.13" version "0.0.14"
resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.13.tgz#03a4e469186e28752d599dc4093519eb64647970" resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d"
integrity sha512-kBdJdfe281gtykzzsrN3IAxWUQIimzPiJGyKWf863ggWJlWYVPmP9hTFlX2z8oPOaypgVBPEPHyw/jNUdc2DbQ== integrity sha512-bvLx6U3OwQwqHfp/WKwIVwdvNYAAPk0+YblXyxmSG3dwlzCgBRRLcB8o6bNruUDyJgh3t73pLDcOz3myxcUmww==
dependencies: dependencies:
autobind-decorator "^2.4.0" autobind-decorator "^2.4.0"
eventemitter3 "^4.0.7" eventemitter3 "^4.0.7"

View file

@ -37,6 +37,7 @@ module.exports = {
] ]
}], }],
*/ */
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
'no-multi-spaces': ['error'], 'no-multi-spaces': ['error'],
'no-var': ['error'], 'no-var': ['error'],
'prefer-arrow-callback': ['error'], 'prefer-arrow-callback': ['error'],
@ -56,7 +57,7 @@ module.exports = {
'object-curly-spacing': ['error', 'always'], 'object-curly-spacing': ['error', 'always'],
'space-infix-ops': ['error'], 'space-infix-ops': ['error'],
'space-before-blocks': ['error', 'always'], 'space-before-blocks': ['error', 'always'],
'@typescript-eslint/no-unnecessary-condition': ['error'], '@typescript-eslint/no-unnecessary-condition': ['warn'],
'@typescript-eslint/no-var-requires': ['warn'], '@typescript-eslint/no-var-requires': ['warn'],
'@typescript-eslint/no-inferrable-types': ['warn'], '@typescript-eslint/no-inferrable-types': ['warn'],
'@typescript-eslint/no-empty-function': ['off'], '@typescript-eslint/no-empty-function': ['off'],