Merge branch 'develop' into fix-msg-room
This commit is contained in:
		
						commit
						927317b5bb
					
				
					 36 changed files with 454 additions and 356 deletions
				
			
		| 
						 | 
				
			
			@ -122,7 +122,7 @@
 | 
			
		|||
		"langmap": "0.0.16",
 | 
			
		||||
		"mfm-js": "0.21.0",
 | 
			
		||||
		"mime-types": "2.1.34",
 | 
			
		||||
		"misskey-js": "0.0.13",
 | 
			
		||||
		"misskey-js": "0.0.14",
 | 
			
		||||
		"mocha": "8.4.0",
 | 
			
		||||
		"ms": "3.0.0-canary.1",
 | 
			
		||||
		"multer": "1.4.4",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,7 +32,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => {
 | 
			
		|||
	// Authentication
 | 
			
		||||
	authenticate(body['i']).then(([user, app]) => {
 | 
			
		||||
		// 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);
 | 
			
		||||
		}).catch((e: ApiError) => {
 | 
			
		||||
			reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
import * as Koa from 'koa';
 | 
			
		||||
import { performance } from 'perf_hooks';
 | 
			
		||||
import { limiter } from './limiter';
 | 
			
		||||
import { User } from '@/models/entities/user';
 | 
			
		||||
| 
						 | 
				
			
			@ -12,7 +13,7 @@ const accessDenied = {
 | 
			
		|||
	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 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
 | 
			
		||||
	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) {
 | 
			
		||||
			throw e;
 | 
			
		||||
		} else {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,15 +39,13 @@ export const meta = {
 | 
			
		|||
		},
 | 
			
		||||
 | 
			
		||||
		isSensitive: {
 | 
			
		||||
			validator: $.optional.either($.bool, $.str),
 | 
			
		||||
			validator: $.optional.bool,
 | 
			
		||||
			default: false,
 | 
			
		||||
			transform: (v: any): boolean => v === true || v === 'true',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		force: {
 | 
			
		||||
			validator: $.optional.either($.bool, $.str),
 | 
			
		||||
			validator: $.optional.bool,
 | 
			
		||||
			default: false,
 | 
			
		||||
			transform: (v: any): boolean => v === true || v === 'true',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,7 @@ const _dirname = dirname(_filename);
 | 
			
		|||
const app = new Koa();
 | 
			
		||||
app.use(cors());
 | 
			
		||||
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();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,7 @@ import { proxyMedia } from './proxy-media';
 | 
			
		|||
const app = new Koa();
 | 
			
		||||
app.use(cors());
 | 
			
		||||
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();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4967,10 +4967,10 @@ minizlib@^2.0.0, minizlib@^2.1.1:
 | 
			
		|||
    minipass "^3.0.0"
 | 
			
		||||
    yallist "^4.0.0"
 | 
			
		||||
 | 
			
		||||
misskey-js@0.0.13:
 | 
			
		||||
  version "0.0.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.13.tgz#03a4e469186e28752d599dc4093519eb64647970"
 | 
			
		||||
  integrity sha512-kBdJdfe281gtykzzsrN3IAxWUQIimzPiJGyKWf863ggWJlWYVPmP9hTFlX2z8oPOaypgVBPEPHyw/jNUdc2DbQ==
 | 
			
		||||
misskey-js@0.0.14:
 | 
			
		||||
  version "0.0.14"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d"
 | 
			
		||||
  integrity sha512-bvLx6U3OwQwqHfp/WKwIVwdvNYAAPk0+YblXyxmSG3dwlzCgBRRLcB8o6bNruUDyJgh3t73pLDcOz3myxcUmww==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    autobind-decorator "^2.4.0"
 | 
			
		||||
    eventemitter3 "^4.0.7"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,7 @@ module.exports = {
 | 
			
		|||
		// data の禁止理由: 抽象的すぎるため
 | 
			
		||||
		// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
 | 
			
		||||
		"id-denylist": ["error", "window", "data", "e"],
 | 
			
		||||
		'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
 | 
			
		||||
		"vue/attributes-order": ["error", {
 | 
			
		||||
			"alphabetical": false
 | 
			
		||||
		}],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,7 +69,7 @@
 | 
			
		|||
		"langmap": "0.0.16",
 | 
			
		||||
		"matter-js": "0.18.0",
 | 
			
		||||
		"mfm-js": "0.21.0",
 | 
			
		||||
		"misskey-js": "0.0.13",
 | 
			
		||||
		"misskey-js": "0.0.14",
 | 
			
		||||
		"mocha": "8.4.0",
 | 
			
		||||
		"ms": "2.1.3",
 | 
			
		||||
		"nested-property": "4.0.0",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										51
									
								
								packages/client/src/components/chart-tooltip.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								packages/client/src/components/chart-tooltip.vue
									
										
									
									
									
										Normal 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>
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,7 @@
 | 
			
		|||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, onMounted, ref, watch, PropType } from 'vue';
 | 
			
		||||
import { defineComponent, onMounted, ref, watch, PropType, onUnmounted, shallowRef } from 'vue';
 | 
			
		||||
import {
 | 
			
		||||
	Chart,
 | 
			
		||||
	ArcElement,
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +31,7 @@ import { enUS } from 'date-fns/locale';
 | 
			
		|||
import zoomPlugin from 'chartjs-plugin-zoom';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
import MkChartTooltip from '@/components/chart-tooltip.vue';
 | 
			
		||||
 | 
			
		||||
Chart.register(
 | 
			
		||||
	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 = () => {
 | 
			
		||||
			if (chartInstance) {
 | 
			
		||||
				chartInstance.destroy();
 | 
			
		||||
| 
						 | 
				
			
			@ -222,10 +260,12 @@ export default defineComponent({
 | 
			
		|||
							},
 | 
			
		||||
						},
 | 
			
		||||
						tooltip: {
 | 
			
		||||
							enabled: false,
 | 
			
		||||
							mode: 'index',
 | 
			
		||||
							animation: {
 | 
			
		||||
								duration: 0,
 | 
			
		||||
							},
 | 
			
		||||
							external: externalTooltipHandler,
 | 
			
		||||
						},
 | 
			
		||||
						zoom: {
 | 
			
		||||
							pan: {
 | 
			
		||||
| 
						 | 
				
			
			@ -684,6 +724,10 @@ export default defineComponent({
 | 
			
		|||
			fetchAndRender();
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		onUnmounted(() => {
 | 
			
		||||
			if (disposeTooltipComponent) disposeTooltipComponent();
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			chartEl,
 | 
			
		||||
			fetching,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -117,7 +117,7 @@ export default defineComponent({
 | 
			
		|||
				text: computed(() => {
 | 
			
		||||
					return props.textConverter(finalValue.value);
 | 
			
		||||
				}),
 | 
			
		||||
				source: thumbEl,
 | 
			
		||||
				targetElement: thumbEl,
 | 
			
		||||
			}, {}, 'closed');
 | 
			
		||||
 | 
			
		||||
			const style = document.createElement('style');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,45 +20,33 @@
 | 
			
		|||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, ref, toRefs } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, Ref } from 'vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import Ripple from '@/components/ripple.vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	props: {
 | 
			
		||||
		modelValue: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			default: false
 | 
			
		||||
		},
 | 
			
		||||
		disabled: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			default: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	modelValue: boolean | Ref<boolean>;
 | 
			
		||||
	disabled?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
	setup(props, context) {
 | 
			
		||||
		const button = ref<HTMLElement>();
 | 
			
		||||
		const checked = toRefs(props).modelValue;
 | 
			
		||||
		const toggle = () => {
 | 
			
		||||
			if (props.disabled) return;
 | 
			
		||||
			context.emit('update:modelValue', !checked.value);
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'update:modelValue', v: boolean): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
			if (!checked.value) {
 | 
			
		||||
				const rect = button.value.getBoundingClientRect();
 | 
			
		||||
				const x = rect.left + (button.value.offsetWidth / 2);
 | 
			
		||||
				const y = rect.top + (button.value.offsetHeight / 2);
 | 
			
		||||
				os.popup(Ripple, { x, y, particle: false }, {}, 'end');
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
let button = $ref<HTMLElement>();
 | 
			
		||||
const checked = toRefs(props).modelValue;
 | 
			
		||||
const toggle = () => {
 | 
			
		||||
	if (props.disabled) return;
 | 
			
		||||
	emit('update:modelValue', !checked.value);
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			button,
 | 
			
		||||
			checked,
 | 
			
		||||
			toggle,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
	if (!checked.value) {
 | 
			
		||||
		const rect = button.getBoundingClientRect();
 | 
			
		||||
		const x = rect.left + (button.offsetWidth / 2);
 | 
			
		||||
		const y = rect.top + (button.offsetHeight / 2);
 | 
			
		||||
		os.popup(Ripple, { x, y, particle: false }, {}, 'end');
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -153,7 +153,7 @@ export default defineComponent({
 | 
			
		|||
				showing,
 | 
			
		||||
				reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
 | 
			
		||||
				emojis: props.notification.note.emojis,
 | 
			
		||||
				source: reactionRef.value.$el,
 | 
			
		||||
				targetElement: reactionRef.value.$el,
 | 
			
		||||
			}, {}, 'closed');
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -135,7 +135,10 @@ let showPreview = $ref(false);
 | 
			
		|||
let cw = $ref<string | null>(null);
 | 
			
		||||
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 visibleUsers = $ref(props.initialVisibleUsers ?? []);
 | 
			
		||||
let visibleUsers = $ref([]);
 | 
			
		||||
if (props.initialVisibleUsers) {
 | 
			
		||||
	props.initialVisibleUsers.forEach(pushVisibleUser);
 | 
			
		||||
}
 | 
			
		||||
let autocomplete = $ref(null);
 | 
			
		||||
let draghover = $ref(false);
 | 
			
		||||
let quoteId = $ref(null);
 | 
			
		||||
| 
						 | 
				
			
			@ -262,12 +265,12 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
 | 
			
		|||
		os.api('users/show', {
 | 
			
		||||
			userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId)
 | 
			
		||||
		}).then(users => {
 | 
			
		||||
			visibleUsers.push(...users);
 | 
			
		||||
			users.forEach(pushVisibleUser);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (props.reply.userId !== $i.id) {
 | 
			
		||||
			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) {
 | 
			
		||||
	visibility = 'specified';
 | 
			
		||||
	visibleUsers.push(props.specified);
 | 
			
		||||
	pushVisibleUser(props.specified);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// keep cw when reply
 | 
			
		||||
| 
						 | 
				
			
			@ -397,9 +400,15 @@ function setVisibility() {
 | 
			
		|||
	}, 'closed');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function pushVisibleUser(user) {
 | 
			
		||||
	if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) {
 | 
			
		||||
		visibleUsers.push(user);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addVisibleUser() {
 | 
			
		||||
	os.selectUser().then(user => {
 | 
			
		||||
		visibleUsers.push(user);
 | 
			
		||||
		pushVisibleUser(user);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -540,8 +549,8 @@ async function post() {
 | 
			
		|||
	};
 | 
			
		||||
 | 
			
		||||
	if (withHashtags && hashtags && hashtags.trim() !== '') {
 | 
			
		||||
		const hashtags = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
 | 
			
		||||
		data.text = data.text ? `${data.text} ${hashtags}` : hashtags;
 | 
			
		||||
		const hashtags_ = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
 | 
			
		||||
		data.text = data.text ? `${data.text} ${hashtags_}` : hashtags_;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// plugin
 | 
			
		||||
| 
						 | 
				
			
			@ -565,9 +574,9 @@ async function post() {
 | 
			
		|||
			deleteDraft();
 | 
			
		||||
			emit('posted');
 | 
			
		||||
			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[];
 | 
			
		||||
				localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
 | 
			
		||||
				localStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
 | 
			
		||||
			}
 | 
			
		||||
			posting = false;
 | 
			
		||||
			postAccount = null;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<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">
 | 
			
		||||
		<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
 | 
			
		||||
		<div class="name">{{ reaction.replace('@.', '') }}</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -15,11 +15,11 @@ import XReactionIcon from './reaction-icon.vue';
 | 
			
		|||
const props = defineProps<{
 | 
			
		||||
	reaction: string;
 | 
			
		||||
	emojis: any[]; // TODO
 | 
			
		||||
	source: any; // TODO
 | 
			
		||||
	targetElement: HTMLElement;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<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="reaction">
 | 
			
		||||
			<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -26,11 +26,11 @@ const props = defineProps<{
 | 
			
		|||
	users: any[]; // TODO
 | 
			
		||||
	count: number;
 | 
			
		||||
	emojis: any[]; // TODO
 | 
			
		||||
	source: any; // TODO
 | 
			
		||||
	targetElement: HTMLElement;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<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 v-for="u in users" :key="u.id" class="user">
 | 
			
		||||
			<MkAvatar class="avatar" :user="u"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -17,11 +17,11 @@ import MkTooltip from './ui/tooltip.vue';
 | 
			
		|||
const props = defineProps<{
 | 
			
		||||
	users: any[]; // TODO
 | 
			
		||||
	count: number;
 | 
			
		||||
	source: any; // TODO
 | 
			
		||||
	targetElement: HTMLElement;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,88 +1,71 @@
 | 
			
		|||
<template>
 | 
			
		||||
<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')"/>
 | 
			
		||||
	</div>
 | 
			
		||||
</transition>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, onBeforeUnmount } from 'vue';
 | 
			
		||||
import contains from '@/scripts/contains';
 | 
			
		||||
import MkMenu from './menu.vue';
 | 
			
		||||
import { MenuItem } from './types/menu.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkMenu,
 | 
			
		||||
	},
 | 
			
		||||
	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 props = defineProps<{
 | 
			
		||||
	items: MenuItem[];
 | 
			
		||||
	ev: MouseEvent;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
		const width = this.$el.offsetWidth;
 | 
			
		||||
		const height = this.$el.offsetHeight;
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
		if (left + width - window.pageXOffset > window.innerWidth) {
 | 
			
		||||
			left = window.innerWidth - width + window.pageXOffset;
 | 
			
		||||
		}
 | 
			
		||||
let rootEl = $ref<HTMLDivElement>();
 | 
			
		||||
 | 
			
		||||
		if (top + height - window.pageYOffset > window.innerHeight) {
 | 
			
		||||
			top = window.innerHeight - height + window.pageYOffset;
 | 
			
		||||
		}
 | 
			
		||||
let zIndex = $ref<number>(os.claimZIndex('high'));
 | 
			
		||||
 | 
			
		||||
		if (top < 0) {
 | 
			
		||||
			top = 0;
 | 
			
		||||
		}
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
 | 
			
		||||
	let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
 | 
			
		||||
 | 
			
		||||
		if (left < 0) {
 | 
			
		||||
			left = 0;
 | 
			
		||||
		}
 | 
			
		||||
	const width = rootEl.offsetWidth;
 | 
			
		||||
	const height = rootEl.offsetHeight;
 | 
			
		||||
 | 
			
		||||
		this.$el.style.top = top + 'px';
 | 
			
		||||
		this.$el.style.left = left + 'px';
 | 
			
		||||
	if (left + width - window.pageXOffset > window.innerWidth) {
 | 
			
		||||
		left = window.innerWidth - width + window.pageXOffset;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		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: {
 | 
			
		||||
		onMousedown(e) {
 | 
			
		||||
			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.$emit('closed');
 | 
			
		||||
		},
 | 
			
		||||
	if (top + height - window.pageYOffset > window.innerHeight) {
 | 
			
		||||
		top = window.innerHeight - height + window.pageYOffset;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (top < 0) {
 | 
			
		||||
		top = 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (left < 0) {
 | 
			
		||||
		left = 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rootEl.style.top = `${top}px`;
 | 
			
		||||
	rootEl.style.left = `${left}px`;
 | 
			
		||||
 | 
			
		||||
	for (const el of Array.from(document.querySelectorAll('body *'))) {
 | 
			
		||||
		el.addEventListener('mousedown', onMousedown);
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div ref="items" v-hotkey="keymap"
 | 
			
		||||
<div ref="itemsEl" v-hotkey="keymap"
 | 
			
		||||
	class="rrevdjwt"
 | 
			
		||||
	: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()"
 | 
			
		||||
>
 | 
			
		||||
	<template v-for="(item, i) in items2">
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +28,9 @@
 | 
			
		|||
			<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
 | 
			
		||||
			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
 | 
			
		||||
		</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)">
 | 
			
		||||
			<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
 | 
			
		||||
			<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -41,114 +44,78 @@
 | 
			
		|||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, ref, unref } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { nextTick, onMounted, watch } from 'vue';
 | 
			
		||||
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({
 | 
			
		||||
	props: {
 | 
			
		||||
		items: {
 | 
			
		||||
			type: Array,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		viaKeyboard: {
 | 
			
		||||
			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));
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	items: MenuItem[];
 | 
			
		||||
	viaKeyboard?: boolean;
 | 
			
		||||
	asDrawer?: boolean;
 | 
			
		||||
	align?: 'center' | string;
 | 
			
		||||
	width?: number;
 | 
			
		||||
	maxHeight?: number;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
				for (let i = 0; i < items.value.length; i++) {
 | 
			
		||||
					const item = items.value[i];
 | 
			
		||||
					
 | 
			
		||||
					if (item && item.then) { // if item is Promise
 | 
			
		||||
						items.value[i] = { type: 'pending' };
 | 
			
		||||
						item.then(actualItem => {
 | 
			
		||||
							items.value[i] = actualItem;
 | 
			
		||||
						});
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'close'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
				this.items2 = items;
 | 
			
		||||
			},
 | 
			
		||||
			immediate: true
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		if (this.viaKeyboard) {
 | 
			
		||||
			this.$nextTick(() => {
 | 
			
		||||
				focusNext(this.$refs.items.children[0], true, false);
 | 
			
		||||
let itemsEl = $ref<HTMLDivElement>();
 | 
			
		||||
 | 
			
		||||
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 => {
 | 
			
		||||
				items2[i] = actualItem;
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		if (this.contextmenuEvent) {
 | 
			
		||||
			this.$el.style.top = this.contextmenuEvent.pageY + 'px';
 | 
			
		||||
			this.$el.style.left = this.contextmenuEvent.pageX + 'px';
 | 
			
		||||
	items2 = items as InnerMenuItem[];
 | 
			
		||||
}, {
 | 
			
		||||
	immediate: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
			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);
 | 
			
		||||
			this.close();
 | 
			
		||||
		},
 | 
			
		||||
		close() {
 | 
			
		||||
			this.$emit('close');
 | 
			
		||||
		},
 | 
			
		||||
		focusUp() {
 | 
			
		||||
			focusPrev(document.activeElement);
 | 
			
		||||
		},
 | 
			
		||||
		focusDown() {
 | 
			
		||||
			focusNext(document.activeElement);
 | 
			
		||||
		},
 | 
			
		||||
		onMousedown(e) {
 | 
			
		||||
			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
 | 
			
		||||
		},
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	if (props.viaKeyboard) {
 | 
			
		||||
		nextTick(() => {
 | 
			
		||||
			focusNext(itemsEl.children[0], true, false);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function clicked(fn: MenuAction, ev: MouseEvent) {
 | 
			
		||||
	fn(ev);
 | 
			
		||||
	close();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function close() {
 | 
			
		||||
	emit('close');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function focusUp() {
 | 
			
		||||
	focusPrev(document.activeElement);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function focusDown() {
 | 
			
		||||
	focusNext(document.activeElement);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,44 +1,28 @@
 | 
			
		|||
<template>
 | 
			
		||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="$refs.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()"/>
 | 
			
		||||
<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="modal.close()"/>
 | 
			
		||||
</MkModal>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import MkModal from './modal.vue';
 | 
			
		||||
import MkMenu from './menu.vue';
 | 
			
		||||
import { MenuItem } from '@/types/menu';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkModal,
 | 
			
		||||
		MkMenu,
 | 
			
		||||
	},
 | 
			
		||||
defineProps<{
 | 
			
		||||
	items: MenuItem[];
 | 
			
		||||
	align?: 'center' | string;
 | 
			
		||||
	width?: number;
 | 
			
		||||
	viaKeyboard?: boolean;
 | 
			
		||||
	src?: any;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		items: {
 | 
			
		||||
			type: Array,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		align: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		width: {
 | 
			
		||||
			type: Number,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		viaKeyboard: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		src: {
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
	emits: ['close', 'closed'],
 | 
			
		||||
});
 | 
			
		||||
let modal = $ref<InstanceType<typeof MkModal>>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,99 +1,96 @@
 | 
			
		|||
<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' }">
 | 
			
		||||
		<slot>{{ text }}</slot>
 | 
			
		||||
	</div>
 | 
			
		||||
</transition>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, nextTick, onMounted, onUnmounted, ref } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	props: {
 | 
			
		||||
		showing: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: true,
 | 
			
		||||
		},
 | 
			
		||||
		source: {
 | 
			
		||||
			required: true,
 | 
			
		||||
		},
 | 
			
		||||
		text: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		maxWidth: {
 | 
			
		||||
			type: Number,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: 250,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	showing: boolean;
 | 
			
		||||
	targetElement?: HTMLElement;
 | 
			
		||||
	x?: number;
 | 
			
		||||
	y?: number;
 | 
			
		||||
	text?: string;
 | 
			
		||||
	maxWidth?: number;
 | 
			
		||||
}>(), {
 | 
			
		||||
	maxWidth: 250,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
	emits: ['closed'],
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
	setup(props, context) {
 | 
			
		||||
		const el = ref<HTMLElement>();
 | 
			
		||||
		const zIndex = os.claimZIndex('high');
 | 
			
		||||
const el = ref<HTMLElement>();
 | 
			
		||||
const zIndex = os.claimZIndex('high');
 | 
			
		||||
 | 
			
		||||
		const setPosition = () => {
 | 
			
		||||
			if (el.value == null) return;
 | 
			
		||||
const setPosition = () => {
 | 
			
		||||
	if (el.value == null) return;
 | 
			
		||||
 | 
			
		||||
			const rect = props.source.getBoundingClientRect();
 | 
			
		||||
	const contentWidth = el.value.offsetWidth;
 | 
			
		||||
	const contentHeight = el.value.offsetHeight;
 | 
			
		||||
 | 
			
		||||
			const contentWidth = el.value.offsetWidth;
 | 
			
		||||
			const contentHeight = el.value.offsetHeight;
 | 
			
		||||
	let left: number;
 | 
			
		||||
	let top: number;
 | 
			
		||||
 | 
			
		||||
			let left = rect.left + window.pageXOffset + (props.source.offsetWidth / 2);
 | 
			
		||||
			let top = rect.top + window.pageYOffset - contentHeight;
 | 
			
		||||
	let rect: DOMRect;
 | 
			
		||||
 | 
			
		||||
			left -= (el.value.offsetWidth / 2);
 | 
			
		||||
	if (props.targetElement) {
 | 
			
		||||
		rect = props.targetElement.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
			if (left + contentWidth - window.pageXOffset > window.innerWidth) {
 | 
			
		||||
				left = window.innerWidth - contentWidth + window.pageXOffset - 1;
 | 
			
		||||
			}
 | 
			
		||||
		left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2);
 | 
			
		||||
		top = rect.top + window.pageYOffset - contentHeight;
 | 
			
		||||
 | 
			
		||||
			if (top - window.pageYOffset < 0) {
 | 
			
		||||
				top = rect.top + window.pageYOffset + props.source.offsetHeight;
 | 
			
		||||
				el.value.style.transformOrigin = 'center top';
 | 
			
		||||
			}
 | 
			
		||||
		el.value.style.transformOrigin = 'center bottom';
 | 
			
		||||
	} else {
 | 
			
		||||
		left = props.x;
 | 
			
		||||
		top = props.y - contentHeight;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
			el.value.style.left = left + 'px';
 | 
			
		||||
			el.value.style.top = top + 'px';
 | 
			
		||||
		};
 | 
			
		||||
	left -= (el.value.offsetWidth / 2);
 | 
			
		||||
 | 
			
		||||
		onMounted(() => {
 | 
			
		||||
			nextTick(() => {
 | 
			
		||||
				if (props.source == null) {
 | 
			
		||||
					context.emit('closed');
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
	if (left + contentWidth - window.pageXOffset > window.innerWidth) {
 | 
			
		||||
		left = window.innerWidth - contentWidth + window.pageXOffset - 1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
 | 
			
		||||
	if (top - window.pageYOffset < 0) {
 | 
			
		||||
		if (props.targetElement) {
 | 
			
		||||
			top = rect.top + window.pageYOffset + props.targetElement.offsetHeight;
 | 
			
		||||
			el.value.style.transformOrigin = 'center top';
 | 
			
		||||
		} else {
 | 
			
		||||
			top = props.y;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	el.value.style.left = left + 'px';
 | 
			
		||||
	el.value.style.top = top + 'px';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	nextTick(() => {
 | 
			
		||||
		setPosition();
 | 
			
		||||
 | 
			
		||||
		let loopHandler;
 | 
			
		||||
 | 
			
		||||
		const loop = () => {
 | 
			
		||||
			loopHandler = window.requestAnimationFrame(() => {
 | 
			
		||||
				setPosition();
 | 
			
		||||
 | 
			
		||||
				let loopHandler;
 | 
			
		||||
 | 
			
		||||
				const loop = () => {
 | 
			
		||||
					loopHandler = window.requestAnimationFrame(() => {
 | 
			
		||||
						setPosition();
 | 
			
		||||
						loop();
 | 
			
		||||
					});
 | 
			
		||||
				};
 | 
			
		||||
 | 
			
		||||
				loop();
 | 
			
		||||
 | 
			
		||||
				onUnmounted(() => {
 | 
			
		||||
					window.cancelAnimationFrame(loopHandler);
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			el,
 | 
			
		||||
			zIndex,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
		loop();
 | 
			
		||||
 | 
			
		||||
		onUnmounted(() => {
 | 
			
		||||
			window.cancelAnimationFrame(loopHandler);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
| 
						 | 
				
			
			@ -118,6 +115,6 @@ export default defineComponent({
 | 
			
		|||
	border-radius: 4px;
 | 
			
		||||
	border: solid 0.5px var(--divider);
 | 
			
		||||
	pointer-events: none;
 | 
			
		||||
	transform-origin: center bottom;
 | 
			
		||||
	transform-origin: center center;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,7 +48,7 @@ export default {
 | 
			
		|||
			popup(import('@/components/ui/tooltip.vue'), {
 | 
			
		||||
				showing,
 | 
			
		||||
				text: self.text,
 | 
			
		||||
				source: el
 | 
			
		||||
				targetElement: el,
 | 
			
		||||
			}, {}, 'closed');
 | 
			
		||||
 | 
			
		||||
			self._close = () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -56,8 +56,8 @@ export default {
 | 
			
		|||
			};
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		el.addEventListener('selectstart', e => {
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
		el.addEventListener('selectstart', ev => {
 | 
			
		||||
			ev.preventDefault();
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		el.addEventListener(start, () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,8 +7,10 @@ import * as Misskey from 'misskey-js';
 | 
			
		|||
import { apiUrl, url } from '@/config';
 | 
			
		||||
import MkPostFormDialog from '@/components/post-form-dialog.vue';
 | 
			
		||||
import MkWaitingDialog from '@/components/waiting-dialog.vue';
 | 
			
		||||
import { MenuItem } from '@/types/menu';
 | 
			
		||||
import { resolve } from '@/router';
 | 
			
		||||
import { $i } from '@/account';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
	width?: number;
 | 
			
		||||
	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();
 | 
			
		||||
	return new Promise((resolve, reject) => {
 | 
			
		||||
		let dispose;
 | 
			
		||||
| 
						 | 
				
			
			@ -541,7 +543,7 @@ export const uploads = ref<{
 | 
			
		|||
	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;
 | 
			
		||||
 | 
			
		||||
	return new Promise((resolve, reject) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -559,6 +561,8 @@ export function upload(file: File, folder?: any, name?: string): Promise<Misskey
 | 
			
		|||
 | 
			
		||||
			uploads.value.push(ctx);
 | 
			
		||||
 | 
			
		||||
			console.log(keepOriginal);
 | 
			
		||||
 | 
			
		||||
			const data = new FormData();
 | 
			
		||||
			data.append('i', $i.token);
 | 
			
		||||
			data.append('force', 'true');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,6 +29,7 @@
 | 
			
		|||
			<template #label>Moderation</template>
 | 
			
		||||
			<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch>
 | 
			
		||||
			<FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch>
 | 
			
		||||
			<MkButton @click="refreshMetadata">Refresh metadata</MkButton>
 | 
			
		||||
		</FormSection>
 | 
			
		||||
 | 
			
		||||
		<FormSection>
 | 
			
		||||
| 
						 | 
				
			
			@ -111,6 +112,7 @@ import MkChart from '@/components/chart.vue';
 | 
			
		|||
import MkObjectView from '@/components/object-view.vue';
 | 
			
		||||
import FormLink from '@/components/form/link.vue';
 | 
			
		||||
import MkLink from '@/components/link.vue';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import FormSection from '@/components/form/section.vue';
 | 
			
		||||
import MkKeyValue from '@/components/key-value.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();
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,6 +28,7 @@
 | 
			
		|||
			<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
 | 
			
		||||
			<template #suffixIcon><i class="fas fa-folder-open"></i></template>
 | 
			
		||||
		</FormLink>
 | 
			
		||||
		<FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ $ts.keepOriginalUploading }}<template #caption>{{ $ts.keepOriginalUploadingDescription }}</template></FormSwitch>
 | 
			
		||||
	</FormSection>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -36,18 +37,21 @@
 | 
			
		|||
import { defineComponent } from 'vue';
 | 
			
		||||
import * as tinycolor from 'tinycolor2';
 | 
			
		||||
import FormLink from '@/components/form/link.vue';
 | 
			
		||||
import FormSwitch from '@/components/form/switch.vue';
 | 
			
		||||
import FormSection from '@/components/form/section.vue';
 | 
			
		||||
import MkKeyValue from '@/components/key-value.vue';
 | 
			
		||||
import FormSplit from '@/components/form/split.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import bytes from '@/filters/bytes';
 | 
			
		||||
import * as symbols from '@/symbols';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
 | 
			
		||||
// TODO: render chart
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		FormLink,
 | 
			
		||||
		FormSwitch,
 | 
			
		||||
		FormSection,
 | 
			
		||||
		MkKeyValue,
 | 
			
		||||
		FormSplit,
 | 
			
		||||
| 
						 | 
				
			
			@ -79,7 +83,8 @@ export default defineComponent({
 | 
			
		|||
					l: 0.5
 | 
			
		||||
				})
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
		},
 | 
			
		||||
		keepOriginalUploading: defaultStore.makeGetterSetter('keepOriginalUploading'),
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	async created() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
import { ref } from 'vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { stream } from '@/stream';
 | 
			
		||||
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[]> {
 | 
			
		||||
	return new Promise((res, rej) => {
 | 
			
		||||
		const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
 | 
			
		||||
 | 
			
		||||
		const chooseFileFromPc = () => {
 | 
			
		||||
			const input = document.createElement('input');
 | 
			
		||||
			input.type = 'file';
 | 
			
		||||
			input.multiple = multiple;
 | 
			
		||||
			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 => {
 | 
			
		||||
					res(multiple ? driveFiles : driveFiles[0]);
 | 
			
		||||
| 
						 | 
				
			
			@ -74,6 +77,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
 | 
			
		|||
			text: label,
 | 
			
		||||
			type: 'label'
 | 
			
		||||
		} : undefined, {
 | 
			
		||||
			type: 'switch',
 | 
			
		||||
			text: i18n.ts.keepOriginalUploading,
 | 
			
		||||
			ref: keepOriginal
 | 
			
		||||
		}, {
 | 
			
		||||
			text: i18n.ts.upload,
 | 
			
		||||
			icon: 'fas fa-upload',
 | 
			
		||||
			action: chooseFileFromPc
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,6 +43,10 @@ export const defaultStore = markRaw(new Storage('base', {
 | 
			
		|||
		where: 'account',
 | 
			
		||||
		default: 'yyyy-MM-dd HH-mm-ss [{{number}}]'
 | 
			
		||||
	},
 | 
			
		||||
	keepOriginalUploading: {
 | 
			
		||||
		where: 'account',
 | 
			
		||||
		default: false
 | 
			
		||||
	},
 | 
			
		||||
	memo: {
 | 
			
		||||
		where: 'account',
 | 
			
		||||
		default: null
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										20
									
								
								packages/client/src/types/menu.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								packages/client/src/types/menu.ts
									
										
									
									
									
										Normal 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;
 | 
			
		||||
| 
						 | 
				
			
			@ -54,13 +54,13 @@ const charts = ref([]);
 | 
			
		|||
const fetching = ref(true);
 | 
			
		||||
 | 
			
		||||
const fetch = async () => {
 | 
			
		||||
	const instances = await os.api('federation/instances', {
 | 
			
		||||
	const fetchedInstances = await os.api('federation/instances', {
 | 
			
		||||
		sort: '+lastCommunicatedAt',
 | 
			
		||||
		limit: 5
 | 
			
		||||
	});
 | 
			
		||||
	const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
 | 
			
		||||
	instances.value = instances;
 | 
			
		||||
	charts.value = charts;
 | 
			
		||||
	const fetchedCharts = await Promise.all(fetchedInstances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
 | 
			
		||||
	instances.value = fetchedInstances;
 | 
			
		||||
	charts.value = fetchedCharts;
 | 
			
		||||
	fetching.value = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4139,10 +4139,10 @@ minimist@^1.2.0, minimist@^1.2.5:
 | 
			
		|||
  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
 | 
			
		||||
  integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 | 
			
		||||
 | 
			
		||||
misskey-js@0.0.13:
 | 
			
		||||
  version "0.0.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.13.tgz#03a4e469186e28752d599dc4093519eb64647970"
 | 
			
		||||
  integrity sha512-kBdJdfe281gtykzzsrN3IAxWUQIimzPiJGyKWf863ggWJlWYVPmP9hTFlX2z8oPOaypgVBPEPHyw/jNUdc2DbQ==
 | 
			
		||||
misskey-js@0.0.14:
 | 
			
		||||
  version "0.0.14"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d"
 | 
			
		||||
  integrity sha512-bvLx6U3OwQwqHfp/WKwIVwdvNYAAPk0+YblXyxmSG3dwlzCgBRRLcB8o6bNruUDyJgh3t73pLDcOz3myxcUmww==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    autobind-decorator "^2.4.0"
 | 
			
		||||
    eventemitter3 "^4.0.7"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,6 +37,7 @@ module.exports = {
 | 
			
		|||
			]
 | 
			
		||||
		}],
 | 
			
		||||
		*/
 | 
			
		||||
		'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
 | 
			
		||||
		'no-multi-spaces': ['error'],
 | 
			
		||||
		'no-var': ['error'],
 | 
			
		||||
		'prefer-arrow-callback': ['error'],
 | 
			
		||||
| 
						 | 
				
			
			@ -56,7 +57,7 @@ module.exports = {
 | 
			
		|||
		'object-curly-spacing': ['error', 'always'],
 | 
			
		||||
		'space-infix-ops': ['error'],
 | 
			
		||||
		'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-inferrable-types': ['warn'],
 | 
			
		||||
		'@typescript-eslint/no-empty-function': ['off'],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue