summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-10-23 01:08:45 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-10-23 01:08:45 +0900
commitd0d5068f728e13f3ebe1dc227ddaacf380817ec4 (patch)
tree7bb95207e01bff1bee9877829c0556d3ecf62176 /src
parentMerge branch 'develop' (diff)
parent12.93.0 (diff)
downloadmisskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.tar.gz
misskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.tar.bz2
misskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.zip
Merge branch 'develop'
Diffstat (limited to 'src')
-rw-r--r--src/client/components/chart.vue628
-rw-r--r--src/client/components/form/input.vue2
-rw-r--r--src/client/components/form/radios.vue12
-rw-r--r--src/client/components/form/select.vue65
-rw-r--r--src/client/components/form/textarea.vue2
-rw-r--r--src/client/components/global/emoji.vue8
-rw-r--r--src/client/components/global/header.vue6
-rw-r--r--src/client/components/instance-stats.vue487
-rw-r--r--src/client/components/mfm.ts2
-rw-r--r--src/client/components/number-diff.vue47
-rw-r--r--src/client/components/post-form.vue38
-rw-r--r--src/client/components/queue-chart.vue212
-rw-r--r--src/client/components/tab.vue2
-rw-r--r--src/client/components/ui/container.vue9
-rw-r--r--src/client/components/ui/menu.vue5
-rw-r--r--src/client/components/ui/popup-menu.vue6
-rw-r--r--src/client/components/ui/super-menu.vue2
-rw-r--r--src/client/os.ts7
-rw-r--r--src/client/pages/admin/abuses.vue (renamed from src/client/pages/instance/abuses.vue)0
-rw-r--r--src/client/pages/admin/ads.vue (renamed from src/client/pages/instance/ads.vue)0
-rw-r--r--src/client/pages/admin/announcements.vue (renamed from src/client/pages/instance/announcements.vue)0
-rw-r--r--src/client/pages/admin/bot-protection.vue (renamed from src/client/pages/instance/bot-protection.vue)0
-rw-r--r--src/client/pages/admin/database.vue (renamed from src/client/pages/instance/database.vue)0
-rw-r--r--src/client/pages/admin/email-settings.vue (renamed from src/client/pages/instance/email-settings.vue)0
-rw-r--r--src/client/pages/admin/emoji-edit-dialog.vue (renamed from src/client/pages/instance/emoji-edit-dialog.vue)0
-rw-r--r--src/client/pages/admin/emojis.vue (renamed from src/client/pages/instance/emojis.vue)0
-rw-r--r--src/client/pages/admin/file-dialog.vue (renamed from src/client/pages/instance/file-dialog.vue)0
-rw-r--r--src/client/pages/admin/files-settings.vue (renamed from src/client/pages/instance/files-settings.vue)0
-rw-r--r--src/client/pages/admin/files.vue (renamed from src/client/pages/instance/files.vue)0
-rw-r--r--src/client/pages/admin/index.vue (renamed from src/client/pages/instance/index.vue)52
-rw-r--r--src/client/pages/admin/instance-block.vue (renamed from src/client/pages/instance/instance-block.vue)0
-rw-r--r--src/client/pages/admin/instance.vue (renamed from src/client/pages/instance/instance.vue)266
-rw-r--r--src/client/pages/admin/integrations-discord.vue (renamed from src/client/pages/instance/integrations-discord.vue)0
-rw-r--r--src/client/pages/admin/integrations-github.vue (renamed from src/client/pages/instance/integrations-github.vue)0
-rw-r--r--src/client/pages/admin/integrations-twitter.vue (renamed from src/client/pages/instance/integrations-twitter.vue)0
-rw-r--r--src/client/pages/admin/integrations.vue (renamed from src/client/pages/instance/integrations.vue)6
-rw-r--r--src/client/pages/admin/metrics.vue (renamed from src/client/pages/instance/metrics.vue)83
-rw-r--r--src/client/pages/admin/object-storage.vue (renamed from src/client/pages/instance/object-storage.vue)0
-rw-r--r--src/client/pages/admin/other-settings.vue (renamed from src/client/pages/instance/other-settings.vue)0
-rw-r--r--src/client/pages/admin/overview.vue242
-rw-r--r--src/client/pages/admin/proxy-account.vue (renamed from src/client/pages/instance/proxy-account.vue)0
-rw-r--r--src/client/pages/admin/queue.chart.vue102
-rw-r--r--src/client/pages/admin/queue.vue (renamed from src/client/pages/instance/queue.vue)0
-rw-r--r--src/client/pages/admin/relays.vue (renamed from src/client/pages/instance/relays.vue)0
-rw-r--r--src/client/pages/admin/security.vue (renamed from src/client/pages/instance/security.vue)2
-rw-r--r--src/client/pages/admin/service-worker.vue (renamed from src/client/pages/instance/service-worker.vue)0
-rw-r--r--src/client/pages/admin/settings.vue (renamed from src/client/pages/instance/settings.vue)0
-rw-r--r--src/client/pages/admin/users.vue (renamed from src/client/pages/instance/users.vue)0
-rw-r--r--src/client/pages/explore.vue19
-rw-r--r--src/client/pages/instance-info.vue268
-rw-r--r--src/client/pages/instance/logs.vue97
-rw-r--r--src/client/pages/instance/overview.vue127
-rw-r--r--src/client/pages/instance/queue.chart.vue218
-rw-r--r--src/client/pages/settings/import-export.vue20
-rw-r--r--src/client/pages/settings/privacy.vue7
-rw-r--r--src/client/pages/settings/theme.manage.vue14
-rw-r--r--src/client/pages/share.vue158
-rw-r--r--src/client/pages/user/index.timeline.vue7
-rw-r--r--src/client/pages/user/index.vue9
-rw-r--r--src/client/pages/user/reactions.vue81
-rw-r--r--src/client/router.ts4
-rw-r--r--src/client/scripts/hpml/lib.ts7
-rw-r--r--src/client/scripts/theme.ts2
-rw-r--r--src/client/themes/d-astro.json52
-rw-r--r--src/client/themes/d-future.json52
-rw-r--r--src/client/themes/d-persimmon.json52
-rw-r--r--src/client/themes/d-pumpkin.json588
-rw-r--r--src/client/themes/l-apricot.json52
-rw-r--r--src/client/themes/l-rainy.json52
-rw-r--r--src/client/themes/l-sushi.json518
-rw-r--r--src/client/themes/l-vivid.json52
-rw-r--r--src/client/ui/_common_/sidebar.vue2
-rw-r--r--src/client/ui/chat/post-form.vue6
-rw-r--r--src/client/ui/default.header.vue2
-rw-r--r--src/client/ui/default.sidebar.vue2
-rw-r--r--src/db/postgre.ts2
-rw-r--r--src/docs/ar-SA/advanced/share-page.md54
-rw-r--r--src/docs/ar-SA/features/timeline.md4
-rw-r--r--src/docs/cs-CZ/advanced/share-page.md54
-rw-r--r--src/docs/da-DK/advanced/share-page.md54
-rw-r--r--src/docs/de-DE/advanced/share-page.md54
-rw-r--r--src/docs/en-US/advanced/share-page.md54
-rw-r--r--src/docs/en-US/general/glossary.md4
-rw-r--r--src/docs/eo-UY/advanced/share-page.md54
-rw-r--r--src/docs/eo-UY/features/mfm.md2
-rw-r--r--src/docs/eo-UY/features/mute-and-block.md18
-rw-r--r--src/docs/eo-UY/features/note.md4
-rw-r--r--src/docs/eo-UY/general/faq.md2
-rw-r--r--src/docs/eo-UY/general/troubleshooting.md4
-rw-r--r--src/docs/es-ES/advanced/share-page.md54
-rw-r--r--src/docs/fr-FR/advanced/aiscript.md2
-rw-r--r--src/docs/fr-FR/advanced/share-page.md54
-rw-r--r--src/docs/fr-FR/general/glossary.md16
-rw-r--r--src/docs/ht-HT/advanced/share-page.md54
-rw-r--r--src/docs/id-ID/advanced/share-page.md54
-rw-r--r--src/docs/it-IT/advanced/share-page.md54
-rw-r--r--src/docs/ja-JP/advanced/share-page.md56
-rw-r--r--src/docs/ja-KS/advanced/share-page.md54
-rw-r--r--src/docs/jbo-EN/advanced/share-page.md54
-rw-r--r--src/docs/kab-KAB/advanced/share-page.md54
-rw-r--r--src/docs/kn-IN/advanced/share-page.md54
-rw-r--r--src/docs/ko-KR/advanced/share-page.md54
-rw-r--r--src/docs/nl-NL/advanced/share-page.md54
-rw-r--r--src/docs/no-NO/advanced/share-page.md54
-rw-r--r--src/docs/pl-PL/advanced/share-page.md54
-rw-r--r--src/docs/pt-PT/advanced/share-page.md54
-rw-r--r--src/docs/ru-RU/advanced/share-page.md54
-rw-r--r--src/docs/th-TH/advanced/share-page.md54
-rw-r--r--src/docs/ug-CN/advanced/share-page.md54
-rw-r--r--src/docs/uk-UA/advanced/share-page.md54
-rw-r--r--src/docs/zh-CN/advanced/share-page.md54
-rw-r--r--src/docs/zh-TW/advanced/share-page.md54
-rw-r--r--src/misc/acct.ts9
-rw-r--r--src/models/entities/log.ts46
-rw-r--r--src/models/entities/user-profile.ts5
-rw-r--r--src/models/index.ts2
-rw-r--r--src/models/repositories/note-reaction.ts14
-rw-r--r--src/models/repositories/signin.ts2
-rw-r--r--src/models/repositories/user.ts1
-rw-r--r--src/queue/index.ts48
-rw-r--r--src/queue/processors/db/import-blocking.ts74
-rw-r--r--src/queue/processors/db/import-muting.ts83
-rw-r--r--src/queue/processors/db/index.ts4
-rw-r--r--src/queue/processors/system/index.ts12
-rw-r--r--src/queue/processors/system/resync-charts.ts21
-rw-r--r--src/queue/queues.ts1
-rw-r--r--src/remote/activitypub/kernel/undo/like.ts6
-rw-r--r--src/server/api/common/read-messaging-message.ts2
-rw-r--r--src/server/api/endpoints/admin/logs.ts126
-rw-r--r--src/server/api/endpoints/admin/resync-chart.ts4
-rw-r--r--src/server/api/endpoints/antennas/update.ts2
-rw-r--r--src/server/api/endpoints/blocking/create.ts13
-rw-r--r--src/server/api/endpoints/i/import-blocking.ts60
-rw-r--r--src/server/api/endpoints/i/import-muting.ts60
-rw-r--r--src/server/api/endpoints/i/notifications.ts11
-rw-r--r--src/server/api/endpoints/i/update.ts5
-rw-r--r--src/server/api/endpoints/mute/create.ts2
-rw-r--r--src/server/api/endpoints/users/reactions.ts79
-rw-r--r--src/server/api/endpoints/users/search-by-username-and-host.ts74
-rw-r--r--src/server/api/endpoints/users/search.ts101
-rw-r--r--src/server/api/stream/channels/antenna.ts11
-rw-r--r--src/server/api/stream/channels/channel.ts3
-rw-r--r--src/server/api/stream/channels/main.ts24
-rw-r--r--src/server/api/stream/channels/messaging.ts6
-rw-r--r--src/server/api/stream/index.ts35
-rw-r--r--src/server/api/stream/types.ts299
-rw-r--r--src/server/web/manifest.json2
-rw-r--r--src/services/blocking/create.ts4
-rw-r--r--src/services/logger.ts16
-rw-r--r--src/services/stream.ts53
150 files changed, 4334 insertions, 1965 deletions
diff --git a/src/client/components/chart.vue b/src/client/components/chart.vue
new file mode 100644
index 0000000000..8eb9f93f33
--- /dev/null
+++ b/src/client/components/chart.vue
@@ -0,0 +1,628 @@
+<template>
+<canvas ref="chartEl"></canvas>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref, watch, PropType } from 'vue';
+import {
+ Chart,
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle,
+ Filler,
+} from 'chart.js';
+import 'chartjs-adapter-date-fns';
+import { enUS } from 'date-fns/locale';
+import zoomPlugin from 'chartjs-plugin-zoom';
+import * as os from '@client/os';
+import { defaultStore } from '@client/store';
+
+Chart.register(
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle,
+ Filler,
+ zoomPlugin,
+);
+
+const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
+const negate = arr => arr.map(x => -x);
+const alpha = (hex, a) => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+ const r = parseInt(result[1], 16);
+ const g = parseInt(result[2], 16);
+ const b = parseInt(result[3], 16);
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
+
+const colors = ['#008FFB', '#00E396', '#FEB019', '#FF4560'];
+const getColor = (i) => {
+ return colors[i % colors.length];
+};
+
+export default defineComponent({
+ props: {
+ src: {
+ type: String,
+ required: true,
+ },
+ args: {
+ type: Object,
+ required: false,
+ },
+ limit: {
+ type: Number,
+ required: false,
+ default: 90
+ },
+ span: {
+ type: String as PropType<'hour' | 'day'>,
+ required: true,
+ },
+ detailed: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ },
+
+ setup(props) {
+ const now = new Date();
+ let chartInstance: Chart = null;
+ let data: {
+ series: {
+ name: string;
+ type: 'line' | 'area';
+ color?: string;
+ borderDash?: number[];
+ hidden?: boolean;
+ data: {
+ x: number;
+ y: number;
+ }[];
+ }[];
+ } = null;
+
+ const chartEl = ref<HTMLCanvasElement>(null);
+ const fetching = ref(true);
+
+ const getDate = (ago: number) => {
+ const y = now.getFullYear();
+ const m = now.getMonth();
+ const d = now.getDate();
+ const h = now.getHours();
+
+ return props.span === 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
+ };
+
+ const format = (arr) => {
+ return arr.map((v, i) => ({
+ x: getDate(i).getTime(),
+ y: v
+ }));
+ };
+
+ const render = () => {
+ if (chartInstance) {
+ chartInstance.destroy();
+ }
+
+ const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+
+ // フォントカラー
+ Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+
+ chartInstance = new Chart(chartEl.value, {
+ type: 'line',
+ data: {
+ labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
+ datasets: data.series.map((x, i) => ({
+ parsing: false,
+ label: x.name,
+ data: x.data.slice().reverse(),
+ pointRadius: 0,
+ tension: 0,
+ borderWidth: 2,
+ borderColor: x.color ? x.color : getColor(i),
+ borderDash: x.borderDash || [],
+ borderJoinStyle: 'round',
+ backgroundColor: alpha(x.color ? x.color : getColor(i), 0.1),
+ fill: x.type === 'area',
+ hidden: !!x.hidden,
+ })),
+ },
+ options: {
+ aspectRatio: 2.5,
+ layout: {
+ padding: {
+ left: 16,
+ right: 16,
+ top: 16,
+ bottom: 8,
+ },
+ },
+ scales: {
+ x: {
+ type: 'time',
+ time: {
+ stepSize: 1,
+ unit: props.span === 'day' ? 'month' : 'day',
+ },
+ grid: {
+ display: props.detailed,
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
+ },
+ ticks: {
+ display: props.detailed,
+ },
+ adapters: {
+ date: {
+ locale: enUS,
+ },
+ },
+ min: getDate(props.limit).getTime(),
+ },
+ y: {
+ position: 'left',
+ grid: {
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
+ },
+ ticks: {
+ display: props.detailed,
+ },
+ },
+ },
+ interaction: {
+ intersect: false,
+ },
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: {
+ boxWidth: 16,
+ },
+ },
+ tooltip: {
+ mode: 'index',
+ animation: {
+ duration: 0,
+ },
+ },
+ zoom: {
+ pan: {
+ enabled: true,
+ },
+ zoom: {
+ wheel: {
+ enabled: true,
+ },
+ pinch: {
+ enabled: true,
+ },
+ drag: {
+ enabled: false,
+ },
+ mode: 'x',
+ },
+ limits: {
+ x: {
+ min: 'original',
+ max: 'original',
+ },
+ y: {
+ min: 'original',
+ max: 'original',
+ },
+ }
+ },
+ },
+ },
+ });
+ };
+
+ const exportData = () => {
+ // TODO
+ };
+
+ const fetchFederationInstancesChart = async (total: boolean): Promise<typeof data> => {
+ const raw = await os.api('charts/federation', { limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'Instances',
+ type: 'area',
+ data: format(total
+ ? raw.instance.total
+ : sum(raw.instance.inc, negate(raw.instance.dec))
+ ),
+ }],
+ };
+ };
+
+ const fetchNotesChart = async (type: string): Promise<typeof data> => {
+ const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'All',
+ type: 'line',
+ borderDash: [5, 5],
+ data: format(type == 'combined'
+ ? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
+ : sum(raw[type].inc, negate(raw[type].dec))
+ ),
+ }, {
+ name: 'Renotes',
+ type: 'area',
+ data: format(type == 'combined'
+ ? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
+ : raw[type].diffs.renote
+ ),
+ }, {
+ name: 'Replies',
+ type: 'area',
+ data: format(type == 'combined'
+ ? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
+ : raw[type].diffs.reply
+ ),
+ }, {
+ name: 'Normal',
+ type: 'area',
+ data: format(type == 'combined'
+ ? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
+ : raw[type].diffs.normal
+ ),
+ }],
+ };
+ };
+
+ const fetchNotesTotalChart = async (): Promise<typeof data> => {
+ const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'Combined',
+ type: 'line',
+ data: format(sum(raw.local.total, raw.remote.total)),
+ }, {
+ name: 'Local',
+ type: 'area',
+ data: format(raw.local.total),
+ }, {
+ name: 'Remote',
+ type: 'area',
+ data: format(raw.remote.total),
+ }],
+ };
+ };
+
+ const fetchUsersChart = async (total: boolean): Promise<typeof data> => {
+ const raw = await os.api('charts/users', { limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'Combined',
+ type: 'line',
+ data: format(total
+ ? sum(raw.local.total, raw.remote.total)
+ : sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
+ ),
+ }, {
+ name: 'Local',
+ type: 'area',
+ data: format(total
+ ? raw.local.total
+ : sum(raw.local.inc, negate(raw.local.dec))
+ ),
+ }, {
+ name: 'Remote',
+ type: 'area',
+ data: format(total
+ ? raw.remote.total
+ : sum(raw.remote.inc, negate(raw.remote.dec))
+ ),
+ }],
+ };
+ };
+
+ const fetchActiveUsersChart = async (): Promise<typeof data> => {
+ const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'Combined',
+ type: 'line',
+ data: format(sum(raw.local.users, raw.remote.users)),
+ }, {
+ name: 'Local',
+ type: 'area',
+ data: format(raw.local.users),
+ }, {
+ name: 'Remote',
+ type: 'area',
+ data: format(raw.remote.users),
+ }],
+ };
+ };
+
+ const fetchDriveChart = async (): Promise<typeof data> => {
+ const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
+ return {
+ bytes: true,
+ series: [{
+ name: 'All',
+ type: 'line',
+ borderDash: [5, 5],
+ data: format(
+ sum(
+ raw.local.incSize,
+ negate(raw.local.decSize),
+ raw.remote.incSize,
+ negate(raw.remote.decSize)
+ )
+ ),
+ }, {
+ name: 'Local +',
+ type: 'area',
+ data: format(raw.local.incSize),
+ }, {
+ name: 'Local -',
+ type: 'area',
+ data: format(negate(raw.local.decSize)),
+ }, {
+ name: 'Remote +',
+ type: 'area',
+ data: format(raw.remote.incSize),
+ }, {
+ name: 'Remote -',
+ type: 'area',
+ data: format(negate(raw.remote.decSize)),
+ }],
+ };
+ };
+
+ const fetchDriveTotalChart = async (): Promise<typeof data> => {
+ const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
+ return {
+ bytes: true,
+ series: [{
+ name: 'Combined',
+ type: 'line',
+ data: format(sum(raw.local.totalSize, raw.remote.totalSize)),
+ }, {
+ name: 'Local',
+ type: 'area',
+ data: format(raw.local.totalSize),
+ }, {
+ name: 'Remote',
+ type: 'area',
+ data: format(raw.remote.totalSize),
+ }],
+ };
+ };
+
+ const fetchDriveFilesChart = async (): Promise<typeof data> => {
+ const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'All',
+ type: 'line',
+ borderDash: [5, 5],
+ data: format(
+ sum(
+ raw.local.incCount,
+ negate(raw.local.decCount),
+ raw.remote.incCount,
+ negate(raw.remote.decCount)
+ )
+ ),
+ }, {
+ name: 'Local +',
+ type: 'area',
+ data: format(raw.local.incCount),
+ }, {
+ name: 'Local -',
+ type: 'area',
+ data: format(negate(raw.local.decCount)),
+ }, {
+ name: 'Remote +',
+ type: 'area',
+ data: format(raw.remote.incCount),
+ }, {
+ name: 'Remote -',
+ type: 'area',
+ data: format(negate(raw.remote.decCount)),
+ }],
+ };
+ };
+
+ const fetchDriveFilesTotalChart = async (): Promise<typeof data> => {
+ const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'Combined',
+ type: 'line',
+ data: format(sum(raw.local.totalCount, raw.remote.totalCount)),
+ }, {
+ name: 'Local',
+ type: 'area',
+ data: format(raw.local.totalCount),
+ }, {
+ name: 'Remote',
+ type: 'area',
+ data: format(raw.remote.totalCount),
+ }],
+ };
+ };
+
+ const fetchInstanceRequestsChart = async (): Promise<typeof data> => {
+ const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'In',
+ type: 'area',
+ color: '#008FFB',
+ data: format(raw.requests.received)
+ }, {
+ name: 'Out (succ)',
+ type: 'area',
+ color: '#00E396',
+ data: format(raw.requests.succeeded)
+ }, {
+ name: 'Out (fail)',
+ type: 'area',
+ color: '#FEB019',
+ data: format(raw.requests.failed)
+ }]
+ };
+ };
+
+ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof data> => {
+ const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'Users',
+ type: 'area',
+ color: '#008FFB',
+ data: format(total
+ ? raw.users.total
+ : sum(raw.users.inc, negate(raw.users.dec))
+ )
+ }]
+ };
+ };
+
+ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof data> => {
+ const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'Notes',
+ type: 'area',
+ color: '#008FFB',
+ data: format(total
+ ? raw.notes.total
+ : sum(raw.notes.inc, negate(raw.notes.dec))
+ )
+ }]
+ };
+ };
+
+ const fetchInstanceFfChart = async (total: boolean): Promise<typeof data> => {
+ const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'Following',
+ type: 'area',
+ color: '#008FFB',
+ data: format(total
+ ? raw.following.total
+ : sum(raw.following.inc, negate(raw.following.dec))
+ )
+ }, {
+ name: 'Followers',
+ type: 'area',
+ color: '#00E396',
+ data: format(total
+ ? raw.followers.total
+ : sum(raw.followers.inc, negate(raw.followers.dec))
+ )
+ }]
+ };
+ };
+
+ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof data> => {
+ const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+ return {
+ bytes: true,
+ series: [{
+ name: 'Drive usage',
+ type: 'area',
+ color: '#008FFB',
+ data: format(total
+ ? raw.drive.totalUsage
+ : sum(raw.drive.incUsage, negate(raw.drive.decUsage))
+ )
+ }]
+ };
+ };
+
+ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof data> => {
+ const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+ return {
+ series: [{
+ name: 'Drive files',
+ type: 'area',
+ color: '#008FFB',
+ data: format(total
+ ? raw.drive.totalFiles
+ : sum(raw.drive.incFiles, negate(raw.drive.decFiles))
+ )
+ }]
+ };
+ };
+
+ const fetchAndRender = async () => {
+ const fetchData = () => {
+ switch (props.src) {
+ case 'federation-instances': return fetchFederationInstancesChart(false);
+ case 'federation-instances-total': return fetchFederationInstancesChart(true);
+ case 'users': return fetchUsersChart(false);
+ case 'users-total': return fetchUsersChart(true);
+ case 'active-users': return fetchActiveUsersChart();
+ case 'notes': return fetchNotesChart('combined');
+ case 'local-notes': return fetchNotesChart('local');
+ case 'remote-notes': return fetchNotesChart('remote');
+ case 'notes-total': return fetchNotesTotalChart();
+ case 'drive': return fetchDriveChart();
+ case 'drive-total': return fetchDriveTotalChart();
+ case 'drive-files': return fetchDriveFilesChart();
+ case 'drive-files-total': return fetchDriveFilesTotalChart();
+
+ case 'instance-requests': return fetchInstanceRequestsChart();
+ case 'instance-users': return fetchInstanceUsersChart(false);
+ case 'instance-users-total': return fetchInstanceUsersChart(true);
+ case 'instance-notes': return fetchInstanceNotesChart(false);
+ case 'instance-notes-total': return fetchInstanceNotesChart(true);
+ case 'instance-ff': return fetchInstanceFfChart(false);
+ case 'instance-ff-total': return fetchInstanceFfChart(true);
+ case 'instance-drive-usage': return fetchInstanceDriveUsageChart(false);
+ case 'instance-drive-usage-total': return fetchInstanceDriveUsageChart(true);
+ case 'instance-drive-files': return fetchInstanceDriveFilesChart(false);
+ case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true);
+ }
+ };
+ fetching.value = true;
+ data = await fetchData();
+ fetching.value = false;
+ render();
+ };
+
+ watch(() => [props.src, props.span], fetchAndRender);
+
+ onMounted(() => {
+ fetchAndRender();
+ });
+
+ return {
+ chartEl,
+ };
+ },
+});
+</script>
diff --git a/src/client/components/form/input.vue b/src/client/components/form/input.vue
index d7b6f77519..591eda9ed5 100644
--- a/src/client/components/form/input.vue
+++ b/src/client/components/form/input.vue
@@ -33,7 +33,7 @@
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import MkButton from '../ui/button.vue';
+import MkButton from '@client/components/ui/button.vue';
import { debounce } from 'throttle-debounce';
export default defineComponent({
diff --git a/src/client/components/form/radios.vue b/src/client/components/form/radios.vue
index 1d3d80172a..998a738202 100644
--- a/src/client/components/form/radios.vue
+++ b/src/client/components/form/radios.vue
@@ -22,7 +22,6 @@ export default defineComponent({
}
},
render() {
- const label = this.$slots.desc();
let options = this.$slots.default();
// なぜかFragmentになることがあるため
@@ -31,7 +30,6 @@ export default defineComponent({
return h('div', {
class: 'novjtcto'
}, [
- h('div', { class: 'label' }, label),
...options.map(option => h(MkRadio, {
key: option.key,
value: option.props.value,
@@ -45,16 +43,6 @@ export default defineComponent({
<style lang="scss">
.novjtcto {
- > .label {
- font-size: 0.85em;
- padding: 0 0 8px 12px;
- user-select: none;
-
- &:empty {
- display: none;
- }
- }
-
&:first-child {
margin-top: 0;
}
diff --git a/src/client/components/form/select.vue b/src/client/components/form/select.vue
index 257e2cc990..30ccfd312b 100644
--- a/src/client/components/form/select.vue
+++ b/src/client/components/form/select.vue
@@ -1,9 +1,9 @@
<template>
<div class="vblkjoeq">
<div class="label" @click="focus"><slot name="label"></slot></div>
- <div class="input" :class="{ inline, disabled, focused }">
+ <div class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick" ref="container">
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
- <select ref="inputEl"
+ <select class="select" ref="inputEl"
v-model="v"
:disabled="disabled"
:required="required"
@@ -25,7 +25,8 @@
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import MkButton from '../ui/button.vue';
+import MkButton from '@client/components/ui/button.vue';
+import * as os from '@client/os';
export default defineComponent({
components: {
@@ -81,6 +82,7 @@ export default defineComponent({
const inputEl = ref(null);
const prefixEl = ref(null);
const suffixEl = ref(null);
+ const container = ref(null);
const focus = () => inputEl.value.focus();
const onInput = (ev) => {
@@ -132,6 +134,47 @@ export default defineComponent({
});
});
+ const onClick = (ev: MouseEvent) => {
+ focused.value = true;
+
+ const menu = [];
+ let options = context.slots.default();
+
+ for (const optionOrOptgroup of options) {
+ if (optionOrOptgroup.type === 'optgroup') {
+ const optgroup = optionOrOptgroup;
+ menu.push({
+ type: 'label',
+ text: optgroup.props.label,
+ });
+ for (const option of optgroup.children) {
+ menu.push({
+ text: option.children,
+ active: v.value === option.props.value,
+ action: () => {
+ v.value = option.props.value;
+ },
+ });
+ }
+ } else {
+ const option = optionOrOptgroup;
+ menu.push({
+ text: option.children,
+ active: v.value === option.props.value,
+ action: () => {
+ v.value = option.props.value;
+ },
+ });
+ }
+ }
+
+ os.popupMenu(menu, container.value, {
+ width: container.value.offsetWidth,
+ }).then(() => {
+ focused.value = false;
+ });
+ };
+
return {
v,
focused,
@@ -141,8 +184,10 @@ export default defineComponent({
inputEl,
prefixEl,
suffixEl,
+ container,
focus,
onInput,
+ onClick,
updated,
};
},
@@ -174,8 +219,15 @@ export default defineComponent({
> .input {
$height: 42px;
position: relative;
+ cursor: pointer;
- > select {
+ &:hover {
+ > .select {
+ border-color: var(--inputBorderHover);
+ }
+ }
+
+ > .select {
appearance: none;
-webkit-appearance: none;
display: block;
@@ -195,10 +247,7 @@ export default defineComponent({
box-sizing: border-box;
cursor: pointer;
transition: border-color 0.1s ease-out;
-
- &:hover {
- border-color: var(--inputBorderHover);
- }
+ pointer-events: none;
}
> .prefix,
diff --git a/src/client/components/form/textarea.vue b/src/client/components/form/textarea.vue
index 50be69f930..048e9032df 100644
--- a/src/client/components/form/textarea.vue
+++ b/src/client/components/form/textarea.vue
@@ -26,7 +26,7 @@
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import MkButton from '../ui/button.vue';
+import MkButton from '@client/components/ui/button.vue';
import { debounce } from 'throttle-debounce';
export default defineComponent({
diff --git a/src/client/components/global/emoji.vue b/src/client/components/global/emoji.vue
index f4ebd5f3b3..f92e35c38f 100644
--- a/src/client/components/global/emoji.vue
+++ b/src/client/components/global/emoji.vue
@@ -27,8 +27,7 @@ export default defineComponent({
default: false
},
customEmojis: {
- required: false,
- default: () => []
+ required: false
},
isReaction: {
type: Boolean,
@@ -58,10 +57,7 @@ export default defineComponent({
},
ce() {
- let ce = [];
- if (this.customEmojis) ce = ce.concat(this.customEmojis);
- if (this.$instance && this.$instance.emojis) ce = ce.concat(this.$instance.emojis);
- return ce;
+ return this.customEmojis || this.$instance?.emojis || [];
}
},
diff --git a/src/client/components/global/header.vue b/src/client/components/global/header.vue
index a4466da498..2bf490c98a 100644
--- a/src/client/components/global/header.vue
+++ b/src/client/components/global/header.vue
@@ -203,6 +203,12 @@ export default defineComponent({
&.thin {
--height: 50px;
+
+ > .buttons {
+ > .button {
+ font-size: 0.9em;
+ }
+ }
}
&.slim {
diff --git a/src/client/components/instance-stats.vue b/src/client/components/instance-stats.vue
index 5e7c71ea65..fd0b75609f 100644
--- a/src/client/components/instance-stats.vue
+++ b/src/client/components/instance-stats.vue
@@ -24,35 +24,26 @@
<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
</optgroup>
</MkSelect>
- <MkSelect v-model="chartSpan" style="margin: 0;">
+ <MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
<option value="hour">{{ $ts.perHour }}</option>
<option value="day">{{ $ts.perDay }}</option>
</MkSelect>
</div>
- <canvas ref="chart"></canvas>
+ <MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
</div>
</template>
<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import Chart from 'chart.js';
-import MkSelect from './form/select.vue';
-import number from '@client/filters/number';
-
-const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
-const negate = arr => arr.map(x => -x);
-const alpha = (hex, a) => {
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
- const r = parseInt(result[1], 16);
- const g = parseInt(result[2], 16);
- const b = parseInt(result[3], 16);
- return `rgba(${r}, ${g}, ${b}, ${a})`;
-};
+import { defineComponent, onMounted, ref, watch } from 'vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkChart from '@client/components/chart.vue';
import * as os from '@client/os';
+import { defaultStore } from '@client/store';
export default defineComponent({
components: {
- MkSelect
+ MkSelect,
+ MkChart,
},
props: {
@@ -68,463 +59,15 @@ export default defineComponent({
},
},
- data() {
- return {
- notesLocalWoW: 0,
- notesLocalDoD: 0,
- notesRemoteWoW: 0,
- notesRemoteDoD: 0,
- usersLocalWoW: 0,
- usersLocalDoD: 0,
- usersRemoteWoW: 0,
- usersRemoteDoD: 0,
- now: null,
- chart: null,
- chartInstance: null,
- chartSrc: 'notes',
- chartSpan: 'hour',
- }
- },
-
- computed: {
- data(): any {
- if (this.chart == null) return null;
- switch (this.chartSrc) {
- case 'federation-instances': return this.federationInstancesChart(false);
- case 'federation-instances-total': return this.federationInstancesChart(true);
- case 'users': return this.usersChart(false);
- case 'users-total': return this.usersChart(true);
- case 'active-users': return this.activeUsersChart();
- case 'notes': return this.notesChart('combined');
- case 'local-notes': return this.notesChart('local');
- case 'remote-notes': return this.notesChart('remote');
- case 'notes-total': return this.notesTotalChart();
- case 'drive': return this.driveChart();
- case 'drive-total': return this.driveTotalChart();
- case 'drive-files': return this.driveFilesChart();
- case 'drive-files-total': return this.driveFilesTotalChart();
- }
- },
-
- stats(): any[] {
- const stats =
- this.chartSpan == 'day' ? this.chart.perDay :
- this.chartSpan == 'hour' ? this.chart.perHour :
- null;
-
- return stats;
- }
- },
-
- watch: {
- chartSrc() {
- this.renderChart();
- },
-
- chartSpan() {
- this.renderChart();
- }
- },
-
- async created() {
- this.now = new Date();
+ setup() {
+ const chartSpan = ref<'hour' | 'day'>('hour');
+ const chartSrc = ref('notes');
- this.fetchChart();
+ return {
+ chartSrc,
+ chartSpan,
+ };
},
-
- methods: {
- async fetchChart() {
- const [perHour, perDay] = await Promise.all([Promise.all([
- os.api('charts/federation', { limit: this.chartLimit, span: 'hour' }),
- os.api('charts/users', { limit: this.chartLimit, span: 'hour' }),
- os.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }),
- os.api('charts/notes', { limit: this.chartLimit, span: 'hour' }),
- os.api('charts/drive', { limit: this.chartLimit, span: 'hour' }),
- ]), Promise.all([
- os.api('charts/federation', { limit: this.chartLimit, span: 'day' }),
- os.api('charts/users', { limit: this.chartLimit, span: 'day' }),
- os.api('charts/active-users', { limit: this.chartLimit, span: 'day' }),
- os.api('charts/notes', { limit: this.chartLimit, span: 'day' }),
- os.api('charts/drive', { limit: this.chartLimit, span: 'day' }),
- ])]);
-
- const chart = {
- perHour: {
- federation: perHour[0],
- users: perHour[1],
- activeUsers: perHour[2],
- notes: perHour[3],
- drive: perHour[4],
- },
- perDay: {
- federation: perDay[0],
- users: perDay[1],
- activeUsers: perDay[2],
- notes: perDay[3],
- drive: perDay[4],
- }
- };
-
- this.chart = chart;
-
- this.renderChart();
- },
-
- renderChart() {
- if (this.chartInstance) {
- this.chartInstance.destroy();
- }
-
- // TODO: var(--panel)の色が暗いか明るいかで判定する
- const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
-
- Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
- this.chartInstance = markRaw(new Chart(this.$refs.chart, {
- type: 'line',
- data: {
- labels: new Array(this.chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
- datasets: this.data.series.map(x => ({
- label: x.name,
- data: x.data.slice().reverse(),
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: x.color,
- borderDash: x.borderDash || [],
- backgroundColor: alpha(x.color, 0.1),
- fill: x.fill == null ? true : x.fill,
- hidden: !!x.hidden
- }))
- },
- options: {
- aspectRatio: 2.5,
- layout: {
- padding: {
- left: 16,
- right: 16,
- top: 16,
- bottom: 8
- }
- },
- legend: {
- position: 'bottom',
- labels: {
- boxWidth: 16,
- }
- },
- scales: {
- xAxes: [{
- type: 'time',
- time: {
- stepSize: 1,
- unit: this.chartSpan == 'day' ? 'month' : 'day',
- },
- gridLines: {
- display: this.detailed,
- color: gridColor,
- zeroLineColor: gridColor,
- },
- ticks: {
- display: this.detailed
- }
- }],
- yAxes: [{
- position: 'left',
- gridLines: {
- color: gridColor,
- zeroLineColor: gridColor,
- },
- ticks: {
- display: this.detailed
- }
- }]
- },
- tooltips: {
- intersect: false,
- mode: 'index',
- }
- }
- }));
- },
-
- getDate(ago: number) {
- const y = this.now.getFullYear();
- const m = this.now.getMonth();
- const d = this.now.getDate();
- const h = this.now.getHours();
-
- return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
- },
-
- format(arr) {
- const now = Date.now();
- return arr.map((v, i) => ({
- x: new Date(now - ((this.chartSpan == 'day' ? 86400000 :3600000 ) * i)),
- y: v
- }));
- },
-
- federationInstancesChart(total: boolean): any {
- return {
- series: [{
- name: 'Instances',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.federation.instance.total
- : sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec))
- )
- }]
- };
- },
-
- notesChart(type: string): any {
- return {
- series: [{
- name: 'All',
- type: 'line',
- color: '#008FFB',
- borderDash: [5, 5],
- fill: false,
- data: this.format(type == 'combined'
- ? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec))
- : sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec))
- )
- }, {
- name: 'Renotes',
- type: 'area',
- color: '#00E396',
- data: this.format(type == 'combined'
- ? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote)
- : this.stats.notes[type].diffs.renote
- )
- }, {
- name: 'Replies',
- type: 'area',
- color: '#FEB019',
- data: this.format(type == 'combined'
- ? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply)
- : this.stats.notes[type].diffs.reply
- )
- }, {
- name: 'Normal',
- type: 'area',
- color: '#FF4560',
- data: this.format(type == 'combined'
- ? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal)
- : this.stats.notes[type].diffs.normal
- )
- }]
- };
- },
-
- notesTotalChart(): any {
- return {
- series: [{
- name: 'Combined',
- type: 'line',
- color: '#008FFB',
- data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total))
- }, {
- name: 'Local',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.notes.local.total)
- }, {
- name: 'Remote',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.notes.remote.total)
- }]
- };
- },
-
- usersChart(total: boolean): any {
- return {
- series: [{
- name: 'Combined',
- type: 'line',
- color: '#008FFB',
- data: this.format(total
- ? sum(this.stats.users.local.total, this.stats.users.remote.total)
- : sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
- )
- }, {
- name: 'Local',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(total
- ? this.stats.users.local.total
- : sum(this.stats.users.local.inc, negate(this.stats.users.local.dec))
- )
- }, {
- name: 'Remote',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(total
- ? this.stats.users.remote.total
- : sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
- )
- }]
- };
- },
-
- activeUsersChart(): any {
- return {
- series: [{
- name: 'Combined',
- type: 'line',
- color: '#008FFB',
- data: this.format(sum(this.stats.activeUsers.local.count, this.stats.activeUsers.remote.count))
- }, {
- name: 'Local',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.activeUsers.local.count)
- }, {
- name: 'Remote',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.activeUsers.remote.count)
- }]
- };
- },
-
- driveChart(): any {
- return {
- bytes: true,
- series: [{
- name: 'All',
- type: 'line',
- color: '#09d8e2',
- borderDash: [5, 5],
- fill: false,
- data: this.format(
- sum(
- this.stats.drive.local.incSize,
- negate(this.stats.drive.local.decSize),
- this.stats.drive.remote.incSize,
- negate(this.stats.drive.remote.decSize)
- )
- )
- }, {
- name: 'Local +',
- type: 'area',
- color: '#008FFB',
- data: this.format(this.stats.drive.local.incSize)
- }, {
- name: 'Local -',
- type: 'area',
- color: '#FF4560',
- data: this.format(negate(this.stats.drive.local.decSize))
- }, {
- name: 'Remote +',
- type: 'area',
- color: '#00E396',
- data: this.format(this.stats.drive.remote.incSize)
- }, {
- name: 'Remote -',
- type: 'area',
- color: '#FEB019',
- data: this.format(negate(this.stats.drive.remote.decSize))
- }]
- };
- },
-
- driveTotalChart(): any {
- return {
- bytes: true,
- series: [{
- name: 'Combined',
- type: 'line',
- color: '#008FFB',
- data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize))
- }, {
- name: 'Local',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.drive.local.totalSize)
- }, {
- name: 'Remote',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.drive.remote.totalSize)
- }]
- };
- },
-
- driveFilesChart(): any {
- return {
- series: [{
- name: 'All',
- type: 'line',
- color: '#09d8e2',
- borderDash: [5, 5],
- fill: false,
- data: this.format(
- sum(
- this.stats.drive.local.incCount,
- negate(this.stats.drive.local.decCount),
- this.stats.drive.remote.incCount,
- negate(this.stats.drive.remote.decCount)
- )
- )
- }, {
- name: 'Local +',
- type: 'area',
- color: '#008FFB',
- data: this.format(this.stats.drive.local.incCount)
- }, {
- name: 'Local -',
- type: 'area',
- color: '#FF4560',
- data: this.format(negate(this.stats.drive.local.decCount))
- }, {
- name: 'Remote +',
- type: 'area',
- color: '#00E396',
- data: this.format(this.stats.drive.remote.incCount)
- }, {
- name: 'Remote -',
- type: 'area',
- color: '#FEB019',
- data: this.format(negate(this.stats.drive.remote.decCount))
- }]
- };
- },
-
- driveFilesTotalChart(): any {
- return {
- series: [{
- name: 'Combined',
- type: 'line',
- color: '#008FFB',
- data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount))
- }, {
- name: 'Local',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.drive.local.totalCount)
- }, {
- name: 'Remote',
- type: 'area',
- color: '#008FFB',
- hidden: true,
- data: this.format(this.stats.drive.remote.totalCount)
- }]
- };
- },
-
- number
- }
});
</script>
diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts
index 2bdd7d46ee..f3411cadc3 100644
--- a/src/client/components/mfm.ts
+++ b/src/client/components/mfm.ts
@@ -185,7 +185,7 @@ export default defineComponent({
}
}
if (style == null) {
- return h('span', {}, ['[', token.props.name, ' ', ...genEl(token.children), ']']);
+ return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']);
} else {
return h('span', {
style: 'display: inline-block;' + style,
diff --git a/src/client/components/number-diff.vue b/src/client/components/number-diff.vue
new file mode 100644
index 0000000000..690f89dd59
--- /dev/null
+++ b/src/client/components/number-diff.vue
@@ -0,0 +1,47 @@
+<template>
+<span class="ceaaebcd" :class="{ isPlus, isMinus, isZero }">
+ <slot name="before"></slot>{{ isPlus ? '+' : '' }}{{ number(value) }}<slot name="after"></slot>
+</span>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import number from '@client/filters/number';
+
+export default defineComponent({
+ props: {
+ value: {
+ type: Number,
+ required: true
+ },
+ },
+
+ setup(props) {
+ const isPlus = computed(() => props.value > 0);
+ const isMinus = computed(() => props.value < 0);
+ const isZero = computed(() => props.value === 0);
+ return {
+ isPlus,
+ isMinus,
+ isZero,
+ number,
+ };
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.ceaaebcd {
+ &.isPlus {
+ color: var(--success);
+ }
+
+ &.isMinus {
+ color: var(--error);
+ }
+
+ &.isZero {
+ opacity: 0.5;
+ }
+}
+</style>
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index a1d89d2a2e..816a69e731 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -117,11 +117,28 @@ export default defineComponent({
type: String,
required: false
},
+ initialVisibility: {
+ type: String,
+ required: false
+ },
+ initialFiles: {
+ type: Array,
+ required: false
+ },
+ initialLocalOnly: {
+ type: Boolean,
+ required: false
+ },
+ visibleUsers: {
+ type: Array,
+ required: false,
+ default: () => []
+ },
initialNote: {
type: Object,
required: false
},
- instant: {
+ share: {
type: Boolean,
required: false,
default: false
@@ -150,8 +167,7 @@ export default defineComponent({
showPreview: false,
cw: null,
localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
- visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility,
- visibleUsers: [],
+ visibility: (this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility) as typeof noteVisibilities[number],
autocomplete: null,
draghover: false,
quoteId: null,
@@ -246,6 +262,18 @@ export default defineComponent({
this.text = this.initialText;
}
+ if (this.initialVisibility) {
+ this.visibility = this.initialVisibility;
+ }
+
+ if (this.initialFiles) {
+ this.files = this.initialFiles;
+ }
+
+ if (typeof this.initialLocalOnly === 'boolean') {
+ this.localOnly = this.initialLocalOnly;
+ }
+
if (this.mention) {
this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
this.text += ' ';
@@ -321,7 +349,7 @@ export default defineComponent({
this.$nextTick(() => {
// 書きかけの投稿を復元
- if (!this.instant && !this.mention && !this.specified) {
+ if (!this.share && !this.mention && !this.specified) {
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
if (draft) {
this.text = draft.data.text;
@@ -582,8 +610,6 @@ export default defineComponent({
},
saveDraft() {
- if (this.instant) return;
-
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
data[this.draftKey] = {
diff --git a/src/client/components/queue-chart.vue b/src/client/components/queue-chart.vue
new file mode 100644
index 0000000000..59c9723f89
--- /dev/null
+++ b/src/client/components/queue-chart.vue
@@ -0,0 +1,212 @@
+<template>
+<canvas ref="chartEl"></canvas>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
+import {
+ Chart,
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle,
+ Filler,
+} from 'chart.js';
+import number from '@client/filters/number';
+import * as os from '@client/os';
+import { defaultStore } from '@client/store';
+
+Chart.register(
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle,
+ Filler,
+);
+
+const alpha = (hex, a) => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+ const r = parseInt(result[1], 16);
+ const g = parseInt(result[2], 16);
+ const b = parseInt(result[3], 16);
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
+
+export default defineComponent({
+ props: {
+ domain: {
+ type: String,
+ required: true,
+ },
+ connection: {
+ required: true,
+ },
+ },
+
+ setup(props) {
+ const chartEl = ref<HTMLCanvasElement>(null);
+
+ const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+
+ // フォントカラー
+ Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+
+ onMounted(() => {
+ const chartInstance = new Chart(chartEl.value, {
+ type: 'line',
+ data: {
+ labels: [],
+ datasets: [{
+ label: 'Process',
+ pointRadius: 0,
+ tension: 0,
+ borderWidth: 2,
+ borderColor: '#00E396',
+ backgroundColor: alpha('#00E396', 0.1),
+ data: []
+ }, {
+ label: 'Active',
+ pointRadius: 0,
+ tension: 0,
+ borderWidth: 2,
+ borderColor: '#00BCD4',
+ backgroundColor: alpha('#00BCD4', 0.1),
+ data: []
+ }, {
+ label: 'Waiting',
+ pointRadius: 0,
+ tension: 0,
+ borderWidth: 2,
+ borderColor: '#FFB300',
+ backgroundColor: alpha('#FFB300', 0.1),
+ data: []
+ }, {
+ label: 'Delayed',
+ pointRadius: 0,
+ tension: 0,
+ borderWidth: 2,
+ borderColor: '#E53935',
+ borderDash: [5, 5],
+ fill: false,
+ data: []
+ }],
+ },
+ options: {
+ aspectRatio: 2.5,
+ layout: {
+ padding: {
+ left: 16,
+ right: 16,
+ top: 16,
+ bottom: 8,
+ },
+ },
+ scales: {
+ x: {
+ grid: {
+ display: false,
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
+ },
+ ticks: {
+ display: false,
+ },
+ },
+ y: {
+ grid: {
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
+ },
+ },
+ },
+ interaction: {
+ intersect: false,
+ },
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: {
+ boxWidth: 16,
+ },
+ },
+ tooltip: {
+ mode: 'index',
+ animation: {
+ duration: 0,
+ },
+ },
+ },
+ },
+ });
+
+ const onStats = (stats) => {
+ chartInstance.data.labels.push('');
+ chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
+ chartInstance.data.datasets[1].data.push(stats[props.domain].active);
+ chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
+ chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
+ if (chartInstance.data.datasets[0].data.length > 200) {
+ chartInstance.data.labels.shift();
+ chartInstance.data.datasets[0].data.shift();
+ chartInstance.data.datasets[1].data.shift();
+ chartInstance.data.datasets[2].data.shift();
+ chartInstance.data.datasets[3].data.shift();
+ }
+ chartInstance.update();
+ };
+
+ const onStatsLog = (statsLog) => {
+ for (const stats of [...statsLog].reverse()) {
+ chartInstance.data.labels.push('');
+ chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
+ chartInstance.data.datasets[1].data.push(stats[props.domain].active);
+ chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
+ chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
+ if (chartInstance.data.datasets[0].data.length > 200) {
+ chartInstance.data.labels.shift();
+ chartInstance.data.datasets[0].data.shift();
+ chartInstance.data.datasets[1].data.shift();
+ chartInstance.data.datasets[2].data.shift();
+ chartInstance.data.datasets[3].data.shift();
+ }
+ }
+ chartInstance.update();
+ };
+
+ props.connection.on('stats', onStats);
+ props.connection.on('statsLog', onStatsLog);
+
+ onUnmounted(() => {
+ props.connection.off('stats', onStats);
+ props.connection.off('statsLog', onStatsLog);
+ });
+ });
+
+ return {
+ chartEl,
+ }
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/src/client/components/tab.vue b/src/client/components/tab.vue
index ce86af8f95..c629727358 100644
--- a/src/client/components/tab.vue
+++ b/src/client/components/tab.vue
@@ -36,7 +36,7 @@ export default defineComponent({
> button {
flex: 1;
padding: 10px 8px;
- border-radius: 6px;
+ border-radius: var(--radius);
&:disabled {
opacity: 1 !important;
diff --git a/src/client/components/ui/container.vue b/src/client/components/ui/container.vue
index 1940099096..14673dfcd7 100644
--- a/src/client/components/ui/container.vue
+++ b/src/client/components/ui/container.vue
@@ -1,5 +1,5 @@
<template>
-<div class="ukygtjoj _block" :class="{ naked, hideHeader: !showHeader, scrollable, closed: !showBody }" v-size="{ max: [380] }">
+<div class="ukygtjoj _panel" :class="{ naked, thin, hideHeader: !showHeader, scrollable, closed: !showBody }" v-size="{ max: [380] }">
<header v-if="showHeader" ref="header">
<div class="title"><slot name="header"></slot></div>
<div class="sub">
@@ -36,6 +36,11 @@ export default defineComponent({
required: false,
default: true
},
+ thin: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
naked: {
type: Boolean,
required: false,
@@ -226,7 +231,7 @@ export default defineComponent({
}
}
- &.max-width_380px {
+ &.max-width_380px, &.thin {
> header {
> .title {
padding: 8px 10px;
diff --git a/src/client/components/ui/menu.vue b/src/client/components/ui/menu.vue
index da24d90170..aaef527f1a 100644
--- a/src/client/components/ui/menu.vue
+++ b/src/client/components/ui/menu.vue
@@ -1,5 +1,6 @@
<template>
<div class="rrevdjwt" :class="{ center: align === 'center' }"
+ :style="{ width: width ? width + 'px' : null }"
ref="items"
@contextmenu.self="e => e.preventDefault()"
v-hotkey="keymap"
@@ -59,6 +60,10 @@ export default defineComponent({
type: String,
requried: false
},
+ width: {
+ type: Number,
+ required: false
+ },
},
emits: ['close'],
data() {
diff --git a/src/client/components/ui/popup-menu.vue b/src/client/components/ui/popup-menu.vue
index 23f7c89f3b..3ff4c658b1 100644
--- a/src/client/components/ui/popup-menu.vue
+++ b/src/client/components/ui/popup-menu.vue
@@ -1,6 +1,6 @@
<template>
<MkPopup ref="popup" :src="src" @closed="$emit('closed')">
- <MkMenu :items="items" :align="align" @close="$refs.popup.close()" class="_popup _shadow"/>
+ <MkMenu :items="items" :align="align" :width="width" @close="$refs.popup.close()" class="_popup _shadow"/>
</MkPopup>
</template>
@@ -24,6 +24,10 @@ export default defineComponent({
type: String,
required: false
},
+ width: {
+ type: Number,
+ required: false
+ },
viaKeyboard: {
type: Boolean,
required: false
diff --git a/src/client/components/ui/super-menu.vue b/src/client/components/ui/super-menu.vue
index 35fc81550d..6ab94d744d 100644
--- a/src/client/components/ui/super-menu.vue
+++ b/src/client/components/ui/super-menu.vue
@@ -120,7 +120,7 @@ export default defineComponent({
> .items {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
grid-gap: 8px;
padding: 0 16px;
diff --git a/src/client/os.ts b/src/client/os.ts
index 7ae774dd92..743d2d131f 100644
--- a/src/client/os.ts
+++ b/src/client/os.ts
@@ -372,12 +372,17 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
});
}
-export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) {
+export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: {
+ align?: string;
+ width?: number;
+ viaKeyboard?: boolean;
+}) {
return new Promise((resolve, reject) => {
let dispose;
popup(import('@client/components/ui/popup-menu.vue'), {
items,
src,
+ width: options?.width,
align: options?.align,
viaKeyboard: options?.viaKeyboard
}, {
diff --git a/src/client/pages/instance/abuses.vue b/src/client/pages/admin/abuses.vue
index 29da8cc2c5..29da8cc2c5 100644
--- a/src/client/pages/instance/abuses.vue
+++ b/src/client/pages/admin/abuses.vue
diff --git a/src/client/pages/instance/ads.vue b/src/client/pages/admin/ads.vue
index e776f99a4c..e776f99a4c 100644
--- a/src/client/pages/instance/ads.vue
+++ b/src/client/pages/admin/ads.vue
diff --git a/src/client/pages/instance/announcements.vue b/src/client/pages/admin/announcements.vue
index 78637c095a..78637c095a 100644
--- a/src/client/pages/instance/announcements.vue
+++ b/src/client/pages/admin/announcements.vue
diff --git a/src/client/pages/instance/bot-protection.vue b/src/client/pages/admin/bot-protection.vue
index 731f114cc2..731f114cc2 100644
--- a/src/client/pages/instance/bot-protection.vue
+++ b/src/client/pages/admin/bot-protection.vue
diff --git a/src/client/pages/instance/database.vue b/src/client/pages/admin/database.vue
index ffbeed8b30..ffbeed8b30 100644
--- a/src/client/pages/instance/database.vue
+++ b/src/client/pages/admin/database.vue
diff --git a/src/client/pages/instance/email-settings.vue b/src/client/pages/admin/email-settings.vue
index ebf724fcdd..ebf724fcdd 100644
--- a/src/client/pages/instance/email-settings.vue
+++ b/src/client/pages/admin/email-settings.vue
diff --git a/src/client/pages/instance/emoji-edit-dialog.vue b/src/client/pages/admin/emoji-edit-dialog.vue
index 4854c69884..4854c69884 100644
--- a/src/client/pages/instance/emoji-edit-dialog.vue
+++ b/src/client/pages/admin/emoji-edit-dialog.vue
diff --git a/src/client/pages/instance/emojis.vue b/src/client/pages/admin/emojis.vue
index 4cd34b046d..4cd34b046d 100644
--- a/src/client/pages/instance/emojis.vue
+++ b/src/client/pages/admin/emojis.vue
diff --git a/src/client/pages/instance/file-dialog.vue b/src/client/pages/admin/file-dialog.vue
index 02d83e5022..02d83e5022 100644
--- a/src/client/pages/instance/file-dialog.vue
+++ b/src/client/pages/admin/file-dialog.vue
diff --git a/src/client/pages/instance/files-settings.vue b/src/client/pages/admin/files-settings.vue
index 8aefa9e90d..8aefa9e90d 100644
--- a/src/client/pages/instance/files-settings.vue
+++ b/src/client/pages/admin/files-settings.vue
diff --git a/src/client/pages/instance/files.vue b/src/client/pages/admin/files.vue
index 55189cfd84..55189cfd84 100644
--- a/src/client/pages/instance/files.vue
+++ b/src/client/pages/admin/files.vue
diff --git a/src/client/pages/instance/index.vue b/src/client/pages/admin/index.vue
index 7b07bf2dde..416e68206c 100644
--- a/src/client/pages/instance/index.vue
+++ b/src/client/pages/admin/index.vue
@@ -7,8 +7,8 @@
<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
</div>
- <MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/instance/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
- <MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/instance/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo>
+ <MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
+ <MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
</div>
@@ -93,47 +93,47 @@ export default defineComponent({
items: [{
icon: 'fas fa-tachometer-alt',
text: i18n.locale.dashboard,
- to: '/instance/overview',
+ to: '/admin/overview',
active: page.value === 'overview',
}, {
icon: 'fas fa-users',
text: i18n.locale.users,
- to: '/instance/users',
+ to: '/admin/users',
active: page.value === 'users',
}, {
icon: 'fas fa-laugh',
text: i18n.locale.customEmojis,
- to: '/instance/emojis',
+ to: '/admin/emojis',
active: page.value === 'emojis',
}, {
icon: 'fas fa-globe',
text: i18n.locale.federation,
- to: '/instance/federation',
+ to: '/admin/federation',
active: page.value === 'federation',
}, {
icon: 'fas fa-clipboard-list',
text: i18n.locale.jobQueue,
- to: '/instance/queue',
+ to: '/admin/queue',
active: page.value === 'queue',
}, {
icon: 'fas fa-cloud',
text: i18n.locale.files,
- to: '/instance/files',
+ to: '/admin/files',
active: page.value === 'files',
}, {
icon: 'fas fa-broadcast-tower',
text: i18n.locale.announcements,
- to: '/instance/announcements',
+ to: '/admin/announcements',
active: page.value === 'announcements',
}, {
icon: 'fas fa-audio-description',
text: i18n.locale.ads,
- to: '/instance/ads',
+ to: '/admin/ads',
active: page.value === 'ads',
}, {
icon: 'fas fa-exclamation-circle',
text: i18n.locale.abuseReports,
- to: '/instance/abuses',
+ to: '/admin/abuses',
active: page.value === 'abuses',
}],
}, {
@@ -141,57 +141,57 @@ export default defineComponent({
items: [{
icon: 'fas fa-cog',
text: i18n.locale.general,
- to: '/instance/settings',
+ to: '/admin/settings',
active: page.value === 'settings',
}, {
icon: 'fas fa-cloud',
text: i18n.locale.files,
- to: '/instance/files-settings',
+ to: '/admin/files-settings',
active: page.value === 'files-settings',
}, {
icon: 'fas fa-envelope',
text: i18n.locale.emailServer,
- to: '/instance/email-settings',
+ to: '/admin/email-settings',
active: page.value === 'email-settings',
}, {
icon: 'fas fa-cloud',
text: i18n.locale.objectStorage,
- to: '/instance/object-storage',
+ to: '/admin/object-storage',
active: page.value === 'object-storage',
}, {
icon: 'fas fa-lock',
text: i18n.locale.security,
- to: '/instance/security',
+ to: '/admin/security',
active: page.value === 'security',
}, {
icon: 'fas fa-bolt',
text: 'ServiceWorker',
- to: '/instance/service-worker',
+ to: '/admin/service-worker',
active: page.value === 'service-worker',
}, {
icon: 'fas fa-globe',
text: i18n.locale.relays,
- to: '/instance/relays',
+ to: '/admin/relays',
active: page.value === 'relays',
}, {
icon: 'fas fa-share-alt',
text: i18n.locale.integration,
- to: '/instance/integrations',
+ to: '/admin/integrations',
active: page.value === 'integrations',
}, {
icon: 'fas fa-ban',
text: i18n.locale.instanceBlocking,
- to: '/instance/instance-block',
+ to: '/admin/instance-block',
active: page.value === 'instance-block',
}, {
icon: 'fas fa-ghost',
text: i18n.locale.proxyAccount,
- to: '/instance/proxy-account',
+ to: '/admin/proxy-account',
active: page.value === 'proxy-account',
}, {
icon: 'fas fa-cogs',
text: i18n.locale.other,
- to: '/instance/other-settings',
+ to: '/admin/other-settings',
active: page.value === 'other-settings',
}],
}, {
@@ -199,13 +199,8 @@ export default defineComponent({
items: [{
icon: 'fas fa-database',
text: i18n.locale.database,
- to: '/instance/database',
+ to: '/admin/database',
active: page.value === 'database',
- }, {
- icon: 'fas fa-stream',
- text: i18n.locale.logs,
- to: '/instance/logs',
- active: page.value === 'logs',
}],
}]);
const component = computed(() => {
@@ -220,7 +215,6 @@ export default defineComponent({
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
case 'database': return defineAsyncComponent(() => import('./database.vue'));
- case 'logs': return defineAsyncComponent(() => import('./logs.vue'));
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue'));
diff --git a/src/client/pages/instance/instance-block.vue b/src/client/pages/admin/instance-block.vue
index 105cdb4941..105cdb4941 100644
--- a/src/client/pages/instance/instance-block.vue
+++ b/src/client/pages/admin/instance-block.vue
diff --git a/src/client/pages/instance/instance.vue b/src/client/pages/admin/instance.vue
index 6117f090de..5572fbbf75 100644
--- a/src/client/pages/instance/instance.vue
+++ b/src/client/pages/admin/instance.vue
@@ -78,17 +78,17 @@
<span class="label">{{ $ts.charts }}</span>
<div class="selects">
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
- <option value="requests">{{ $ts._instanceCharts.requests }}</option>
- <option value="users">{{ $ts._instanceCharts.users }}</option>
- <option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option>
- <option value="notes">{{ $ts._instanceCharts.notes }}</option>
- <option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
- <option value="ff">{{ $ts._instanceCharts.ff }}</option>
- <option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
- <option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
- <option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
- <option value="drive-files">{{ $ts._instanceCharts.files }}</option>
- <option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
+ <option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
+ <option value="instance-users">{{ $ts._instanceCharts.users }}</option>
+ <option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
+ <option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
+ <option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
+ <option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
+ <option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
+ <option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
+ <option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
+ <option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
+ <option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
</MkSelect>
<MkSelect v-model="chartSpan" style="margin: 0;">
<option value="hour">{{ $ts.perHour }}</option>
@@ -97,7 +97,7 @@
</div>
</div>
<div class="chart">
- <canvas :ref="setChart"></canvas>
+ <MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
</div>
</div>
<div class="operations section">
@@ -124,28 +124,17 @@
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
-import Chart from 'chart.js';
import XModalWindow from '@client/components/ui/modal-window.vue';
import MkUsersDialog from '@client/components/users-dialog.vue';
import MkSelect from '@client/components/form/select.vue';
import MkButton from '@client/components/ui/button.vue';
import MkSwitch from '@client/components/form/switch.vue';
import MkInfo from '@client/components/ui/info.vue';
+import MkChart from '@client/components/chart.vue';
import bytes from '@client/filters/bytes';
import number from '@client/filters/number';
import * as os from '@client/os';
-const chartLimit = 90;
-const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
-const negate = arr => arr.map(x => -x);
-const alpha = hex => {
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
- const r = parseInt(result[1], 16);
- const g = parseInt(result[2], 16);
- const b = parseInt(result[3], 16);
- return `rgba(${r}, ${g}, ${b}, 0.1)`;
-};
-
export default defineComponent({
components: {
XModalWindow,
@@ -153,6 +142,7 @@ export default defineComponent({
MkButton,
MkSwitch,
MkInfo,
+ MkChart,
},
props: {
@@ -167,42 +157,12 @@ export default defineComponent({
data() {
return {
isSuspended: this.instance.isSuspended,
- now: null,
- canvas: null,
- chart: null,
- chartInstance: null,
chartSrc: 'requests',
chartSpan: 'hour',
};
},
computed: {
- data(): any {
- if (this.chart == null) return null;
- switch (this.chartSrc) {
- case 'requests': return this.requestsChart();
- case 'users': return this.usersChart(false);
- case 'users-total': return this.usersChart(true);
- case 'notes': return this.notesChart(false);
- case 'notes-total': return this.notesChart(true);
- case 'ff': return this.ffChart(false);
- case 'ff-total': return this.ffChart(true);
- case 'drive-usage': return this.driveUsageChart(false);
- case 'drive-usage-total': return this.driveUsageChart(true);
- case 'drive-files': return this.driveFilesChart(false);
- case 'drive-files-total': return this.driveFilesChart(true);
- }
- },
-
- stats(): any[] {
- const stats =
- this.chartSpan == 'day' ? this.chart.perDay :
- this.chartSpan == 'hour' ? this.chart.perHour :
- null;
-
- return stats;
- },
-
meta() {
return this.$instance;
},
@@ -219,49 +179,15 @@ export default defineComponent({
isSuspended: this.isSuspended
});
},
-
- chartSrc() {
- this.renderChart();
- },
-
- chartSpan() {
- this.renderChart();
- }
- },
-
- async created() {
- this.now = new Date();
-
- const [perHour, perDay] = await Promise.all([
- os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
- os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
- ]);
-
- const chart = {
- perHour: perHour,
- perDay: perDay
- };
-
- this.chart = chart;
-
- this.renderChart();
},
methods: {
- setChart(el) {
- this.canvas = el;
- },
-
changeBlock(e) {
os.api('admin/update-meta', {
blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
});
},
- setSrc(src) {
- this.chartSrc = src;
- },
-
removeAllFollowing() {
os.apiWithDialog('admin/federation/remove-all-following', {
host: this.instance.host
@@ -274,170 +200,6 @@ export default defineComponent({
});
},
- renderChart() {
- if (this.chartInstance) {
- this.chartInstance.destroy();
- }
-
- Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
- this.chartInstance = markRaw(new Chart(this.canvas, {
- type: 'line',
- data: {
- labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
- datasets: this.data.series.map(x => ({
- label: x.name,
- data: x.data.slice().reverse(),
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: x.color,
- backgroundColor: alpha(x.color),
- }))
- },
- options: {
- aspectRatio: 2.5,
- layout: {
- padding: {
- left: 16,
- right: 16,
- top: 16,
- bottom: 0
- }
- },
- legend: {
- position: 'bottom',
- labels: {
- boxWidth: 16,
- }
- },
- scales: {
- xAxes: [{
- gridLines: {
- display: false
- },
- ticks: {
- display: false
- }
- }],
- yAxes: [{
- position: 'right',
- ticks: {
- display: false
- }
- }]
- },
- tooltips: {
- intersect: false,
- mode: 'index',
- }
- }
- }));
- },
-
- getDate(ago: number) {
- const y = this.now.getFullYear();
- const m = this.now.getMonth();
- const d = this.now.getDate();
- const h = this.now.getHours();
-
- return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
- },
-
- format(arr) {
- return arr;
- },
-
- requestsChart(): any {
- return {
- series: [{
- name: 'In',
- color: '#008FFB',
- data: this.format(this.stats.requests.received)
- }, {
- name: 'Out (succ)',
- color: '#00E396',
- data: this.format(this.stats.requests.succeeded)
- }, {
- name: 'Out (fail)',
- color: '#FEB019',
- data: this.format(this.stats.requests.failed)
- }]
- };
- },
-
- usersChart(total: boolean): any {
- return {
- series: [{
- name: 'Users',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.users.total
- : sum(this.stats.users.inc, negate(this.stats.users.dec))
- )
- }]
- };
- },
-
- notesChart(total: boolean): any {
- return {
- series: [{
- name: 'Notes',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.notes.total
- : sum(this.stats.notes.inc, negate(this.stats.notes.dec))
- )
- }]
- };
- },
-
- ffChart(total: boolean): any {
- return {
- series: [{
- name: 'Following',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.following.total
- : sum(this.stats.following.inc, negate(this.stats.following.dec))
- )
- }, {
- name: 'Followers',
- color: '#00E396',
- data: this.format(total
- ? this.stats.followers.total
- : sum(this.stats.followers.inc, negate(this.stats.followers.dec))
- )
- }]
- };
- },
-
- driveUsageChart(total: boolean): any {
- return {
- bytes: true,
- series: [{
- name: 'Drive usage',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.drive.totalUsage
- : sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage))
- )
- }]
- };
- },
-
- driveFilesChart(total: boolean): any {
- return {
- series: [{
- name: 'Drive files',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.drive.totalFiles
- : sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles))
- )
- }]
- };
- },
-
showFollowing() {
os.modal(MkUsersDialog, {
title: this.$ts.instanceFollowing,
diff --git a/src/client/pages/instance/integrations-discord.vue b/src/client/pages/admin/integrations-discord.vue
index c33b24f17f..c33b24f17f 100644
--- a/src/client/pages/instance/integrations-discord.vue
+++ b/src/client/pages/admin/integrations-discord.vue
diff --git a/src/client/pages/instance/integrations-github.vue b/src/client/pages/admin/integrations-github.vue
index cdf85868ff..cdf85868ff 100644
--- a/src/client/pages/instance/integrations-github.vue
+++ b/src/client/pages/admin/integrations-github.vue
diff --git a/src/client/pages/instance/integrations-twitter.vue b/src/client/pages/admin/integrations-twitter.vue
index ed7d097d0a..ed7d097d0a 100644
--- a/src/client/pages/instance/integrations-twitter.vue
+++ b/src/client/pages/admin/integrations-twitter.vue
diff --git a/src/client/pages/instance/integrations.vue b/src/client/pages/admin/integrations.vue
index 6964ae5704..bdc2cec4d0 100644
--- a/src/client/pages/instance/integrations.vue
+++ b/src/client/pages/admin/integrations.vue
@@ -1,15 +1,15 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormLink to="/instance/integrations/twitter">
+ <FormLink to="/admin/integrations/twitter">
<i class="fab fa-twitter"></i> Twitter
<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
</FormLink>
- <FormLink to="/instance/integrations/github">
+ <FormLink to="/admin/integrations/github">
<i class="fab fa-github"></i> GitHub
<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
</FormLink>
- <FormLink to="/instance/integrations/discord">
+ <FormLink to="/admin/integrations/discord">
<i class="fab fa-discord"></i> Discord
<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
</FormLink>
diff --git a/src/client/pages/instance/metrics.vue b/src/client/pages/admin/metrics.vue
index 1606063aee..da36f6c688 100644
--- a/src/client/pages/instance/metrics.vue
+++ b/src/client/pages/admin/metrics.vue
@@ -52,7 +52,21 @@
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
-import Chart from 'chart.js';
+import {
+ Chart,
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle
+} from 'chart.js';
import MkButton from '@client/components/ui/button.vue';
import MkSelect from '@client/components/form/select.vue';
import MkInput from '@client/components/form/input.vue';
@@ -64,6 +78,21 @@ import bytes from '@client/filters/bytes';
import number from '@client/filters/number';
import MkInstanceInfo from './instance.vue';
+Chart.register(
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle
+);
+
const alpha = (hex, a) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
const r = parseInt(result[1], 16);
@@ -116,7 +145,7 @@ export default defineComponent({
mounted() {
this.fetchJobs();
- Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+ Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
os.api('admin/server-info', {}).then(res => {
this.serverInfo = res;
@@ -157,7 +186,7 @@ export default defineComponent({
datasets: [{
label: 'CPU',
pointRadius: 0,
- lineTension: 0,
+ tension: 0,
borderWidth: 2,
borderColor: '#86b300',
backgroundColor: alpha('#86b300', 0.1),
@@ -165,7 +194,7 @@ export default defineComponent({
}, {
label: 'MEM (active)',
pointRadius: 0,
- lineTension: 0,
+ tension: 0,
borderWidth: 2,
borderColor: '#935dbf',
backgroundColor: alpha('#935dbf', 0.02),
@@ -173,7 +202,7 @@ export default defineComponent({
}, {
label: 'MEM (used)',
pointRadius: 0,
- lineTension: 0,
+ tension: 0,
borderWidth: 2,
borderColor: '#935dbf',
borderDash: [5, 5],
@@ -198,7 +227,7 @@ export default defineComponent({
}
},
scales: {
- xAxes: [{
+ x: {
gridLines: {
display: false,
color: this.gridColor,
@@ -207,8 +236,8 @@ export default defineComponent({
ticks: {
display: false,
}
- }],
- yAxes: [{
+ },
+ y: {
position: 'right',
gridLines: {
display: true,
@@ -219,7 +248,7 @@ export default defineComponent({
display: false,
max: 100
}
- }]
+ }
},
tooltips: {
intersect: false,
@@ -238,7 +267,7 @@ export default defineComponent({
datasets: [{
label: 'In',
pointRadius: 0,
- lineTension: 0,
+ tension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
@@ -246,7 +275,7 @@ export default defineComponent({
}, {
label: 'Out',
pointRadius: 0,
- lineTension: 0,
+ tension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
@@ -270,7 +299,7 @@ export default defineComponent({
}
},
scales: {
- xAxes: [{
+ x: {
gridLines: {
display: false,
color: this.gridColor,
@@ -279,8 +308,8 @@ export default defineComponent({
ticks: {
display: false
}
- }],
- yAxes: [{
+ },
+ y: {
position: 'right',
gridLines: {
display: true,
@@ -290,7 +319,7 @@ export default defineComponent({
ticks: {
display: false,
}
- }]
+ }
},
tooltips: {
intersect: false,
@@ -309,7 +338,7 @@ export default defineComponent({
datasets: [{
label: 'Read',
pointRadius: 0,
- lineTension: 0,
+ tension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
@@ -317,7 +346,7 @@ export default defineComponent({
}, {
label: 'Write',
pointRadius: 0,
- lineTension: 0,
+ tension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
@@ -341,7 +370,7 @@ export default defineComponent({
}
},
scales: {
- xAxes: [{
+ x: {
gridLines: {
display: false,
color: this.gridColor,
@@ -350,8 +379,8 @@ export default defineComponent({
ticks: {
display: false
}
- }],
- yAxes: [{
+ },
+ y: {
position: 'right',
gridLines: {
display: true,
@@ -361,7 +390,7 @@ export default defineComponent({
ticks: {
display: false,
}
- }]
+ }
},
tooltips: {
intersect: false,
@@ -371,18 +400,6 @@ export default defineComponent({
}));
},
- async showInstanceInfo(q) {
- let instance = q;
- if (typeof q === 'string') {
- instance = await os.api('federation/show-instance', {
- host: q
- });
- }
- os.popup(MkInstanceInfo, {
- instance: instance
- }, {}, 'closed');
- },
-
fetchJobs() {
os.api('admin/queue/deliver-delayed', {}).then(jobs => {
this.jobs = jobs;
diff --git a/src/client/pages/instance/object-storage.vue b/src/client/pages/admin/object-storage.vue
index 2d765270e6..2d765270e6 100644
--- a/src/client/pages/instance/object-storage.vue
+++ b/src/client/pages/admin/object-storage.vue
diff --git a/src/client/pages/instance/other-settings.vue b/src/client/pages/admin/other-settings.vue
index 4e55df41fb..4e55df41fb 100644
--- a/src/client/pages/instance/other-settings.vue
+++ b/src/client/pages/admin/other-settings.vue
diff --git a/src/client/pages/admin/overview.vue b/src/client/pages/admin/overview.vue
new file mode 100644
index 0000000000..bb9c10f106
--- /dev/null
+++ b/src/client/pages/admin/overview.vue
@@ -0,0 +1,242 @@
+<template>
+<div>
+ <MkHeader :info="header"/>
+
+ <div class="edbbcaef" v-size="{ max: [880] }">
+ <div v-if="stats" class="cfcdecdf" style="margin: var(--margin)">
+ <div class="number _panel">
+ <div class="label">Users</div>
+ <div class="value _monospace">
+ {{ number(stats.originalUsersCount) }}
+ <MkNumberDiff v-if="usersComparedToThePrevDay != null" class="diff" :value="usersComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff>
+ </div>
+ </div>
+ <div class="number _panel">
+ <div class="label">Notes</div>
+ <div class="value _monospace">
+ {{ number(stats.originalNotesCount) }}
+ <MkNumberDiff v-if="notesComparedToThePrevDay != null" class="diff" :value="notesComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff>
+ </div>
+ </div>
+ </div>
+
+ <MkContainer :foldable="true" class="charts">
+ <template #header><i class="fas fa-chart-bar"></i>{{ $ts.charts }}</template>
+ <div style="padding-top: 12px;">
+ <MkInstanceStats :chart-limit="500" :detailed="true"/>
+ </div>
+ </MkContainer>
+
+ <div class="queue">
+ <MkContainer :foldable="true" :thin="true" class="deliver">
+ <template #header>Queue: deliver</template>
+ <MkQueueChart :connection="queueStatsConnection" domain="deliver"/>
+ </MkContainer>
+ <MkContainer :foldable="true" :thin="true" class="inbox">
+ <template #header>Queue: inbox</template>
+ <MkQueueChart :connection="queueStatsConnection" domain="inbox"/>
+ </MkContainer>
+ </div>
+
+ <!--<XMetrics/>-->
+
+ <MkFolder style="margin: var(--margin)">
+ <template #header><i class="fas fa-info-circle"></i> {{ $ts.info }}</template>
+ <div class="cfcdecdf">
+ <div class="number _panel">
+ <div class="label">Misskey</div>
+ <div class="value _monospace">{{ version }}</div>
+ </div>
+ <div class="number _panel" v-if="serverInfo">
+ <div class="label">Node.js</div>
+ <div class="value _monospace">{{ serverInfo.node }}</div>
+ </div>
+ <div class="number _panel" v-if="serverInfo">
+ <div class="label">PostgreSQL</div>
+ <div class="value _monospace">{{ serverInfo.psql }}</div>
+ </div>
+ <div class="number _panel" v-if="serverInfo">
+ <div class="label">Redis</div>
+ <div class="value _monospace">{{ serverInfo.redis }}</div>
+ </div>
+ <div class="number _panel">
+ <div class="label">Vue</div>
+ <div class="value _monospace">{{ vueVersion }}</div>
+ </div>
+ </div>
+ </MkFolder>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, markRaw, version as vueVersion } from 'vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
+import MkInstanceStats from '@client/components/instance-stats.vue';
+import MkButton from '@client/components/ui/button.vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkNumberDiff from '@client/components/number-diff.vue';
+import MkContainer from '@client/components/ui/container.vue';
+import MkFolder from '@client/components/ui/folder.vue';
+import MkQueueChart from '@client/components/queue-chart.vue';
+import { version, url } from '@client/config';
+import bytes from '@client/filters/bytes';
+import number from '@client/filters/number';
+import MkInstanceInfo from './instance.vue';
+import XMetrics from './metrics.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+ components: {
+ MkNumberDiff,
+ FormKeyValueView,
+ MkInstanceStats,
+ MkContainer,
+ MkFolder,
+ MkQueueChart,
+ XMetrics,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: this.$ts.dashboard,
+ icon: 'fas fa-tachometer-alt',
+ bg: 'var(--bg)',
+ },
+ header: {
+ title: this.$ts.dashboard,
+ icon: 'fas fa-tachometer-alt',
+ },
+ version,
+ vueVersion,
+ url,
+ stats: null,
+ meta: null,
+ serverInfo: null,
+ usersComparedToThePrevDay: null,
+ notesComparedToThePrevDay: null,
+ fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
+ fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
+ queueStatsConnection: markRaw(os.stream.useChannel('queueStats')),
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+
+ os.api('meta', { detail: true }).then(meta => {
+ this.meta = meta;
+ });
+
+ os.api('stats', {}).then(stats => {
+ this.stats = stats;
+
+ os.api('charts/users', { limit: 2, span: 'day' }).then(chart => {
+ this.usersComparedToThePrevDay = this.stats.originalUsersCount - chart.local.total[1];
+ });
+
+ os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => {
+ this.notesComparedToThePrevDay = this.stats.originalNotesCount - chart.local.total[1];
+ });
+ });
+
+ os.api('admin/server-info', {}).then(serverInfo => {
+ this.serverInfo = serverInfo;
+ });
+
+ this.$nextTick(() => {
+ this.queueStatsConnection.send('requestLog', {
+ id: Math.random().toString().substr(2, 8),
+ length: 200
+ });
+ });
+ },
+
+ beforeUnmount() {
+ this.queueStatsConnection.dispose();
+ },
+
+ methods: {
+ async showInstanceInfo(q) {
+ let instance = q;
+ if (typeof q === 'string') {
+ instance = await os.api('federation/show-instance', {
+ host: q
+ });
+ }
+ os.popup(MkInstanceInfo, {
+ instance: instance
+ }, {}, 'closed');
+ },
+
+ bytes,
+
+ number,
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.edbbcaef {
+ .cfcdecdf {
+ display: grid;
+ grid-gap: 8px;
+ grid-template-columns: repeat(auto-fill,minmax(150px,1fr));
+
+ > .number {
+ padding: 12px 16px;
+
+ > .label {
+ opacity: 0.7;
+ font-size: 0.8em;
+ }
+
+ > .value {
+ font-weight: bold;
+ font-size: 1.2em;
+
+ > .diff {
+ font-size: 0.8em;
+ }
+ }
+ }
+ }
+
+ > .charts {
+ margin: var(--margin);
+ }
+
+ > .queue {
+ margin: var(--margin);
+ display: flex;
+
+ > .deliver,
+ > .inbox {
+ flex: 1;
+ width: 50%;
+
+ &:not(:first-child) {
+ margin-left: var(--margin);
+ }
+ }
+ }
+
+ &.max-width_800px {
+ > .queue {
+ display: block;
+
+ > .deliver,
+ > .inbox {
+ &:not(:first-child) {
+ margin-top: var(--margin);
+ margin-left: 0;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/pages/instance/proxy-account.vue b/src/client/pages/admin/proxy-account.vue
index b1ece19710..b1ece19710 100644
--- a/src/client/pages/instance/proxy-account.vue
+++ b/src/client/pages/admin/proxy-account.vue
diff --git a/src/client/pages/admin/queue.chart.vue b/src/client/pages/admin/queue.chart.vue
new file mode 100644
index 0000000000..084181a606
--- /dev/null
+++ b/src/client/pages/admin/queue.chart.vue
@@ -0,0 +1,102 @@
+<template>
+<div class="_debobigegoItem">
+ <div class="_debobigegoLabel"><slot name="title"></slot></div>
+ <div class="_debobigegoPanel pumxzjhg">
+ <div class="_table status">
+ <div class="_row">
+ <div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
+ <div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
+ <div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
+ <div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
+ </div>
+ </div>
+ <div class="">
+ <MkQueueChart :domain="domain" :connection="connection"/>
+ </div>
+ <div class="jobs">
+ <div v-if="jobs.length > 0">
+ <div v-for="job in jobs" :key="job[0]">
+ <span>{{ job[0] }}</span>
+ <span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
+ </div>
+ </div>
+ <span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw, onMounted, onUnmounted, ref } from 'vue';
+import number from '@client/filters/number';
+import MkQueueChart from '@client/components/queue-chart.vue';
+import * as os from '@client/os';
+
+export default defineComponent({
+ components: {
+ MkQueueChart
+ },
+
+ props: {
+ domain: {
+ type: String,
+ required: true,
+ },
+ connection: {
+ required: true,
+ },
+ },
+
+ setup(props) {
+ const activeSincePrevTick = ref(0);
+ const active = ref(0);
+ const waiting = ref(0);
+ const delayed = ref(0);
+ const jobs = ref([]);
+
+ onMounted(() => {
+ os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => {
+ jobs.value = jobs;
+ });
+
+ const onStats = (stats) => {
+ activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
+ active.value = stats[props.domain].active;
+ waiting.value = stats[props.domain].waiting;
+ delayed.value = stats[props.domain].delayed;
+ };
+
+ props.connection.on('stats', onStats);
+
+ onUnmounted(() => {
+ props.connection.off('stats', onStats);
+ });
+ });
+
+ return {
+ jobs,
+ activeSincePrevTick,
+ active,
+ waiting,
+ delayed,
+ number,
+ };
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.pumxzjhg {
+ > .status {
+ padding: 16px;
+ border-bottom: solid 0.5px var(--divider);
+ }
+
+ > .jobs {
+ padding: 16px;
+ border-top: solid 0.5px var(--divider);
+ max-height: 180px;
+ overflow: auto;
+ }
+}
+</style>
diff --git a/src/client/pages/instance/queue.vue b/src/client/pages/admin/queue.vue
index f88825eb19..f88825eb19 100644
--- a/src/client/pages/instance/queue.vue
+++ b/src/client/pages/admin/queue.vue
diff --git a/src/client/pages/instance/relays.vue b/src/client/pages/admin/relays.vue
index 7d7888eaa8..7d7888eaa8 100644
--- a/src/client/pages/instance/relays.vue
+++ b/src/client/pages/admin/relays.vue
diff --git a/src/client/pages/instance/security.vue b/src/client/pages/admin/security.vue
index a854b6dbd0..4365b6800c 100644
--- a/src/client/pages/instance/security.vue
+++ b/src/client/pages/admin/security.vue
@@ -1,7 +1,7 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormLink to="/instance/bot-protection">
+ <FormLink to="/admin/bot-protection">
<i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}
<template #suffix v-if="enableHcaptcha">hCaptcha</template>
<template #suffix v-else-if="enableRecaptcha">reCAPTCHA</template>
diff --git a/src/client/pages/instance/service-worker.vue b/src/client/pages/admin/service-worker.vue
index 430e02ad2e..430e02ad2e 100644
--- a/src/client/pages/instance/service-worker.vue
+++ b/src/client/pages/admin/service-worker.vue
diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/admin/settings.vue
index 7bd363e5f3..7bd363e5f3 100644
--- a/src/client/pages/instance/settings.vue
+++ b/src/client/pages/admin/settings.vue
diff --git a/src/client/pages/instance/users.vue b/src/client/pages/admin/users.vue
index f7f9306b70..f7f9306b70 100644
--- a/src/client/pages/instance/users.vue
+++ b/src/client/pages/admin/users.vue
diff --git a/src/client/pages/explore.vue b/src/client/pages/explore.vue
index 2ca0668611..6f304877b7 100644
--- a/src/client/pages/explore.vue
+++ b/src/client/pages/explore.vue
@@ -65,13 +65,18 @@
</div>
<div v-else-if="tab === 'search'">
<div class="_isolated">
- <MkInput v-model="query" :debounce="true" type="search">
+ <MkInput v-model="searchQuery" :debounce="true" type="search">
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.searchUser }}</template>
</MkInput>
+ <MkRadios v-model="searchOrigin">
+ <option value="local">{{ $ts.local }}</option>
+ <option value="remote">{{ $ts.remote }}</option>
+ <option value="both">{{ $ts.both }}</option>
+ </MkRadios>
</div>
- <XUserList v-if="query" class="_gap" :pagination="searchPagination" ref="search"/>
+ <XUserList v-if="searchQuery" class="_gap" :pagination="searchPagination" ref="search"/>
</div>
</div>
</MkSpacer>
@@ -83,6 +88,7 @@ import { computed, defineComponent } from 'vue';
import XUserList from '@client/components/user-list.vue';
import MkFolder from '@client/components/ui/folder.vue';
import MkInput from '@client/components/form/input.vue';
+import MkRadios from '@client/components/form/radios.vue';
import number from '@client/filters/number';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -92,6 +98,7 @@ export default defineComponent({
XUserList,
MkFolder,
MkInput,
+ MkRadios,
},
props: {
@@ -158,14 +165,16 @@ export default defineComponent({
searchPagination: {
endpoint: 'users/search',
limit: 10,
- params: computed(() => (this.query && this.query !== '') ? {
- query: this.query
+ params: computed(() => (this.searchQuery && this.searchQuery !== '') ? {
+ query: this.searchQuery,
+ origin: this.searchOrigin,
} : null)
},
tagsLocal: [],
tagsRemote: [],
stats: null,
- query: null,
+ searchQuery: null,
+ searchOrigin: 'combined',
num: number,
};
},
diff --git a/src/client/pages/instance-info.vue b/src/client/pages/instance-info.vue
index 4fbf104f0c..291ceb5dfd 100644
--- a/src/client/pages/instance-info.vue
+++ b/src/client/pages/instance-info.vue
@@ -65,17 +65,17 @@
<div class="_debobigegoPanel cmhjzshl">
<div class="selects">
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
- <option value="requests">{{ $ts._instanceCharts.requests }}</option>
- <option value="users">{{ $ts._instanceCharts.users }}</option>
- <option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option>
- <option value="notes">{{ $ts._instanceCharts.notes }}</option>
- <option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
- <option value="ff">{{ $ts._instanceCharts.ff }}</option>
- <option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
- <option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
- <option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
- <option value="drive-files">{{ $ts._instanceCharts.files }}</option>
- <option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
+ <option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
+ <option value="instance-users">{{ $ts._instanceCharts.users }}</option>
+ <option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
+ <option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
+ <option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
+ <option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
+ <option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
+ <option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
+ <option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
+ <option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
+ <option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
</MkSelect>
<MkSelect v-model="chartSpan" style="margin: 0;">
<option value="hour">{{ $ts.perHour }}</option>
@@ -83,7 +83,7 @@
</MkSelect>
</div>
<div class="chart">
- <canvas :ref="setChart"></canvas>
+ <MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
</div>
</div>
</div>
@@ -135,7 +135,7 @@
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
-import Chart from 'chart.js';
+import MkChart from '@client/components/chart.vue';
import FormObjectView from '@client/components/debobigego/object-view.vue';
import FormTextarea from '@client/components/debobigego/textarea.vue';
import FormLink from '@client/components/debobigego/link.vue';
@@ -149,18 +149,7 @@ import * as os from '@client/os';
import number from '@client/filters/number';
import bytes from '@client/filters/bytes';
import * as symbols from '@client/symbols';
-import MkInstanceInfo from '@client/pages/instance/instance.vue';
-
-const chartLimit = 90;
-const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
-const negate = arr => arr.map(x => -x);
-const alpha = hex => {
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
- const r = parseInt(result[1], 16);
- const g = parseInt(result[2], 16);
- const b = parseInt(result[3], 16);
- return `rgba(${r}, ${g}, ${b}, 0.1)`;
-};
+import MkInstanceInfo from '@client/pages/admin/instance.vue';
export default defineComponent({
components: {
@@ -173,6 +162,7 @@ export default defineComponent({
FormKeyValueView,
FormSuspense,
MkSelect,
+ MkChart,
},
props: {
@@ -199,53 +189,11 @@ export default defineComponent({
dnsPromiseFactory: () => os.api('federation/dns', {
host: this.host
}),
- now: null,
- canvas: null,
- chart: null,
- chartInstance: null,
- chartSrc: 'requests',
+ chartSrc: 'instance-requests',
chartSpan: 'hour',
}
},
- computed: {
- data(): any {
- if (this.chart == null) return null;
- switch (this.chartSrc) {
- case 'requests': return this.requestsChart();
- case 'users': return this.usersChart(false);
- case 'users-total': return this.usersChart(true);
- case 'notes': return this.notesChart(false);
- case 'notes-total': return this.notesChart(true);
- case 'ff': return this.ffChart(false);
- case 'ff-total': return this.ffChart(true);
- case 'drive-usage': return this.driveUsageChart(false);
- case 'drive-usage-total': return this.driveUsageChart(true);
- case 'drive-files': return this.driveFilesChart(false);
- case 'drive-files-total': return this.driveFilesChart(true);
- }
- },
-
- stats(): any[] {
- const stats =
- this.chartSpan == 'day' ? this.chart.perDay :
- this.chartSpan == 'hour' ? this.chart.perHour :
- null;
-
- return stats;
- },
- },
-
- watch: {
- chartSrc() {
- this.renderChart();
- },
-
- chartSpan() {
- this.renderChart();
- }
- },
-
mounted() {
this.fetch();
},
@@ -258,190 +206,6 @@ export default defineComponent({
this.instance = await os.api('federation/show-instance', {
host: this.host
});
-
- this.now = new Date();
-
- const [perHour, perDay] = await Promise.all([
- os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
- os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
- ]);
-
- const chart = {
- perHour: perHour,
- perDay: perDay
- };
-
- this.chart = chart;
-
- this.renderChart();
- },
-
- setChart(el) {
- this.canvas = el;
- },
-
- renderChart() {
- if (this.chartInstance) {
- this.chartInstance.destroy();
- }
-
- Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
- this.chartInstance = new Chart(this.canvas, {
- type: 'line',
- data: {
- labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
- datasets: this.data.series.map(x => ({
- label: x.name,
- data: x.data.slice().reverse(),
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: x.color,
- backgroundColor: alpha(x.color),
- }))
- },
- options: {
- aspectRatio: 2.5,
- layout: {
- padding: {
- left: 16,
- right: 16,
- top: 16,
- bottom: 16
- }
- },
- legend: {
- position: 'bottom',
- labels: {
- boxWidth: 16,
- }
- },
- scales: {
- xAxes: [{
- gridLines: {
- display: false
- },
- ticks: {
- display: false
- }
- }],
- yAxes: [{
- position: 'right',
- ticks: {
- display: false
- }
- }]
- },
- tooltips: {
- intersect: false,
- mode: 'index',
- }
- }
- });
- },
-
- getDate(ago: number) {
- const y = this.now.getFullYear();
- const m = this.now.getMonth();
- const d = this.now.getDate();
- const h = this.now.getHours();
-
- return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
- },
-
- format(arr) {
- return arr;
- },
-
- requestsChart(): any {
- return {
- series: [{
- name: 'In',
- color: '#008FFB',
- data: this.format(this.stats.requests.received)
- }, {
- name: 'Out (succ)',
- color: '#00E396',
- data: this.format(this.stats.requests.succeeded)
- }, {
- name: 'Out (fail)',
- color: '#FEB019',
- data: this.format(this.stats.requests.failed)
- }]
- };
- },
-
- usersChart(total: boolean): any {
- return {
- series: [{
- name: 'Users',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.users.total
- : sum(this.stats.users.inc, negate(this.stats.users.dec))
- )
- }]
- };
- },
-
- notesChart(total: boolean): any {
- return {
- series: [{
- name: 'Notes',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.notes.total
- : sum(this.stats.notes.inc, negate(this.stats.notes.dec))
- )
- }]
- };
- },
-
- ffChart(total: boolean): any {
- return {
- series: [{
- name: 'Following',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.following.total
- : sum(this.stats.following.inc, negate(this.stats.following.dec))
- )
- }, {
- name: 'Followers',
- color: '#00E396',
- data: this.format(total
- ? this.stats.followers.total
- : sum(this.stats.followers.inc, negate(this.stats.followers.dec))
- )
- }]
- };
- },
-
- driveUsageChart(total: boolean): any {
- return {
- bytes: true,
- series: [{
- name: 'Drive usage',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.drive.totalUsage
- : sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage))
- )
- }]
- };
- },
-
- driveFilesChart(total: boolean): any {
- return {
- series: [{
- name: 'Drive files',
- color: '#008FFB',
- data: this.format(total
- ? this.stats.drive.totalFiles
- : sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles))
- )
- }]
- };
},
info() {
diff --git a/src/client/pages/instance/logs.vue b/src/client/pages/instance/logs.vue
deleted file mode 100644
index 74aea0fc45..0000000000
--- a/src/client/pages/instance/logs.vue
+++ /dev/null
@@ -1,97 +0,0 @@
-<template>
-<div class="_section">
- <div class="_inputs">
- <MkInput v-model="logDomain" :debounce="true">
- <template #label>{{ $ts.domain }}</template>
- </MkInput>
- <MkSelect v-model="logLevel">
- <template #label>Level</template>
- <option value="all">All</option>
- <option value="info">Info</option>
- <option value="success">Success</option>
- <option value="warning">Warning</option>
- <option value="error">Error</option>
- <option value="debug">Debug</option>
- </MkSelect>
- </div>
-
- <div class="logs">
- <code v-for="log in logs" :key="log.id" :class="log.level">
- <details>
- <summary><MkTime :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary>
- <!--<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty>-->
- </details>
- </code>
- </div>
-
- <MkButton @click="deleteAllLogs()" primary><i class="fas fa-trash-alt"></i> {{ $ts.deleteAll }}</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
- components: {
- MkButton,
- MkInput,
- MkSelect,
- MkTextarea,
- },
-
- emits: ['info'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.serverLogs,
- icon: 'fas fa-stream'
- },
- logs: [],
- logLevel: 'all',
- logDomain: '',
- }
- },
-
- watch: {
- logLevel() {
- this.logs = [];
- this.fetchLogs();
- },
- logDomain() {
- this.logs = [];
- this.fetchLogs();
- }
- },
-
- created() {
- this.fetchLogs();
- },
-
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
- methods: {
- fetchLogs() {
- os.api('admin/logs', {
- level: this.logLevel === 'all' ? null : this.logLevel,
- domain: this.logDomain === '' ? null : this.logDomain,
- limit: 30
- }).then(logs => {
- this.logs = logs.reverse();
- });
- },
-
- deleteAllLogs() {
- os.apiWithDialog('admin/delete-logs');
- },
- }
-});
-</script>
diff --git a/src/client/pages/instance/overview.vue b/src/client/pages/instance/overview.vue
deleted file mode 100644
index c6db9d0c04..0000000000
--- a/src/client/pages/instance/overview.vue
+++ /dev/null
@@ -1,127 +0,0 @@
-<template>
-<FormBase>
- <FormSuspense :p="init">
- <FormSuspense :p="fetchStats" v-slot="{ result: stats }">
- <FormGroup>
- <FormKeyValueView>
- <template #key>Users</template>
- <template #value>{{ number(stats.originalUsersCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>Notes</template>
- <template #value>{{ number(stats.originalNotesCount) }}</template>
- </FormKeyValueView>
- </FormGroup>
- </FormSuspense>
-
- <div class="_debobigegoItem">
- <div class="_debobigegoPanel">
- <MkInstanceStats :chart-limit="300" :detailed="true"/>
- </div>
- </div>
-
- <XMetrics/>
-
- <FormSuspense :p="fetchServerInfo" v-slot="{ result: serverInfo }">
- <FormGroup>
- <FormKeyValueView>
- <template #key>Node.js</template>
- <template #value>{{ serverInfo.node }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>PostgreSQL</template>
- <template #value>{{ serverInfo.psql }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>Redis</template>
- <template #value>{{ serverInfo.redis }}</template>
- </FormKeyValueView>
- </FormGroup>
- </FormSuspense>
- </FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, markRaw } from 'vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import MkInstanceStats from '@client/components/instance-stats.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkContainer from '@client/components/ui/container.vue';
-import MkFolder from '@client/components/ui/folder.vue';
-import { version, url } from '@client/config';
-import bytes from '@client/filters/bytes';
-import number from '@client/filters/number';
-import MkInstanceInfo from './instance.vue';
-import XMetrics from './metrics.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
- components: {
- FormBase,
- FormSuspense,
- FormGroup,
- FormInfo,
- FormKeyValueView,
- MkInstanceStats,
- XMetrics,
- },
-
- emits: ['info'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.overview,
- icon: 'fas fa-tachometer-alt',
- bg: 'var(--bg)',
- },
- page: 'index',
- version,
- url,
- stats: null,
- meta: null,
- fetchStats: () => os.api('stats', {}),
- fetchServerInfo: () => os.api('admin/server-info', {}),
- fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
- fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
- }
- },
-
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
- methods: {
- async init() {
- this.meta = await os.api('meta', { detail: true });
- },
-
- async showInstanceInfo(q) {
- let instance = q;
- if (typeof q === 'string') {
- instance = await os.api('federation/show-instance', {
- host: q
- });
- }
- os.popup(MkInstanceInfo, {
- instance: instance
- }, {}, 'closed');
- },
-
- bytes,
-
- number,
- }
-});
-</script>
diff --git a/src/client/pages/instance/queue.chart.vue b/src/client/pages/instance/queue.chart.vue
deleted file mode 100644
index 887fe9a574..0000000000
--- a/src/client/pages/instance/queue.chart.vue
+++ /dev/null
@@ -1,218 +0,0 @@
-<template>
-<div class="_debobigegoItem">
- <div class="_debobigegoLabel"><slot name="title"></slot></div>
- <div class="_debobigegoPanel pumxzjhg">
- <div class="_table status">
- <div class="_row">
- <div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
- <div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
- <div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
- <div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
- </div>
- </div>
- <div class="">
- <canvas ref="chart"></canvas>
- </div>
- <div class="jobs">
- <div v-if="jobs.length > 0">
- <div v-for="job in jobs" :key="job[0]">
- <span>{{ job[0] }}</span>
- <span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
- </div>
- </div>
- <span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
- </div>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import Chart from 'chart.js';
-import number from '@client/filters/number';
-
-const alpha = (hex, a) => {
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
- const r = parseInt(result[1], 16);
- const g = parseInt(result[2], 16);
- const b = parseInt(result[3], 16);
- return `rgba(${r}, ${g}, ${b}, ${a})`;
-};
-import * as os from '@client/os';
-
-export default defineComponent({
- props: {
- domain: {
- required: true
- },
- connection: {
- required: true
- },
- },
-
- data() {
- return {
- chart: null,
- jobs: [],
- activeSincePrevTick: 0,
- active: 0,
- waiting: 0,
- delayed: 0,
- }
- },
-
- mounted() {
- this.fetchJobs();
-
- // TODO: var(--panel)の色が暗いか明るいかで判定する
- const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
-
- Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
- this.chart = markRaw(new Chart(this.$refs.chart, {
- type: 'line',
- data: {
- labels: [],
- datasets: [{
- label: 'Process',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#00E396',
- backgroundColor: alpha('#00E396', 0.1),
- data: []
- }, {
- label: 'Active',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#00BCD4',
- backgroundColor: alpha('#00BCD4', 0.1),
- data: []
- }, {
- label: 'Waiting',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#FFB300',
- backgroundColor: alpha('#FFB300', 0.1),
- data: []
- }, {
- label: 'Delayed',
- pointRadius: 0,
- lineTension: 0,
- borderWidth: 2,
- borderColor: '#E53935',
- borderDash: [5, 5],
- fill: false,
- data: []
- }]
- },
- options: {
- aspectRatio: 3,
- layout: {
- padding: {
- left: 16,
- right: 16,
- top: 16,
- bottom: 12
- }
- },
- legend: {
- position: 'bottom',
- labels: {
- boxWidth: 16,
- }
- },
- scales: {
- xAxes: [{
- gridLines: {
- display: false,
- color: gridColor,
- zeroLineColor: gridColor,
- },
- ticks: {
- display: false
- }
- }],
- yAxes: [{
- position: 'right',
- gridLines: {
- display: true,
- color: gridColor,
- zeroLineColor: gridColor,
- },
- ticks: {
- display: false,
- }
- }]
- },
- tooltips: {
- intersect: false,
- mode: 'index',
- }
- }
- }));
-
- this.connection.on('stats', this.onStats);
- this.connection.on('statsLog', this.onStatsLog);
- },
-
- beforeUnmount() {
- this.connection.off('stats', this.onStats);
- this.connection.off('statsLog', this.onStatsLog);
- },
-
- methods: {
- onStats(stats) {
- this.activeSincePrevTick = stats[this.domain].activeSincePrevTick;
- this.active = stats[this.domain].active;
- this.waiting = stats[this.domain].waiting;
- this.delayed = stats[this.domain].delayed;
- this.chart.data.labels.push('');
- this.chart.data.datasets[0].data.push(stats[this.domain].activeSincePrevTick);
- this.chart.data.datasets[1].data.push(stats[this.domain].active);
- this.chart.data.datasets[2].data.push(stats[this.domain].waiting);
- this.chart.data.datasets[3].data.push(stats[this.domain].delayed);
- if (this.chart.data.datasets[0].data.length > 200) {
- this.chart.data.labels.shift();
- this.chart.data.datasets[0].data.shift();
- this.chart.data.datasets[1].data.shift();
- this.chart.data.datasets[2].data.shift();
- this.chart.data.datasets[3].data.shift();
- }
- this.chart.update();
- },
-
- onStatsLog(statsLog) {
- for (const stats of [...statsLog].reverse()) {
- this.onStats(stats);
- }
- },
-
- fetchJobs() {
- os.api(this.domain === 'inbox' ? 'admin/queue/inbox-delayed' : this.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => {
- this.jobs = jobs;
- });
- },
-
- number
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.pumxzjhg {
- > .status {
- padding: 16px;
- border-bottom: solid 0.5px var(--divider);
- }
-
- > .jobs {
- padding: 16px;
- border-top: solid 0.5px var(--divider);
- max-height: 180px;
- overflow: auto;
- }
-}
-</style>
diff --git a/src/client/pages/settings/import-export.vue b/src/client/pages/settings/import-export.vue
index 2b49996dda..eeaa1f1602 100644
--- a/src/client/pages/settings/import-export.vue
+++ b/src/client/pages/settings/import-export.vue
@@ -16,11 +16,13 @@
</FormSection>
<FormSection>
<template #label>{{ $ts._exportOrImport.muteList }}</template>
- <MkButton :class="$style.button" inline @click="doExport('mute')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+ <MkButton :class="$style.button" inline @click="doExport('muting')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+ <MkButton :class="$style.button" inline @click="doImport('muting', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
</FormSection>
<FormSection>
<template #label>{{ $ts._exportOrImport.blockingList }}</template>
<MkButton :class="$style.button" inline @click="doExport('blocking')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+ <MkButton :class="$style.button" inline @click="doImport('blocking', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
</FormSection>
</div>
</template>
@@ -58,11 +60,11 @@ export default defineComponent({
methods: {
doExport(target) {
os.api(
- target == 'notes' ? 'i/export-notes' :
- target == 'following' ? 'i/export-following' :
- target == 'blocking' ? 'i/export-blocking' :
- target == 'user-lists' ? 'i/export-user-lists' :
- target == 'mute' ? 'i/export-mute' :
+ target === 'notes' ? 'i/export-notes' :
+ target === 'following' ? 'i/export-following' :
+ target === 'blocking' ? 'i/export-blocking' :
+ target === 'user-lists' ? 'i/export-user-lists' :
+ target === 'muting' ? 'i/export-mute' :
null, {})
.then(() => {
os.dialog({
@@ -81,8 +83,10 @@ export default defineComponent({
const file = await selectFile(e.currentTarget || e.target);
os.api(
- target == 'following' ? 'i/import-following' :
- target == 'user-lists' ? 'i/import-user-lists' :
+ target === 'following' ? 'i/import-following' :
+ target === 'user-lists' ? 'i/import-user-lists' :
+ target === 'muting' ? 'i/import-muting' :
+ target === 'blocking' ? 'i/import-blocking' :
null, {
fileId: file.id
}).then(() => {
diff --git a/src/client/pages/settings/privacy.vue b/src/client/pages/settings/privacy.vue
index 7756158578..2a60ae1f46 100644
--- a/src/client/pages/settings/privacy.vue
+++ b/src/client/pages/settings/privacy.vue
@@ -5,6 +5,10 @@
<FormSwitch v-model="autoAcceptFollowed" :disabled="!isLocked" @update:modelValue="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch>
<template #caption>{{ $ts.lockedAccountInfo }}</template>
</FormGroup>
+ <FormSwitch v-model="publicReactions" @update:modelValue="save()">
+ {{ $ts.makeReactionsPublic }}
+ <template #desc>{{ $ts.makeReactionsPublicDescription }}</template>
+ </FormSwitch>
<FormSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
{{ $ts.hideOnlineStatus }}
<template #desc>{{ $ts.hideOnlineStatusDescription }}</template>
@@ -64,6 +68,7 @@ export default defineComponent({
noCrawle: false,
isExplorable: false,
hideOnlineStatus: false,
+ publicReactions: false,
}
},
@@ -80,6 +85,7 @@ export default defineComponent({
this.noCrawle = this.$i.noCrawle;
this.isExplorable = this.$i.isExplorable;
this.hideOnlineStatus = this.$i.hideOnlineStatus;
+ this.publicReactions = this.$i.publicReactions;
},
mounted() {
@@ -94,6 +100,7 @@ export default defineComponent({
noCrawle: !!this.noCrawle,
isExplorable: !!this.isExplorable,
hideOnlineStatus: !!this.hideOnlineStatus,
+ publicReactions: !!this.publicReactions,
});
}
}
diff --git a/src/client/pages/settings/theme.manage.vue b/src/client/pages/settings/theme.manage.vue
index da21a47a50..1a11a664f0 100644
--- a/src/client/pages/settings/theme.manage.vue
+++ b/src/client/pages/settings/theme.manage.vue
@@ -10,13 +10,13 @@
</optgroup>
</FormSelect>
<template v-if="selectedTheme">
- <FormInput readonly :value="selectedTheme.author">
+ <FormInput readonly :modelValue="selectedTheme.author">
<span>{{ $ts.author }}</span>
</FormInput>
- <FormTextarea readonly :value="selectedTheme.desc" v-if="selectedTheme.desc">
+ <FormTextarea readonly :modelValue="selectedTheme.desc" v-if="selectedTheme.desc">
<span>{{ $ts._theme.description }}</span>
</FormTextarea>
- <FormTextarea readonly tall :value="selectedThemeCode">
+ <FormTextarea readonly tall :modelValue="selectedThemeCode">
<span>{{ $ts._theme.code }}</span>
<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $ts.copy }}</button></template>
</FormTextarea>
@@ -28,12 +28,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
import * as JSON5 from 'json5';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormRadios from '@client/components/form/radios.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormSelect from '@client/components/debobigego/select.vue';
+import FormRadios from '@client/components/debobigego/radios.vue';
import FormBase from '@client/components/debobigego/base.vue';
import FormGroup from '@client/components/debobigego/group.vue';
-import FormInput from '@client/components/form/input.vue';
+import FormInput from '@client/components/debobigego/input.vue';
import FormButton from '@client/components/debobigego/button.vue';
import { Theme, builtinThemes } from '@client/scripts/theme';
import copyToClipboard from '@client/scripts/copy-to-clipboard';
diff --git a/src/client/pages/share.vue b/src/client/pages/share.vue
index 67e598fa8f..70a9661dd0 100644
--- a/src/client/pages/share.vue
+++ b/src/client/pages/share.vue
@@ -1,22 +1,38 @@
<template>
<div class="">
<section class="_section">
- <div class="_title" v-if="title">{{ title }}</div>
<div class="_content">
- <XPostForm v-if="!posted" fixed :instant="true" :initial-text="initialText" @posted="posted = true" class="_panel"/>
- <MkButton v-else primary @click="close()">{{ $ts.close }}</MkButton>
+ <XPostForm
+ v-if="state === 'writing'"
+ fixed
+ :share="true"
+ :initial-text="initialText"
+ :initial-visibility="visibility"
+ :initial-files="files"
+ :initial-local-only="localOnly"
+ :reply="reply"
+ :renote="renote"
+ :visible-users="visibleUsers"
+ @posted="state = 'posted'"
+ class="_panel"
+ />
+ <MkButton v-else-if="state === 'posted'" primary @click="close()" class="close">{{ $ts.close }}</MkButton>
</div>
- <div class="_footer" v-if="url">{{ url }}</div>
</section>
</div>
</template>
<script lang="ts">
+// SPECIFICATION: /src/docs/ja-JP/advanced/share-page.md
+
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
import XPostForm from '@client/components/post-form.vue';
import * as os from '@client/os';
+import { noteVisibilities } from '@/types';
+import { parseAcct } from '@/misc/acct';
import * as symbols from '@client/symbols';
+import * as Misskey from 'misskey-js';
export default defineComponent({
components: {
@@ -30,35 +46,139 @@ export default defineComponent({
title: this.$ts.share,
icon: 'fas fa-share-alt'
},
- title: null,
- text: null,
- url: null,
- initialText: null,
- posted: false,
+ state: 'fetching' as 'fetching' | 'writing' | 'posted',
+ title: null as string | null,
+ initialText: null as string | null,
+ reply: null as Misskey.entities.Note | null,
+ renote: null as Misskey.entities.Note | null,
+ visibility: null as string | null,
+ localOnly: null as boolean | null,
+ files: [] as Misskey.entities.DriveFile[],
+ visibleUsers: [] as Misskey.entities.User[],
}
},
- created() {
+ async created() {
const urlParams = new URLSearchParams(window.location.search);
+
this.title = urlParams.get('title');
- this.text = urlParams.get('text');
- this.url = urlParams.get('url');
-
- let text = '';
- if (this.title) text += `【${this.title}】\n`;
- if (this.text) text += `${this.text}\n`;
- if (this.url) text += `${this.url}`;
- this.initialText = text.trim();
+ const text = urlParams.get('text');
+ const url = urlParams.get('url');
+
+ let noteText = '';
+ if (this.title) noteText += `[ ${this.title} ]\n`;
+ // Googleニュース対策
+ if (text?.startsWith(`${this.title}.\n`)) noteText += text.replace(`${this.title}.\n`, '');
+ else if (text && this.title !== text) noteText += `${text}\n`;
+ if (url) noteText += `${url}`;
+ this.initialText = noteText.trim();
+
+ const visibility = urlParams.get('visibility');
+ if (noteVisibilities.includes(visibility)) {
+ this.visibility = visibility;
+ }
+
+ if (this.visibility === 'specified') {
+ const visibleUserIds = urlParams.get('visibleUserIds');
+ const visibleAccts = urlParams.get('visibleAccts');
+ await Promise.all(
+ [
+ ...(visibleUserIds ? visibleUserIds.split(',').map(userId => ({ userId })) : []),
+ ...(visibleAccts ? visibleAccts.split(',').map(parseAcct) : [])
+ ]
+ // TypeScriptの指示通りに変換する
+ .map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q)
+ .map(q => os.api('users/show', q)
+ .then(user => {
+ this.visibleUsers.push(user);
+ }, () => {
+ console.error(`Invalid user query: ${JSON.stringify(q)}`);
+ })
+ )
+ );
+ }
+
+ const localOnly = urlParams.get('localOnly');
+ if (localOnly === '0') this.localOnly = false;
+ else if (localOnly === '1') this.localOnly = true;
+
+ try {
+ //#region Reply
+ const replyId = urlParams.get('replyId');
+ const replyUri = urlParams.get('replyUri');
+ if (replyId) {
+ this.reply = await os.api('notes/show', {
+ noteId: replyId
+ });
+ } else if (replyUri) {
+ const obj = await os.api('ap/show', {
+ uri: replyUri
+ });
+ if (obj.type === 'Note') {
+ this.reply = obj.object;
+ }
+ }
+ //#endregion
+
+ //#region Renote
+ const renoteId = urlParams.get('renoteId');
+ const renoteUri = urlParams.get('renoteUri');
+ if (renoteId) {
+ this.renote = await os.api('notes/show', {
+ noteId: renoteId
+ });
+ } else if (renoteUri) {
+ const obj = await os.api('ap/show', {
+ uri: renoteUri
+ });
+ if (obj.type === 'Note') {
+ this.renote = obj.object;
+ }
+ }
+ //#endregion
+
+ //#region Drive files
+ const fileIds = urlParams.get('fileIds');
+ if (fileIds) {
+ await Promise.all(
+ fileIds.split(',')
+ .map(fileId => os.api('drive/files/show', { fileId })
+ .then(file => {
+ this.files.push(file);
+ }, () => {
+ console.error(`Failed to fetch a file ${fileId}`);
+ })
+ )
+ );
+ }
+ //#endregion
+ } catch (e) {
+ os.dialog({
+ type: 'error',
+ title: e.message,
+ text: e.name
+ });
+ }
+
+ this.state = 'writing';
},
methods: {
close() {
- window.close()
+ window.close();
+
+ // 閉じなければ100ms後タイムラインに
+ setTimeout(() => {
+ this.$router.push('/');
+ }, 100);
}
}
});
</script>
<style lang="scss" scoped>
+.close {
+ margin: 16px auto;
+}
</style>
diff --git a/src/client/pages/user/index.timeline.vue b/src/client/pages/user/index.timeline.vue
index c3444f26f6..9057d90396 100644
--- a/src/client/pages/user/index.timeline.vue
+++ b/src/client/pages/user/index.timeline.vue
@@ -65,4 +65,11 @@ export default defineComponent({
background: var(--bg);
}
}
+
+._fitSide_ .yrzkoczt {
+ > .tab {
+ padding-left: var(--margin);
+ padding-right: var(--margin);
+ }
+}
</style>
diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue
index 0ddf73d572..f74bf49883 100644
--- a/src/client/pages/user/index.vue
+++ b/src/client/pages/user/index.vue
@@ -181,6 +181,7 @@
</template>
<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/>
<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/>
+ <XReactions v-else-if="page === 'reactions'" :user="user" class="_gap"/>
<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/>
@@ -223,6 +224,7 @@ export default defineComponent({
MkTab,
MkInfo,
XFollowList: defineAsyncComponent(() => import('./follow-list.vue')),
+ XReactions: defineAsyncComponent(() => import('./reactions.vue')),
XClips: defineAsyncComponent(() => import('./clips.vue')),
XPages: defineAsyncComponent(() => import('./pages.vue')),
XGallery: defineAsyncComponent(() => import('./gallery.vue')),
@@ -268,7 +270,12 @@ export default defineComponent({
title: this.$ts.overview,
icon: 'fas fa-home',
onClick: () => { this.$router.push('/@' + getAcct(this.user)); },
- }, {
+ }, ...(this.$i && (this.$i.id === this.user.id)) || this.user.publicReactions ? [{
+ active: this.page === 'reactions',
+ title: this.$ts.reaction,
+ icon: 'fas fa-laugh',
+ onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/reactions'); },
+ }] : [], {
active: this.page === 'clips',
title: this.$ts.clips,
icon: 'fas fa-paperclip',
diff --git a/src/client/pages/user/reactions.vue b/src/client/pages/user/reactions.vue
new file mode 100644
index 0000000000..5ac7e01027
--- /dev/null
+++ b/src/client/pages/user/reactions.vue
@@ -0,0 +1,81 @@
+<template>
+<div>
+ <MkPagination :pagination="pagination" #default="{items}" ref="list">
+ <div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap afdcfbfb">
+ <div class="header">
+ <MkAvatar class="avatar" :user="user"/>
+ <MkReactionIcon class="reaction" :reaction="item.type" :custom-emojis="item.note.emojis" :no-style="true"/>
+ <MkTime :time="item.createdAt" class="createdAt"/>
+ </div>
+ <MkNote :note="item.note" @update:note="updated(note, $event)" :key="item.id"/>
+ </div>
+ </MkPagination>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkPagination from '@client/components/ui/pagination.vue';
+import MkNote from '@client/components/note.vue';
+import MkReactionIcon from '@client/components/reaction-icon.vue';
+
+export default defineComponent({
+ components: {
+ MkPagination,
+ MkNote,
+ MkReactionIcon,
+ },
+
+ props: {
+ user: {
+ type: Object,
+ required: true
+ },
+ },
+
+ data() {
+ return {
+ pagination: {
+ endpoint: 'users/reactions',
+ limit: 20,
+ params: {
+ userId: this.user.id,
+ }
+ },
+ };
+ },
+
+ watch: {
+ user() {
+ this.$refs.list.reload();
+ }
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.afdcfbfb {
+ > .header {
+ display: flex;
+ align-items: center;
+ padding: 8px 16px;
+ margin-bottom: 8px;
+ border-bottom: solid 2px var(--divider);
+
+ > .avatar {
+ width: 24px;
+ height: 24px;
+ margin-right: 8px;
+ }
+
+ > .reaction {
+ width: 32px;
+ height: 32px;
+ }
+
+ > .createdAt {
+ margin-left: auto;
+ }
+ }
+}
+</style>
diff --git a/src/client/router.ts b/src/client/router.ts
index 56dc948669..f588bb04fc 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -70,8 +70,8 @@ const defaultRoutes = [
{ path: '/my/antennas/:antennaId', component: page('my-antennas/edit'), props: true },
{ path: '/my/clips', component: page('my-clips/index') },
{ path: '/scratchpad', component: page('scratchpad') },
- { path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) },
- { path: '/instance', component: page('instance/index') },
+ { path: '/admin/:page(.*)?', component: page('admin/index'), props: route => ({ initialPage: route.params.page || null }) },
+ { path: '/admin', component: page('admin/index') },
{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
diff --git a/src/client/scripts/hpml/lib.ts b/src/client/scripts/hpml/lib.ts
index 150a04732f..2a1ac73a40 100644
--- a/src/client/scripts/hpml/lib.ts
+++ b/src/client/scripts/hpml/lib.ts
@@ -1,11 +1,11 @@
import * as tinycolor from 'tinycolor2';
-import Chart from 'chart.js';
import { Hpml } from './evaluator';
import { values, utils } from '@syuilo/aiscript';
import { Fn, HpmlScope } from '.';
import { Expr } from './expr';
import * as seedrandom from 'seedrandom';
+/* TODO: https://www.chartjs.org/docs/latest/configuration/canvas-background.html#color
// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
Chart.pluginService.register({
beforeDraw: (chart, easing) => {
@@ -18,6 +18,7 @@ Chart.pluginService.register({
}
}
});
+*/
export function initAiLib(hpml: Hpml) {
return {
@@ -49,11 +50,12 @@ export function initAiLib(hpml: Hpml) {
]));
}),
'MkPages:chart': values.FN_NATIVE(([id, opts]) => {
+ /* TODO
utils.assertString(id);
utils.assertObject(opts);
const canvas = hpml.canvases[id.value];
const color = getComputedStyle(document.documentElement).getPropertyValue('--accent');
- Chart.defaults.global.defaultFontColor = '#555';
+ Chart.defaults.color = '#555';
const chart = new Chart(canvas, {
type: opts.value.get('type').value,
data: {
@@ -122,6 +124,7 @@ export function initAiLib(hpml: Hpml) {
})
}
});
+ */
})
};
}
diff --git a/src/client/scripts/theme.ts b/src/client/scripts/theme.ts
index e79d54fa6d..8b63821293 100644
--- a/src/client/scripts/theme.ts
+++ b/src/client/scripts/theme.ts
@@ -20,12 +20,14 @@ export const builtinThemes = [
require('@client/themes/l-apricot.json5'),
require('@client/themes/l-rainy.json5'),
require('@client/themes/l-vivid.json5'),
+ require('@client/themes/l-sushi.json5'),
require('@client/themes/d-dark.json5'),
require('@client/themes/d-persimmon.json5'),
require('@client/themes/d-astro.json5'),
require('@client/themes/d-future.json5'),
require('@client/themes/d-botanical.json5'),
+ require('@client/themes/d-pumpkin.json5'),
require('@client/themes/d-black.json5'),
] as Theme[];
diff --git a/src/client/themes/d-astro.json5 b/src/client/themes/d-astro.json5
index 2350e3d46d..c6a927ec3a 100644
--- a/src/client/themes/d-astro.json5
+++ b/src/client/themes/d-astro.json5
@@ -1,7 +1,7 @@
{
id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea',
base: 'dark',
- name: 'Mi Astro',
+ name: 'Mi Astro Dark',
author: 'syuilo',
props: {
bg: '#232125',
diff --git a/src/client/themes/d-future.json5 b/src/client/themes/d-future.json5
index 1882609121..b6fa1ab0c1 100644
--- a/src/client/themes/d-future.json5
+++ b/src/client/themes/d-future.json5
@@ -1,7 +1,7 @@
{
id: '32a637ef-b47a-4775-bb7b-bacbb823f865',
- name: 'Mi Future',
+ name: 'Mi Future Dark',
author: 'syuilo',
base: 'dark',
diff --git a/src/client/themes/d-persimmon.json5 b/src/client/themes/d-persimmon.json5
index 11e9994f5e..e36265ff10 100644
--- a/src/client/themes/d-persimmon.json5
+++ b/src/client/themes/d-persimmon.json5
@@ -1,7 +1,7 @@
{
id: 'c503d768-7c70-4db2-a4e6-08264304bc8d',
- name: 'Mi Persimmon',
+ name: 'Mi Persimmon Dark',
author: 'syuilo',
base: 'dark',
diff --git a/src/client/themes/d-pumpkin.json5 b/src/client/themes/d-pumpkin.json5
new file mode 100644
index 0000000000..064ca4577b
--- /dev/null
+++ b/src/client/themes/d-pumpkin.json5
@@ -0,0 +1,88 @@
+{
+ id: '0b64fef3-02c7-20b5-dd87-b3f77e2b4301',
+
+ name: 'Mi Pumpkin Dark',
+ author: 'syuilo',
+
+ base: 'dark',
+
+ props: {
+ X2: ':darken<2<@panel',
+ X3: 'rgba(255, 255, 255, 0.05)',
+ X4: 'rgba(255, 255, 255, 0.1)',
+ X5: 'rgba(255, 255, 255, 0.05)',
+ X6: 'rgba(255, 255, 255, 0.15)',
+ X7: 'rgba(255, 255, 255, 0.05)',
+ X8: ':lighten<5<@accent',
+ X9: ':darken<5<@accent',
+ bg: 'rgb(37, 32, 47)',
+ fg: '#e0d5c0',
+ X10: ':alpha<0.4<@accent',
+ X11: 'rgba(0, 0, 0, 0.3)',
+ X12: 'rgba(255, 255, 255, 0.1)',
+ X13: 'rgba(255, 255, 255, 0.15)',
+ X14: ':alpha<0.5<@navBg',
+ X15: ':alpha<0<@panel',
+ X16: ':alpha<0.7<@panel',
+ X17: ':alpha<0.8<@bg',
+ cwBg: '#687390',
+ cwFg: '#393f4f',
+ link: 'rgb(172, 193, 68)',
+ warn: '#ecb637',
+ badge: '#31b1ce',
+ error: '#ec4137',
+ focus: ':alpha<0.3<@accent',
+ navBg: '@panel',
+ navFg: '@fg',
+ panel: ':lighten<3<@bg',
+ popup: ':lighten<3<@panel',
+ accent: 'rgb(242, 133, 36)',
+ header: ':alpha<0.7<@panel',
+ infoBg: '#253142',
+ infoFg: '#fff',
+ renote: 'rgb(110, 179, 72)',
+ shadow: 'rgba(0, 0, 0, 0.3)',
+ divider: 'rgba(255, 255, 255, 0.1)',
+ hashtag: 'rgb(188, 90, 255)',
+ mention: 'rgb(72, 179, 139)',
+ modalBg: 'rgba(0, 0, 0, 0.5)',
+ success: '#86b300',
+ buttonBg: 'rgba(255, 255, 255, 0.05)',
+ switchBg: 'rgba(255, 255, 255, 0.15)',
+ acrylicBg: ':alpha<0.5<@bg',
+ cwHoverBg: '#707b97',
+ indicator: '@accent',
+ mentionMe: '@accent',
+ messageBg: '@bg',
+ navActive: '@accent',
+ accentedBg: ':alpha<0.15<@accent',
+ fgOnAccent: '#000',
+ infoWarnBg: '#42321c',
+ infoWarnFg: '#ffbd3e',
+ navHoverFg: ':lighten<17<@fg',
+ dateLabelFg: '@fg',
+ inputBorder: 'rgba(255, 255, 255, 0.1)',
+ panelBorder: '" solid 1px var(--divider)',
+ accentDarken: ':darken<10<@accent',
+ acrylicPanel: ':alpha<0.5<@panel',
+ navIndicator: '@indicator',
+ accentLighten: ':lighten<10<@accent',
+ buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
+ driveFolderBg: ':alpha<0.3<@accent',
+ fgHighlighted: ':lighten<3<@fg',
+ fgTransparent: ':alpha<0.5<@fg',
+ panelHeaderBg: ':lighten<3<@panel',
+ panelHeaderFg: '@fg',
+ buttonGradateA: '@accent',
+ buttonGradateB: ':hue<20<@accent',
+ htmlThemeColor: '@bg',
+ panelHighlight: ':lighten<3<@panel',
+ listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
+ scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
+ inputBorderHover: 'rgba(255, 255, 255, 0.2)',
+ wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
+ fgTransparentWeak: ':alpha<0.75<@fg',
+ panelHeaderDivider: 'rgba(0, 0, 0, 0)',
+ scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
+ },
+}
diff --git a/src/client/themes/l-apricot.json5 b/src/client/themes/l-apricot.json5
index 74cb24d407..1ed5525575 100644
--- a/src/client/themes/l-apricot.json5
+++ b/src/client/themes/l-apricot.json5
@@ -1,7 +1,7 @@
{
id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b',
- name: 'Mi Apricot',
+ name: 'Mi Apricot Light',
author: 'syuilo',
base: 'light',
diff --git a/src/client/themes/l-rainy.json5 b/src/client/themes/l-rainy.json5
index 1edde1cabf..283dd74c6c 100644
--- a/src/client/themes/l-rainy.json5
+++ b/src/client/themes/l-rainy.json5
@@ -1,7 +1,7 @@
{
id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96',
- name: 'Mi Rainy',
+ name: 'Mi Rainy Light',
author: 'syuilo',
base: 'light',
diff --git a/src/client/themes/l-sushi.json5 b/src/client/themes/l-sushi.json5
new file mode 100644
index 0000000000..5846927d65
--- /dev/null
+++ b/src/client/themes/l-sushi.json5
@@ -0,0 +1,18 @@
+{
+ id: '213273e5-7d20-d5f0-6e36-1b6a4f67115c',
+
+ name: 'Mi Sushi Light',
+ author: 'syuilo',
+
+ base: 'light',
+
+ props: {
+ accent: '#e36749',
+ bg: '#f0eee9',
+ fg: '#5f5f5f',
+ renote: '@accent',
+ link: '@accent',
+ mention: '@accent',
+ hashtag: '#229e82',
+ },
+}
diff --git a/src/client/themes/l-vivid.json5 b/src/client/themes/l-vivid.json5
index 0f4abe0a45..b3c08f38ae 100644
--- a/src/client/themes/l-vivid.json5
+++ b/src/client/themes/l-vivid.json5
@@ -1,7 +1,7 @@
{
id: '6128c2a9-5c54-43fe-a47d-17942356470b',
- name: 'Mi Vivid',
+ name: 'Mi Vivid Light',
author: 'syuilo',
base: 'light',
diff --git a/src/client/ui/_common_/sidebar.vue b/src/client/ui/_common_/sidebar.vue
index d00327b096..ec2a17ab86 100644
--- a/src/client/ui/_common_/sidebar.vue
+++ b/src/client/ui/_common_/sidebar.vue
@@ -25,7 +25,7 @@
</component>
</template>
<div class="divider"></div>
- <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" v-click-anime>
+ <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" v-click-anime>
<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
</MkA>
<button class="item _button" @click="more" v-click-anime>
diff --git a/src/client/ui/chat/post-form.vue b/src/client/ui/chat/post-form.vue
index 0cacaf77e7..64b8d08cbc 100644
--- a/src/client/ui/chat/post-form.vue
+++ b/src/client/ui/chat/post-form.vue
@@ -100,7 +100,7 @@ export default defineComponent({
type: Object,
required: false
},
- instant: {
+ share: {
type: Boolean,
required: false,
default: false
@@ -277,7 +277,7 @@ export default defineComponent({
this.$nextTick(() => {
// 書きかけの投稿を復元
- if (!this.instant && !this.mention && !this.specified) {
+ if (!this.share && !this.mention && !this.specified) {
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
if (draft) {
this.text = draft.data.text;
@@ -507,8 +507,6 @@ export default defineComponent({
},
saveDraft() {
- if (this.instant) return;
-
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
data[this.draftKey] = {
diff --git a/src/client/ui/default.header.vue b/src/client/ui/default.header.vue
index 4f6363e82d..908a4719a4 100644
--- a/src/client/ui/default.header.vue
+++ b/src/client/ui/default.header.vue
@@ -13,7 +13,7 @@
</component>
</template>
<div class="divider"></div>
- <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime v-tooltip="$ts.instance">
+ <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime v-tooltip="$ts.instance">
<i class="fas fa-server fa-fw"></i>
</MkA>
<button class="item _button" @click="more" v-click-anime>
diff --git a/src/client/ui/default.sidebar.vue b/src/client/ui/default.sidebar.vue
index e36febb7fa..cce74a53cd 100644
--- a/src/client/ui/default.sidebar.vue
+++ b/src/client/ui/default.sidebar.vue
@@ -20,7 +20,7 @@
</component>
</template>
<div class="divider"></div>
- <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime>
+ <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime>
<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
</MkA>
<button class="item _button" @click="more" v-click-anime>
diff --git a/src/db/postgre.ts b/src/db/postgre.ts
index 0b635ea18e..4f4047b613 100644
--- a/src/db/postgre.ts
+++ b/src/db/postgre.ts
@@ -8,7 +8,6 @@ import { entities as charts } from '@/services/chart/entities';
import { dbLogger } from './logger';
import * as highlight from 'cli-highlight';
-import { Log } from '@/models/entities/log';
import { User } from '@/models/entities/user';
import { DriveFile } from '@/models/entities/drive-file';
import { DriveFolder } from '@/models/entities/drive-folder';
@@ -144,7 +143,6 @@ export const entities = [
PageLike,
GalleryPost,
GalleryLike,
- Log,
DriveFile,
DriveFolder,
Poll,
diff --git a/src/docs/ar-SA/advanced/share-page.md b/src/docs/ar-SA/advanced/share-page.md
new file mode 100644
index 0000000000..b5b4edbebb
--- /dev/null
+++ b/src/docs/ar-SA/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### الملفات
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/ar-SA/features/timeline.md b/src/docs/ar-SA/features/timeline.md
index 988040538e..b280e3dcdc 100644
--- a/src/docs/ar-SA/features/timeline.md
+++ b/src/docs/ar-SA/features/timeline.md
@@ -13,10 +13,10 @@
## الشامل
全てのローカルユーザーの「ホーム」指定されていない投稿と、サーバーに届いた全てのリモートユーザーの「ホーム」指定されていない投稿が流れます。GTLと略されます。
-## 比較
+## مقارنة
| ソース | | | الخيط الزمني | | |
| ------------ | ---------- | ------- | ------------ | --------- | ------ |
-| المستخدمون | 公開範囲 | الرئيسي | المحلي | الاجتماعي | الشامل |
+| المستخدمون | الظهور | الرئيسي | المحلي | الاجتماعي | الشامل |
| ローカル (フォロー) | 公開 | ✔ | ✔ | ✔ | ✔ |
| | الرئيسي | ✔ | | ✔ | |
| | المتابِعين | ✔ | ✔ | ✔ | ✔ |
diff --git a/src/docs/cs-CZ/advanced/share-page.md b/src/docs/cs-CZ/advanced/share-page.md
new file mode 100644
index 0000000000..e3d37c2d0c
--- /dev/null
+++ b/src/docs/cs-CZ/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### Soubor(ů)
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/da-DK/advanced/share-page.md b/src/docs/da-DK/advanced/share-page.md
new file mode 100644
index 0000000000..4fb7ded0b8
--- /dev/null
+++ b/src/docs/da-DK/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### ファイル
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/de-DE/advanced/share-page.md b/src/docs/de-DE/advanced/share-page.md
new file mode 100644
index 0000000000..e818e4646f
--- /dev/null
+++ b/src/docs/de-DE/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### Text
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Sichtbarkeit
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### Dateien
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/en-US/advanced/share-page.md b/src/docs/en-US/advanced/share-page.md
new file mode 100644
index 0000000000..9bec559cda
--- /dev/null
+++ b/src/docs/en-US/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### Text
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Visiblility
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### Files
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/en-US/general/glossary.md b/src/docs/en-US/general/glossary.md
index 139c3eab82..12156134a1 100644
--- a/src/docs/en-US/general/glossary.md
+++ b/src/docs/en-US/general/glossary.md
@@ -77,13 +77,13 @@ Content which may include text, images, surveys and others that has been posted
Users of Misskey.
## Moderator
-Users with the authority to to manage the community of a server by deactivating spam accounts, deleting inappropriate posts etc.
+Users with the authority to manage the community of a server by deactivating spam accounts, silencing users, deleting inappropriate posts, etc.
## Remote
Used in context of servers separate from your own.Also used as prefix in other words like "Remote user".The opposite of "Local".
## Federation
-The act of sharing information created one one's server with other servers.
+The act of sharing information created on one's server with other servers.
## Local
Used in context of your own server.Also used as prefix in other words like "Local users", "local timeline".The opposite of "Remote".
diff --git a/src/docs/eo-UY/advanced/share-page.md b/src/docs/eo-UY/advanced/share-page.md
new file mode 100644
index 0000000000..8c5224f59f
--- /dev/null
+++ b/src/docs/eo-UY/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Videbleco
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### Dosieroj
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/eo-UY/features/mfm.md b/src/docs/eo-UY/features/mfm.md
index 7257033d8d..3389cb499f 100644
--- a/src/docs/eo-UY/features/mfm.md
+++ b/src/docs/eo-UY/features/mfm.md
@@ -5,7 +5,7 @@ MFMは、Misskey Flavored Markdownの略で、Misskeyの様々な場所で使用
- Teksto de notoj
- CW注釈
- La nomo de uzantoj
-- La sinprezento de profiloj
+- Sinprezento en la profilo de la uzanto
## Informoj por programistoj
MFMのパーサー実装はライブラリとして公開されており、簡単にクライアントにMFMを組み込むことが可能です。
diff --git a/src/docs/eo-UY/features/mute-and-block.md b/src/docs/eo-UY/features/mute-and-block.md
index 9d01f2c2fa..d45a793e87 100644
--- a/src/docs/eo-UY/features/mute-and-block.md
+++ b/src/docs/eo-UY/features/mute-and-block.md
@@ -22,19 +22,19 @@
## Bloki
ユーザーをブロックすると、そのユーザーからあなたのコンテンツが見えないようになり、またあなたに対して以下のようなアクションをすることができなくなります。
-- フォローする
-- ユーザーリストに追加する
-- 返信する、Renoteする
-- リアクションする、アンケートに投票する
-- メッセージを送信する
-- など
+- Eksekvi
+- Aldoni al listo de uzantoj
+- Respondi aŭ plusendi
+- Reagi aŭ voĉi
+- Senti mesaĝon
+- k.t.p
また、
-- ブロックする際に既にそのユーザーからフォローされていた場合はフォローが解除されます。
-- ブロックする際に既にそのユーザーがあなたをユーザーリストに入れていた場合はそのリストからあなたが削除されます。
+- Se la uzanto jam sekvas vin kiam vi blokas, la uzanto ĉesos sekvi.
+- Se la uzanto jam aldonis vin al listo de uzantoj kiam vi blokas, oni forigos vin de la listo
-ユーザーをブロックするには、対象のユーザーのユーザーページのメニューを開き、「ブロック」ボタンを押します。
+Por bloki uzanton klaku la butonon "Bloki" el la menuo de uzula paĝo de specifa uzanto.
<div class="warn">⚠️ ブロックを行ったこと自体は相手に通知されませんが、フォローを行ったりなどの上記のアクションが行えなくなるので間接的にブロックされていることは分かります。</div>
diff --git a/src/docs/eo-UY/features/note.md b/src/docs/eo-UY/features/note.md
index 2bfe10951c..3f87c44e1e 100644
--- a/src/docs/eo-UY/features/note.md
+++ b/src/docs/eo-UY/features/note.md
@@ -14,7 +14,7 @@ Notoj estas centraj konceptoj en Misskey kaj enhavoj kiuj konsistas el teksto, b
## Plusendi noton
既にあるノートを引用、もしくはそのノートを新しいノートとして共有する行為、またそれによって作成されたノートをRenoteと呼びます。 自分がフォローしているユーザーの、気に入ったノートを自分のフォロワーに共有したい場合や、過去の自分のノートを再度共有したい場合に使います。 同じノートに対して無制限にRenoteを行うことができますが、あまり連続して使用すると迷惑になる場合もあるので、注意しましょう。
-<div class="warn">⚠️ Se oni sendas notojn nur al sekvantoj aŭ rekte, iliaj ne estas plusendeblaj.</div>
+<div class="warn">⚠️ Se vi havigas al via noto videblecon ke nur al sekvantoj aŭ ke rekte, iliaj ne estos plusendeblaj.</div>
Renoteを削除するには、Renoteの時刻表示の隣にある「...」を押し、「Renote解除」を選択します。
@@ -51,4 +51,4 @@ Viaj notoj estos senditaj nur al viaj sekvantoj.La noto aperos sur ĉiuj templin
ノートをピン留めすると、ユーザーページに常にそのノートを表示しておくことができます。 ノートのメニューを開き、「ピン留め」を選択してピン留めできます。 複数のノートをピン留めできます。
## Observi
-Vi povas ricevi tiuj sciigoj pri reagoj, respondoj, k.t.p al noto kiuj ne apartenas al vi estas ankaŭ ricevebla. Por komenci tion elektu la "Observi" el la menuon kuntekstan de la notoj respektivaj.
+Per la funkcio Observi vi povas ricevi novajn sciigojn pri reagoj, respondoj, k.t.p al tiu noto kiu ne apartenas al vi. Por observi noton elektu la "Observi" el la menuo kunteksta de la noto respektiva.
diff --git a/src/docs/eo-UY/general/faq.md b/src/docs/eo-UY/general/faq.md
index f7ede4c6cc..14e02e39ee 100644
--- a/src/docs/eo-UY/general/faq.md
+++ b/src/docs/eo-UY/general/faq.md
@@ -12,7 +12,7 @@ MisskeyはMastodonのAPIと互換性がないため、一部を除きMastodonク
## 他のサーバーのユーザーをフォローするときは?
メニューから検索を選び、ユーザー名をホスト込みで入力します。例: `@syuilo@misskey.io`
-## Renoteを削除するには?
+## "Kiel mi malfari plusendon de noto?"
Renoteの時刻表示の隣にある「...」を押し、「Renote解除」を選択します。 Renoteについては[こちら](../features/note)をご確認ください。
## URLのプレビューを表示させたくない
diff --git a/src/docs/eo-UY/general/troubleshooting.md b/src/docs/eo-UY/general/troubleshooting.md
index d7edc154e3..d7f132e633 100644
--- a/src/docs/eo-UY/general/troubleshooting.md
+++ b/src/docs/eo-UY/general/troubleshooting.md
@@ -30,8 +30,8 @@
## 通知やアンテナ等の点滅が消えない
点滅は、未読のコンテンツがあることを示しています。通常点滅が消えない場合は、コンテンツを遡ると未読なコンテンツが残っています。 すべて既読にしたと思われるのに、それでもなお点滅が続く場合(おそらく不具合と思われます)は設定から強制的にすべて既読扱いにすることができます。
-## Renoteができない
-フォロワー限定のノートはRenoteすることはできません。
+## Oni ne povas plusendi noton
+Notoj nur al sekvantoj ne estas plusendeblaj.
## UI上で特定の要素が表示されない
広告ブロッカーを使用しているとそのような不具合が発生することがあります。Misskeyではオフにしてご利用ください。
diff --git a/src/docs/es-ES/advanced/share-page.md b/src/docs/es-ES/advanced/share-page.md
new file mode 100644
index 0000000000..20df387679
--- /dev/null
+++ b/src/docs/es-ES/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### Texto
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Visibilidad
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### Archivos
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/fr-FR/advanced/aiscript.md b/src/docs/fr-FR/advanced/aiscript.md
index 604d17daa8..5af011f990 100644
--- a/src/docs/fr-FR/advanced/aiscript.md
+++ b/src/docs/fr-FR/advanced/aiscript.md
@@ -3,5 +3,5 @@ AiScriptは、Misskeyで使用できるスクリプト言語です。
<div class="info">ℹ️ AiScript実装はMisskeyとは別リポジトリで、<a href="https://github.com/syuilo/aiscript" target="_blank">オープンソースで公開されています。</a></div>
-## 使い方
+## Utilisation
AiScriptの構文や組み込み関数などのドキュメントは、[こちら](https://github.com/syuilo/aiscript/tree/master/docs)で公開されています。
diff --git a/src/docs/fr-FR/advanced/share-page.md b/src/docs/fr-FR/advanced/share-page.md
new file mode 100644
index 0000000000..056f8e1efb
--- /dev/null
+++ b/src/docs/fr-FR/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### Texte
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Visibilité
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### Fichiers
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/fr-FR/general/glossary.md b/src/docs/fr-FR/general/glossary.md
index d26d2e8ee6..863ca57baf 100644
--- a/src/docs/fr-FR/general/glossary.md
+++ b/src/docs/fr-FR/general/glossary.md
@@ -8,7 +8,7 @@ Nom du protocole (procédé technique) utilisé par Misskey pour pouvoir fonctio
Langage de programmation qui peut être utilisé sur Misskey. [Voir ici pour plus d'informations.](../advanced/aiscript)
## API
-(読み: えーぴーあい) Misskeyのサーバーが公開している、プログラムからMisskeyを扱うためのインターフェース。[Voir ici pour plus d'informations. ](../advanced/api)
+Interface pour permettre à un programme d'utiliser Misskey, disponible publiquement sur le serveur de Misskey. [Voir ici pour plus d'informations. ](../advanced/api)
## Bot
Anglicisme désignant un compte géré par un programme informatique (vous le trouverez parfois aussi sous le terme de « robot »).
@@ -59,13 +59,13 @@ Désigne les émojis mis à disposition par votre instance. Par opposition, les
todo
## Mettre en sourdine
-ノートをパブリックな公開範囲で投稿できなくされている状態。モデレーターの判断でユーザーごとに設定されます。[Voir ici pour plus d'informations. ](../features/silence)
+Désigne le fait de paramétrer un compte pour empêcher ses publications d'être partagées en mode « Public ». Peut être appliqué à des utilisateur·rice·s individuel·le·s à la discrétion des modérateur·rice·s. [Voir ici pour plus d'informations. ](../features/silence)
## File d’attente
-アクティビティ配送などを順番に行うためのシステム。
+Système permettant de distribuer les activités dans un ordre successif.
## Suspendre
-アカウントが使用不可に設定されている状態。
+Désigne le fait de paramétrer un compte pour le rendre inutilisable à son/sa propriétaire.
## Drive
Fonctionnalité vous permettant de gérer les fichiers que vous avez téléversés sur Misskey. [Voir ici pour plus d'informations. ](../features/drive)
@@ -77,13 +77,13 @@ Nom des publications sur Misskey. Leur contenu peut être du texte, mais aussi d
Désigne les utilisateur·rice·s de Misskey.
## Modérateur·rice·s
-スパムの凍結およびサイレンスや不適切な投稿の削除など、コミュニティ運営に関する権限を持つユーザー。
+Utilisateur·rice·s chargé·e·s de gérer la communauté d'une instance, ayant autorité pour désactiver les comptes de spam, mettre des utilisateur·rice·s en sourdine, supprimer des publications inappropriées, etc.
## Distant
-他サーバーのことを指します。リモートユーザーといったように接頭辞としても使われます。ローカルの逆です。
+Désigne les autres instances que celle où vous êtes inscrit·e. Peut aussi être utilisé pour qualifier d'autres mots, comme « utilisateur·rice·s distant·e·s ». C'est le contraire de « local ».
## Fédération
-サーバー上で作成された情報が他のサーバーに伝わること。
+Désigne le fait de partager les informations publiées sur un serveur avec les autres serveurs du réseau.
## Local
-自サーバーのことを指します。ローカルユーザー、ローカルタイムラインといったように接頭辞としても使われます。リモートの逆です。
+Désigne l'instance où vous êtes inscrit·e. Peut aussi être utilisé pour qualifier d'autres mots, comme « utilisateur·rice·s locaux·cales » ou « fil local ». C'est le contraire de « distant ».
diff --git a/src/docs/ht-HT/advanced/share-page.md b/src/docs/ht-HT/advanced/share-page.md
new file mode 100644
index 0000000000..4fb7ded0b8
--- /dev/null
+++ b/src/docs/ht-HT/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### ファイル
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/id-ID/advanced/share-page.md b/src/docs/id-ID/advanced/share-page.md
new file mode 100644
index 0000000000..b7072a2cc4
--- /dev/null
+++ b/src/docs/id-ID/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### Teks
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Visibilitas
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### Berkas
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/it-IT/advanced/share-page.md b/src/docs/it-IT/advanced/share-page.md
new file mode 100644
index 0000000000..bf6b2c8b48
--- /dev/null
+++ b/src/docs/it-IT/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### Testo
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Visibilità
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### Allegati
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/ja-JP/advanced/share-page.md b/src/docs/ja-JP/advanced/share-page.md
new file mode 100644
index 0000000000..75a9d14d29
--- /dev/null
+++ b/src/docs/ja-JP/advanced/share-page.md
@@ -0,0 +1,56 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。
+ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### ファイル
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/ja-KS/advanced/share-page.md b/src/docs/ja-KS/advanced/share-page.md
new file mode 100644
index 0000000000..4fb7ded0b8
--- /dev/null
+++ b/src/docs/ja-KS/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### ファイル
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/jbo-EN/advanced/share-page.md b/src/docs/jbo-EN/advanced/share-page.md
new file mode 100644
index 0000000000..4fb7ded0b8
--- /dev/null
+++ b/src/docs/jbo-EN/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### ファイル
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/kab-KAB/advanced/share-page.md b/src/docs/kab-KAB/advanced/share-page.md
new file mode 100644
index 0000000000..edcde489c8
--- /dev/null
+++ b/src/docs/kab-KAB/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### Ifuyla
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/kn-IN/advanced/share-page.md b/src/docs/kn-IN/advanced/share-page.md
new file mode 100644
index 0000000000..481b488d73
--- /dev/null
+++ b/src/docs/kn-IN/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### ಕಡತಗಳು
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/ko-KR/advanced/share-page.md b/src/docs/ko-KR/advanced/share-page.md
new file mode 100644
index 0000000000..2b2747f691
--- /dev/null
+++ b/src/docs/ko-KR/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 텍스트
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 공개 범위
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### 파일
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/nl-NL/advanced/share-page.md b/src/docs/nl-NL/advanced/share-page.md
new file mode 100644
index 0000000000..4fb7ded0b8
--- /dev/null
+++ b/src/docs/nl-NL/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### ファイル
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/no-NO/advanced/share-page.md b/src/docs/no-NO/advanced/share-page.md
new file mode 100644
index 0000000000..4fb7ded0b8
--- /dev/null
+++ b/src/docs/no-NO/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### ファイル
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/pl-PL/advanced/share-page.md b/src/docs/pl-PL/advanced/share-page.md
new file mode 100644
index 0000000000..d5ee9906a5
--- /dev/null
+++ b/src/docs/pl-PL/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### Tekst
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Widoczność
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### Pliki
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/pt-PT/advanced/share-page.md b/src/docs/pt-PT/advanced/share-page.md
new file mode 100644
index 0000000000..4fb7ded0b8
--- /dev/null
+++ b/src/docs/pt-PT/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### ファイル
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/ru-RU/advanced/share-page.md b/src/docs/ru-RU/advanced/share-page.md
new file mode 100644
index 0000000000..65ec957e99
--- /dev/null
+++ b/src/docs/ru-RU/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### Текст
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Видимость
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### Файлы
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/th-TH/advanced/share-page.md b/src/docs/th-TH/advanced/share-page.md
new file mode 100644
index 0000000000..4fb7ded0b8
--- /dev/null
+++ b/src/docs/th-TH/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### ファイル
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/ug-CN/advanced/share-page.md b/src/docs/ug-CN/advanced/share-page.md
new file mode 100644
index 0000000000..4fb7ded0b8
--- /dev/null
+++ b/src/docs/ug-CN/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文字
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 公開範囲
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### ファイル
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/uk-UA/advanced/share-page.md b/src/docs/uk-UA/advanced/share-page.md
new file mode 100644
index 0000000000..55c0e1803b
--- /dev/null
+++ b/src/docs/uk-UA/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### Текст
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Видимість
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### Файли
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/zh-CN/advanced/share-page.md b/src/docs/zh-CN/advanced/share-page.md
new file mode 100644
index 0000000000..56f6d5ec19
--- /dev/null
+++ b/src/docs/zh-CN/advanced/share-page.md
@@ -0,0 +1,54 @@
+# 分享页面
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## 查询文本列表
+### 文本
+
+<dl>
+<dt>title</dt>
+<dd>标题。[ … ]将被插入到文本开头。</dd>
+<dt>text</dt>
+<dd>正文。</dd>
+<dt>url</dt>
+<dd>URL。它将被插入到末尾。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 可见性
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### 文件
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/docs/zh-TW/advanced/share-page.md b/src/docs/zh-TW/advanced/share-page.md
new file mode 100644
index 0000000000..cb5d041611
--- /dev/null
+++ b/src/docs/zh-TW/advanced/share-page.md
@@ -0,0 +1,54 @@
+# シェアページ
+`/share`を開くと、共有用の投稿フォームを開くことができます。 ここではシェアページで利用できるクエリ文字列の一覧を示します。
+
+## クエリ文字列一覧
+### 文本
+
+<dl>
+<dt>title</dt>
+<dd>タイトルです。本文の先頭に[ … ]と挿入されます。</dd>
+<dt>text</dt>
+<dd>本文です。</dd>
+<dt>url</dt>
+<dd>URLです。末尾に挿入されます。</dd>
+</dl>
+
+### リプライ情報
+以下のいずれか
+
+<dl>
+<dt>replyId</dt>
+<dd>リプライ先のノートid</dd>
+<dt>replyUri</dt>
+<dd>リプライ先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### Renote情報
+以下のいずれか
+
+<dl>
+<dt>renoteId</dt>
+<dd>Renote先のノートid</dd>
+<dt>renoteUri</dt>
+<dd>Renote先のUrl(リモートのノートオブジェクトを指定)</dd>
+</dl>
+
+### 可見性
+※specifiedに相当する値はvisibility=specifiedとvisibleAccts/visibleUserIdsで指定する
+
+<dl>
+<dt>visibility</dt>
+<dd>公開範囲 ['public' | 'home' | 'followers' | 'specified']</dd>
+<dt>localOnly</dt>
+<dd>0(false) or 1(true)</dd>
+<dt>visibleUserIds</dt>
+<dd>specified時のダイレクト先のユーザーid カンマ区切りで</dd>
+<dt>visibleAccts</dt>
+<dd>specified時のダイレクト先のacct(@?username[@host]) カンマ区切りで</dd>
+</dl>
+
+### 檔案
+<dl>
+<dt>fileIds</dt>
+<dd>添付したいファイルのid(カンマ区切りで)</dd>
+</dl>
diff --git a/src/misc/acct.ts b/src/misc/acct.ts
index 16876c4429..5106b1a09e 100644
--- a/src/misc/acct.ts
+++ b/src/misc/acct.ts
@@ -1,13 +1,10 @@
-export type Acct = {
- username: string;
- host: string | null;
-};
+import * as Misskey from 'misskey-js';
-export const getAcct = (user: Acct) => {
+export const getAcct = (user: Misskey.Acct) => {
return user.host == null ? user.username : `${user.username}@${user.host}`;
};
-export const parseAcct = (acct: string): Acct => {
+export const parseAcct = (acct: string): Misskey.Acct => {
if (acct.startsWith('@')) acct = acct.substr(1);
const split = acct.split('@', 2);
return { username: split[0], host: split[1] || null };
diff --git a/src/models/entities/log.ts b/src/models/entities/log.ts
deleted file mode 100644
index 182a9fbed8..0000000000
--- a/src/models/entities/log.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
-import { id } from '../id';
-
-@Entity()
-export class Log {
- @PrimaryColumn(id())
- public id: string;
-
- @Index()
- @Column('timestamp with time zone', {
- comment: 'The created date of the Log.'
- })
- public createdAt: Date;
-
- @Index()
- @Column('varchar', {
- length: 64, array: true, default: '{}'
- })
- public domain: string[];
-
- @Index()
- @Column('enum', {
- enum: ['error', 'warning', 'info', 'success', 'debug']
- })
- public level: string;
-
- @Column('varchar', {
- length: 8
- })
- public worker: string;
-
- @Column('varchar', {
- length: 128
- })
- public machine: string;
-
- @Column('varchar', {
- length: 2048
- })
- public message: string;
-
- @Column('jsonb', {
- default: {}
- })
- public data: Record<string, any>;
-}
diff --git a/src/models/entities/user-profile.ts b/src/models/entities/user-profile.ts
index a2da07d76f..1f450f223d 100644
--- a/src/models/entities/user-profile.ts
+++ b/src/models/entities/user-profile.ts
@@ -75,6 +75,11 @@ export class UserProfile {
})
public emailNotificationTypes: string[];
+ @Column('boolean', {
+ default: false,
+ })
+ public publicReactions: boolean;
+
@Column('varchar', {
length: 128, nullable: true,
})
diff --git a/src/models/index.ts b/src/models/index.ts
index 059a3d7b87..4c6f19eaff 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -13,7 +13,6 @@ import { UserRepository } from './repositories/user';
import { NoteRepository } from './repositories/note';
import { DriveFileRepository } from './repositories/drive-file';
import { DriveFolderRepository } from './repositories/drive-folder';
-import { Log } from './entities/log';
import { AccessToken } from './entities/access-token';
import { UserNotePining } from './entities/user-note-pining';
import { SigninRepository } from './repositories/signin';
@@ -108,7 +107,6 @@ export const Signins = getCustomRepository(SigninRepository);
export const MessagingMessages = getCustomRepository(MessagingMessageRepository);
export const ReversiGames = getCustomRepository(ReversiGameRepository);
export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
-export const Logs = getRepository(Log);
export const Pages = getCustomRepository(PageRepository);
export const PageLikes = getCustomRepository(PageLikeRepository);
export const GalleryPosts = getCustomRepository(GalleryPostRepository);
diff --git a/src/models/repositories/note-reaction.ts b/src/models/repositories/note-reaction.ts
index ba74076f6c..5d86065526 100644
--- a/src/models/repositories/note-reaction.ts
+++ b/src/models/repositories/note-reaction.ts
@@ -1,6 +1,6 @@
import { EntityRepository, Repository } from 'typeorm';
import { NoteReaction } from '@/models/entities/note-reaction';
-import { Users } from '../index';
+import { Notes, Users } from '../index';
import { Packed } from '@/misc/schema';
import { convertLegacyReaction } from '@/misc/reaction-lib';
import { User } from '@/models/entities/user';
@@ -9,8 +9,15 @@ import { User } from '@/models/entities/user';
export class NoteReactionRepository extends Repository<NoteReaction> {
public async pack(
src: NoteReaction['id'] | NoteReaction,
- me?: { id: User['id'] } | null | undefined
+ me?: { id: User['id'] } | null | undefined,
+ options?: {
+ withNote: boolean;
+ },
): Promise<Packed<'NoteReaction'>> {
+ const opts = Object.assign({
+ withNote: false,
+ }, options);
+
const reaction = typeof src === 'object' ? src : await this.findOneOrFail(src);
return {
@@ -18,6 +25,9 @@ export class NoteReactionRepository extends Repository<NoteReaction> {
createdAt: reaction.createdAt.toISOString(),
user: await Users.pack(reaction.userId, me),
type: convertLegacyReaction(reaction.reaction),
+ ...(opts.withNote ? {
+ note: await Notes.pack(reaction.noteId, me),
+ } : {})
};
}
}
diff --git a/src/models/repositories/signin.ts b/src/models/repositories/signin.ts
index 9942d2d962..f375f9b5c0 100644
--- a/src/models/repositories/signin.ts
+++ b/src/models/repositories/signin.ts
@@ -4,7 +4,7 @@ import { Signin } from '@/models/entities/signin';
@EntityRepository(Signin)
export class SigninRepository extends Repository<Signin> {
public async pack(
- src: any,
+ src: Signin,
) {
return src;
}
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
index b6f27e32e2..2b77b613a8 100644
--- a/src/models/repositories/user.ts
+++ b/src/models/repositories/user.ts
@@ -231,6 +231,7 @@ export class UserRepository extends Repository<User> {
}),
pinnedPageId: profile!.pinnedPageId,
pinnedPage: profile!.pinnedPageId ? Pages.pack(profile!.pinnedPageId, me) : null,
+ publicReactions: profile!.publicReactions,
twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled
diff --git a/src/queue/index.ts b/src/queue/index.ts
index 1e1d5da5a2..37eb809604 100644
--- a/src/queue/index.ts
+++ b/src/queue/index.ts
@@ -10,7 +10,7 @@ import procesObjectStorage from './processors/object-storage/index';
import { queueLogger } from './logger';
import { DriveFile } from '@/models/entities/drive-file';
import { getJobInfo } from './get-job-info';
-import { dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues';
+import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues';
import { ThinUser } from './types';
import { IActivity } from '@/remote/activitypub/type';
@@ -22,11 +22,20 @@ function renderError(e: Error): any {
};
}
+const systemLogger = queueLogger.createSubLogger('system');
const deliverLogger = queueLogger.createSubLogger('deliver');
const inboxLogger = queueLogger.createSubLogger('inbox');
const dbLogger = queueLogger.createSubLogger('db');
const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
+systemQueue
+ .on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`))
+ .on('active', (job) => systemLogger.debug(`active id=${job.id}`))
+ .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
+ .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
+ .on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) }))
+ .on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`));
+
deliverQueue
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
@@ -163,6 +172,26 @@ export function createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']
});
}
+export function createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) {
+ return dbQueue.add('importMuting', {
+ user: user,
+ fileId: fileId
+ }, {
+ removeOnComplete: true,
+ removeOnFail: true
+ });
+}
+
+export function createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) {
+ return dbQueue.add('importBlocking', {
+ user: user,
+ fileId: fileId
+ }, {
+ removeOnComplete: true,
+ removeOnFail: true
+ });
+}
+
export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) {
return dbQueue.add('importUserLists', {
user: user,
@@ -200,12 +229,17 @@ export function createCleanRemoteFilesJob() {
}
export default function() {
- if (!envOption.onlyServer) {
- deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
- inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
- processDb(dbQueue);
- procesObjectStorage(objectStorageQueue);
- }
+ if (envOption.onlyServer) return;
+
+ deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
+ inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
+ processDb(dbQueue);
+ procesObjectStorage(objectStorageQueue);
+
+ systemQueue.add('resyncCharts', {
+ }, {
+ repeat: { cron: '0 0 * * *' }
+ });
}
export function destroy() {
diff --git a/src/queue/processors/db/import-blocking.ts b/src/queue/processors/db/import-blocking.ts
new file mode 100644
index 0000000000..9951da669d
--- /dev/null
+++ b/src/queue/processors/db/import-blocking.ts
@@ -0,0 +1,74 @@
+import * as Bull from 'bull';
+
+import { queueLogger } from '../../logger';
+import { parseAcct } from '@/misc/acct';
+import { resolveUser } from '@/remote/resolve-user';
+import { downloadTextFile } from '@/misc/download-text-file';
+import { isSelfHost, toPuny } from '@/misc/convert-host';
+import { Users, DriveFiles, Blockings } from '@/models/index';
+import { DbUserImportJobData } from '@/queue/types';
+import block from '@/services/blocking/create';
+
+const logger = queueLogger.createSubLogger('import-blocking');
+
+export async function importBlocking(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
+ logger.info(`Importing blocking of ${job.data.user.id} ...`);
+
+ const user = await Users.findOne(job.data.user.id);
+ if (user == null) {
+ done();
+ return;
+ }
+
+ const file = await DriveFiles.findOne({
+ id: job.data.fileId
+ });
+ if (file == null) {
+ done();
+ return;
+ }
+
+ const csv = await downloadTextFile(file.url);
+
+ let linenum = 0;
+
+ for (const line of csv.trim().split('\n')) {
+ linenum++;
+
+ try {
+ const acct = line.split(',')[0].trim();
+ const { username, host } = parseAcct(acct);
+
+ let target = isSelfHost(host!) ? await Users.findOne({
+ host: null,
+ usernameLower: username.toLowerCase()
+ }) : await Users.findOne({
+ host: toPuny(host!),
+ usernameLower: username.toLowerCase()
+ });
+
+ if (host == null && target == null) continue;
+
+ if (target == null) {
+ target = await resolveUser(username, host);
+ }
+
+ if (target == null) {
+ throw `cannot resolve user: @${username}@${host}`;
+ }
+
+ // skip myself
+ if (target.id === job.data.user.id) continue;
+
+ logger.info(`Block[${linenum}] ${target.id} ...`);
+
+ await block(user, target);
+ } catch (e) {
+ logger.warn(`Error in line:${linenum} ${e}`);
+ }
+ }
+
+ logger.succ('Imported');
+ done();
+}
+
diff --git a/src/queue/processors/db/import-muting.ts b/src/queue/processors/db/import-muting.ts
new file mode 100644
index 0000000000..798f03a627
--- /dev/null
+++ b/src/queue/processors/db/import-muting.ts
@@ -0,0 +1,83 @@
+import * as Bull from 'bull';
+
+import { queueLogger } from '../../logger';
+import { parseAcct } from '@/misc/acct';
+import { resolveUser } from '@/remote/resolve-user';
+import { downloadTextFile } from '@/misc/download-text-file';
+import { isSelfHost, toPuny } from '@/misc/convert-host';
+import { Users, DriveFiles, Mutings } from '@/models/index';
+import { DbUserImportJobData } from '@/queue/types';
+import { User } from '@/models/entities/user';
+import { genId } from '@/misc/gen-id';
+
+const logger = queueLogger.createSubLogger('import-muting');
+
+export async function importMuting(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
+ logger.info(`Importing muting of ${job.data.user.id} ...`);
+
+ const user = await Users.findOne(job.data.user.id);
+ if (user == null) {
+ done();
+ return;
+ }
+
+ const file = await DriveFiles.findOne({
+ id: job.data.fileId
+ });
+ if (file == null) {
+ done();
+ return;
+ }
+
+ const csv = await downloadTextFile(file.url);
+
+ let linenum = 0;
+
+ for (const line of csv.trim().split('\n')) {
+ linenum++;
+
+ try {
+ const acct = line.split(',')[0].trim();
+ const { username, host } = parseAcct(acct);
+
+ let target = isSelfHost(host!) ? await Users.findOne({
+ host: null,
+ usernameLower: username.toLowerCase()
+ }) : await Users.findOne({
+ host: toPuny(host!),
+ usernameLower: username.toLowerCase()
+ });
+
+ if (host == null && target == null) continue;
+
+ if (target == null) {
+ target = await resolveUser(username, host);
+ }
+
+ if (target == null) {
+ throw `cannot resolve user: @${username}@${host}`;
+ }
+
+ // skip myself
+ if (target.id === job.data.user.id) continue;
+
+ logger.info(`Mute[${linenum}] ${target.id} ...`);
+
+ await mute(user, target);
+ } catch (e) {
+ logger.warn(`Error in line:${linenum} ${e}`);
+ }
+ }
+
+ logger.succ('Imported');
+ done();
+}
+
+async function mute(user: User, target: User) {
+ await Mutings.insert({
+ id: genId(),
+ createdAt: new Date(),
+ muterId: user.id,
+ muteeId: target.id,
+ });
+}
diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts
index b051a28e0b..97087642b7 100644
--- a/src/queue/processors/db/index.ts
+++ b/src/queue/processors/db/index.ts
@@ -9,6 +9,8 @@ import { exportUserLists } from './export-user-lists';
import { importFollowing } from './import-following';
import { importUserLists } from './import-user-lists';
import { deleteAccount } from './delete-account';
+import { importMuting } from './import-muting';
+import { importBlocking } from './import-blocking';
const jobs = {
deleteDriveFiles,
@@ -18,6 +20,8 @@ const jobs = {
exportBlocking,
exportUserLists,
importFollowing,
+ importMuting,
+ importBlocking,
importUserLists,
deleteAccount,
} as Record<string, Bull.ProcessCallbackFunction<DbJobData> | Bull.ProcessPromiseFunction<DbJobData>>;
diff --git a/src/queue/processors/system/index.ts b/src/queue/processors/system/index.ts
new file mode 100644
index 0000000000..52b7868105
--- /dev/null
+++ b/src/queue/processors/system/index.ts
@@ -0,0 +1,12 @@
+import * as Bull from 'bull';
+import { resyncCharts } from './resync-charts';
+
+const jobs = {
+ resyncCharts,
+} as Record<string, Bull.ProcessCallbackFunction<{}> | Bull.ProcessPromiseFunction<{}>>;
+
+export default function(dbQueue: Bull.Queue<{}>) {
+ for (const [k, v] of Object.entries(jobs)) {
+ dbQueue.process(k, v);
+ }
+}
diff --git a/src/queue/processors/system/resync-charts.ts b/src/queue/processors/system/resync-charts.ts
new file mode 100644
index 0000000000..b36b024cfb
--- /dev/null
+++ b/src/queue/processors/system/resync-charts.ts
@@ -0,0 +1,21 @@
+import * as Bull from 'bull';
+
+import { queueLogger } from '../../logger';
+import { driveChart, notesChart, usersChart } from '@/services/chart/index';
+
+const logger = queueLogger.createSubLogger('resync-charts');
+
+export default async function resyncCharts(job: Bull.Job<{}>, done: any): Promise<void> {
+ logger.info(`Resync charts...`);
+
+ // TODO: ユーザーごとのチャートも更新する
+ // TODO: インスタンスごとのチャートも更新する
+ await Promise.all([
+ driveChart.resync(),
+ notesChart.resync(),
+ usersChart.resync(),
+ ]);
+
+ logger.succ(`All charts successfully resynced.`);
+ done();
+}
diff --git a/src/queue/queues.ts b/src/queue/queues.ts
index d8c09ef86e..a66a7ca451 100644
--- a/src/queue/queues.ts
+++ b/src/queue/queues.ts
@@ -2,6 +2,7 @@ import config from '@/config/index';
import { initialize as initializeQueue } from './initialize';
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData } from './types';
+export const systemQueue = initializeQueue<{}>('system');
export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.deliverJobPerSec || 128);
export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
export const dbQueue = initializeQueue<DbJobData>('db');
diff --git a/src/remote/activitypub/kernel/undo/like.ts b/src/remote/activitypub/kernel/undo/like.ts
index 7f821cada0..107d3053e3 100644
--- a/src/remote/activitypub/kernel/undo/like.ts
+++ b/src/remote/activitypub/kernel/undo/like.ts
@@ -12,6 +12,10 @@ export default async (actor: IRemoteUser, activity: ILike) => {
const note = await fetchNote(targetUri);
if (!note) return `skip: target note not found ${targetUri}`;
- await deleteReaction(actor, note);
+ await deleteReaction(actor, note).catch(e => {
+ if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') return;
+ throw e;
+ });
+
return `ok`;
};
diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts
index 1dce76d2a9..33f41b2770 100644
--- a/src/server/api/common/read-messaging-message.ts
+++ b/src/server/api/common/read-messaging-message.ts
@@ -77,7 +77,7 @@ export async function readGroupMessagingMessage(
id: In(messageIds)
});
- const reads = [];
+ const reads: MessagingMessage['id'][] = [];
for (const message of messages) {
if (message.userId === userId) continue;
diff --git a/src/server/api/endpoints/admin/logs.ts b/src/server/api/endpoints/admin/logs.ts
deleted file mode 100644
index 776403a62e..0000000000
--- a/src/server/api/endpoints/admin/logs.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { Logs } from '@/models/index';
-import { Brackets } from 'typeorm';
-
-export const meta = {
- tags: ['admin'],
-
- requireCredential: true as const,
- requireModerator: true,
-
- params: {
- limit: {
- validator: $.optional.num.range(1, 100),
- default: 30
- },
-
- level: {
- validator: $.optional.nullable.str,
- default: null
- },
-
- domain: {
- validator: $.optional.nullable.str,
- default: null
- }
- },
-
- res: {
- type: 'array' as const,
- optional: false as const, nullable: false as const,
- items: {
- type: 'object' as const,
- optional: false as const, nullable: false as const,
- properties: {
- id: {
- type: 'string' as const,
- optional: false as const, nullable: false as const,
- format: 'id',
- example: 'xxxxxxxxxx',
- },
- createdAt: {
- type: 'string' as const,
- optional: false as const, nullable: false as const,
- format: 'date-time',
- },
- domain: {
- type: 'array' as const,
- optional: false as const, nullable: false as const,
- items: {
- type: 'string' as const,
- optional: true as const, nullable: false as const
- }
- },
- level: {
- type: 'string' as const,
- optional: false as const, nullable: false as const
- },
- worker: {
- type: 'string' as const,
- optional: false as const, nullable: false as const
- },
- machine: {
- type: 'string' as const,
- optional: false as const, nullable: false as const,
- },
- message: {
- type: 'string' as const,
- optional: false as const, nullable: false as const,
- },
- data: {
- type: 'object' as const,
- optional: false as const, nullable: false as const
- }
- }
- }
- }
-};
-
-export default define(meta, async (ps) => {
- const query = Logs.createQueryBuilder('log');
-
- if (ps.level) query.andWhere('log.level = :level', { level: ps.level });
-
- if (ps.domain) {
- const whiteDomains = ps.domain.split(' ').filter(x => !x.startsWith('-'));
- const blackDomains = ps.domain.split(' ').filter(x => x.startsWith('-')).map(x => x.substr(1));
-
- if (whiteDomains.length > 0) {
- query.andWhere(new Brackets(qb => {
- for (const whiteDomain of whiteDomains) {
- let i = 0;
- for (const subDomain of whiteDomain.split('.')) {
- const p = `whiteSubDomain_${subDomain}_${i}`;
- // SQL is 1 based, so we need '+ 1'
- qb.orWhere(`log.domain[${i + 1}] = :${p}`, { [p]: subDomain });
- i++;
- }
- }
- }));
- }
-
- if (blackDomains.length > 0) {
- query.andWhere(new Brackets(qb => {
- for (const blackDomain of blackDomains) {
- qb.andWhere(new Brackets(qb => {
- const subDomains = blackDomain.split('.');
- let i = 0;
- for (const subDomain of subDomains) {
- const p = `blackSubDomain_${subDomain}_${i}`;
- // 全体で否定できないのでド・モルガンの法則で
- // !(P && Q) を !P || !Q で表す
- // SQL is 1 based, so we need '+ 1'
- qb.orWhere(`log.domain[${i + 1}] != :${p}`, { [p]: subDomain });
- i++;
- }
- }));
- }
- }));
- }
- }
-
- const logs = await query.orderBy('log.createdAt', 'DESC').take(ps.limit!).getMany();
-
- return logs;
-});
diff --git a/src/server/api/endpoints/admin/resync-chart.ts b/src/server/api/endpoints/admin/resync-chart.ts
index b0e687333f..e01dfce1b6 100644
--- a/src/server/api/endpoints/admin/resync-chart.ts
+++ b/src/server/api/endpoints/admin/resync-chart.ts
@@ -1,5 +1,5 @@
import define from '../../define';
-import { driveChart, notesChart, usersChart, instanceChart } from '@/services/chart/index';
+import { driveChart, notesChart, usersChart } from '@/services/chart/index';
import { insertModerationLog } from '@/services/insert-moderation-log';
export const meta = {
@@ -15,7 +15,7 @@ export default define(meta, async (ps, me) => {
driveChart.resync();
notesChart.resync();
usersChart.resync();
- instanceChart.resync();
// TODO: ユーザーごとのチャートもキューに入れて更新する
+ // TODO: インスタンスごとのチャートもキューに入れて更新する
});
diff --git a/src/server/api/endpoints/antennas/update.ts b/src/server/api/endpoints/antennas/update.ts
index ff13e89bcc..d69b4feee6 100644
--- a/src/server/api/endpoints/antennas/update.ts
+++ b/src/server/api/endpoints/antennas/update.ts
@@ -137,7 +137,7 @@ export default define(meta, async (ps, user) => {
notify: ps.notify,
});
- publishInternalEvent('antennaUpdated', Antennas.findOneOrFail(antenna.id));
+ publishInternalEvent('antennaUpdated', await Antennas.findOneOrFail(antenna.id));
return await Antennas.pack(antenna.id);
});
diff --git a/src/server/api/endpoints/blocking/create.ts b/src/server/api/endpoints/blocking/create.ts
index 4deaa39974..2953252394 100644
--- a/src/server/api/endpoints/blocking/create.ts
+++ b/src/server/api/endpoints/blocking/create.ts
@@ -43,12 +43,6 @@ export const meta = {
code: 'ALREADY_BLOCKING',
id: '787fed64-acb9-464a-82eb-afbd745b9614'
},
-
- cannotBlockModerator: {
- message: 'Cannot block a moderator or an admin.',
- code: 'CANNOT_BLOCK_MODERATOR',
- id: '8544aaef-89fb-e470-9f6c-385d38b474f5'
- }
},
res: {
@@ -82,12 +76,7 @@ export default define(meta, async (ps, user) => {
throw new ApiError(meta.errors.alreadyBlocking);
}
- try {
- await create(blocker, blockee);
- } catch (e) {
- if (e.id === 'e42b7890-5e4d-9d9c-d54b-cf4dd30adfb5') throw new ApiError(meta.errors.cannotBlockModerator);
- throw e;
- }
+ await create(blocker, blockee);
NoteWatchings.delete({
userId: blocker.id,
diff --git a/src/server/api/endpoints/i/import-blocking.ts b/src/server/api/endpoints/i/import-blocking.ts
new file mode 100644
index 0000000000..d44d0b6077
--- /dev/null
+++ b/src/server/api/endpoints/i/import-blocking.ts
@@ -0,0 +1,60 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { createImportBlockingJob } from '@/queue/index';
+import * as ms from 'ms';
+import { ApiError } from '../../error';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+ secure: true,
+ requireCredential: true as const,
+
+ limit: {
+ duration: ms('1hour'),
+ max: 1,
+ },
+
+ params: {
+ fileId: {
+ validator: $.type(ID),
+ }
+ },
+
+ errors: {
+ noSuchFile: {
+ message: 'No such file.',
+ code: 'NO_SUCH_FILE',
+ id: 'ebb53e5f-6574-9c0c-0b92-7ca6def56d7e'
+ },
+
+ unexpectedFileType: {
+ message: 'We need csv file.',
+ code: 'UNEXPECTED_FILE_TYPE',
+ id: 'b6fab7d6-d945-d67c-dfdb-32da1cd12cfe'
+ },
+
+ tooBigFile: {
+ message: 'That file is too big.',
+ code: 'TOO_BIG_FILE',
+ id: 'b7fbf0b1-aeef-3b21-29ef-fadd4cb72ccf'
+ },
+
+ emptyFile: {
+ message: 'That file is empty.',
+ code: 'EMPTY_FILE',
+ id: '6f3a4dcc-f060-a707-4950-806fbdbe60d6'
+ },
+ }
+};
+
+export default define(meta, async (ps, user) => {
+ const file = await DriveFiles.findOne(ps.fileId);
+
+ if (file == null) throw new ApiError(meta.errors.noSuchFile);
+ //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
+ if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
+ if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
+
+ createImportBlockingJob(user, file.id);
+});
diff --git a/src/server/api/endpoints/i/import-muting.ts b/src/server/api/endpoints/i/import-muting.ts
new file mode 100644
index 0000000000..c17434c587
--- /dev/null
+++ b/src/server/api/endpoints/i/import-muting.ts
@@ -0,0 +1,60 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { createImportMutingJob } from '@/queue/index';
+import * as ms from 'ms';
+import { ApiError } from '../../error';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+ secure: true,
+ requireCredential: true as const,
+
+ limit: {
+ duration: ms('1hour'),
+ max: 1,
+ },
+
+ params: {
+ fileId: {
+ validator: $.type(ID),
+ }
+ },
+
+ errors: {
+ noSuchFile: {
+ message: 'No such file.',
+ code: 'NO_SUCH_FILE',
+ id: 'e674141e-bd2a-ba85-e616-aefb187c9c2a'
+ },
+
+ unexpectedFileType: {
+ message: 'We need csv file.',
+ code: 'UNEXPECTED_FILE_TYPE',
+ id: '568c6e42-c86c-ba09-c004-517f83f9f1a8'
+ },
+
+ tooBigFile: {
+ message: 'That file is too big.',
+ code: 'TOO_BIG_FILE',
+ id: '9b4ada6d-d7f7-0472-0713-4f558bd1ec9c'
+ },
+
+ emptyFile: {
+ message: 'That file is empty.',
+ code: 'EMPTY_FILE',
+ id: 'd2f12af1-e7b4-feac-86a3-519548f2728e'
+ },
+ }
+};
+
+export default define(meta, async (ps, user) => {
+ const file = await DriveFiles.findOne(ps.fileId);
+
+ if (file == null) throw new ApiError(meta.errors.noSuchFile);
+ //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
+ if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
+ if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
+
+ createImportMutingJob(user, file.id);
+});
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index fcabbbc3dd..56668d03b7 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -6,6 +6,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
import { Notifications, Followings, Mutings, Users } from '@/models/index';
import { notificationTypes } from '@/types';
import read from '@/services/note/read';
+import { Brackets } from 'typeorm';
export const meta = {
tags: ['account', 'notifications'],
@@ -94,10 +95,16 @@ export default define(meta, async (ps, user) => {
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
- query.andWhere(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`);
+ query.andWhere(new Brackets(qb => { qb
+ .where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`)
+ .orWhere('notification.notifierId IS NULL');
+ }));
query.setParameters(mutingQuery.getParameters());
- query.andWhere(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`);
+ query.andWhere(new Brackets(qb => { qb
+ .where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`)
+ .orWhere('notification.notifierId IS NULL');
+ }));
if (ps.following) {
query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: user.id });
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 9dd637251d..3b8b1579ea 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -68,6 +68,10 @@ export const meta = {
validator: $.optional.bool,
},
+ publicReactions: {
+ validator: $.optional.bool,
+ },
+
carefulBot: {
validator: $.optional.bool,
},
@@ -180,6 +184,7 @@ export default define(meta, async (ps, _user, token) => {
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable;
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
+ if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions;
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts
index 5163ed63db..3fc64d3eba 100644
--- a/src/server/api/endpoints/mute/create.ts
+++ b/src/server/api/endpoints/mute/create.ts
@@ -67,7 +67,7 @@ export default define(meta, async (ps, user) => {
}
// Create mute
- await Mutings.save({
+ await Mutings.insert({
id: genId(),
createdAt: new Date(),
muterId: muter.id,
diff --git a/src/server/api/endpoints/users/reactions.ts b/src/server/api/endpoints/users/reactions.ts
new file mode 100644
index 0000000000..fe5e4d84a9
--- /dev/null
+++ b/src/server/api/endpoints/users/reactions.ts
@@ -0,0 +1,79 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { NoteReactions, UserProfiles } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { ApiError } from '../../error';
+
+export const meta = {
+ tags: ['users', 'reactions'],
+
+ requireCredential: false as const,
+
+ params: {
+ userId: {
+ validator: $.type(ID),
+ },
+
+ limit: {
+ validator: $.optional.num.range(1, 100),
+ default: 10,
+ },
+
+ sinceId: {
+ validator: $.optional.type(ID),
+ },
+
+ untilId: {
+ validator: $.optional.type(ID),
+ },
+
+ sinceDate: {
+ validator: $.optional.num,
+ },
+
+ untilDate: {
+ validator: $.optional.num,
+ },
+ },
+
+ res: {
+ type: 'array' as const,
+ optional: false as const, nullable: false as const,
+ items: {
+ type: 'object' as const,
+ optional: false as const, nullable: false as const,
+ ref: 'NoteReaction',
+ }
+ },
+
+ errors: {
+ reactionsNotPublic: {
+ message: 'Reactions of the user is not public.',
+ code: 'REACTIONS_NOT_PUBLIC',
+ id: '673a7dd2-6924-1093-e0c0-e68456ceae5c'
+ },
+ }
+};
+
+export default define(meta, async (ps, me) => {
+ const profile = await UserProfiles.findOneOrFail(ps.userId);
+
+ if (me == null || (me.id !== ps.userId && !profile.publicReactions)) {
+ throw new ApiError(meta.errors.reactionsNotPublic);
+ }
+
+ const query = makePaginationQuery(NoteReactions.createQueryBuilder('reaction'),
+ ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+ .andWhere(`reaction.userId = :userId`, { userId: ps.userId })
+ .leftJoinAndSelect('reaction.note', 'note');
+
+ generateVisibilityQuery(query, me);
+
+ const reactions = await query
+ .take(ps.limit!)
+ .getMany();
+
+ return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, me, { withNote: true })));
+});
diff --git a/src/server/api/endpoints/users/search-by-username-and-host.ts b/src/server/api/endpoints/users/search-by-username-and-host.ts
index b9fbf48fb2..1ec5e1a743 100644
--- a/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -1,6 +1,9 @@
import $ from 'cafy';
import define from '../../define';
-import { Users } from '@/models/index';
+import { Followings, Users } from '@/models/index';
+import { Brackets } from 'typeorm';
+import { USER_ACTIVE_THRESHOLD } from '@/const';
+import { User } from '@/models/entities/user';
export const meta = {
tags: ['users'],
@@ -16,11 +19,6 @@ export const meta = {
validator: $.optional.nullable.str,
},
- offset: {
- validator: $.optional.num.min(0),
- default: 0,
- },
-
limit: {
validator: $.optional.num.range(1, 100),
default: 10,
@@ -44,43 +42,73 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
+ const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
+
if (ps.host) {
const q = Users.createQueryBuilder('user')
.where('user.isSuspended = FALSE')
.andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' });
if (ps.username) {
- q.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' });
+ q.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' });
}
q.andWhere('user.updatedAt IS NOT NULL');
q.orderBy('user.updatedAt', 'DESC');
- const users = await q.take(ps.limit!).skip(ps.offset).getMany();
+ const users = await q.take(ps.limit!).getMany();
return await Users.packMany(users, me, { detail: ps.detail });
} else if (ps.username) {
- let users = await Users.createQueryBuilder('user')
- .where('user.host IS NULL')
- .andWhere('user.isSuspended = FALSE')
- .andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' })
- .andWhere('user.updatedAt IS NOT NULL')
- .orderBy('user.updatedAt', 'DESC')
- .take(ps.limit!)
- .skip(ps.offset)
- .getMany();
+ let users: User[] = [];
- if (users.length < ps.limit!) {
- const otherUsers = await Users.createQueryBuilder('user')
- .where('user.host IS NOT NULL')
+ if (me) {
+ const followingQuery = Followings.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: me.id });
+
+ const query = Users.createQueryBuilder('user')
+ .where(`user.id IN (${ followingQuery.getQuery() })`)
+ .andWhere(`user.id != :meId`, { meId: me.id })
.andWhere('user.isSuspended = FALSE')
- .andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' })
+ .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
+ .andWhere(new Brackets(qb => { qb
+ .where('user.updatedAt IS NULL')
+ .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+ }));
+
+ query.setParameters(followingQuery.getParameters());
+
+ users = await query
+ .orderBy('user.usernameLower', 'ASC')
+ .take(ps.limit!)
+ .getMany();
+
+ if (users.length < ps.limit!) {
+ const otherQuery = await Users.createQueryBuilder('user')
+ .where(`user.id NOT IN (${ followingQuery.getQuery() })`)
+ .andWhere(`user.id != :meId`, { meId: me.id })
+ .andWhere('user.isSuspended = FALSE')
+ .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
+ .andWhere('user.updatedAt IS NOT NULL');
+
+ otherQuery.setParameters(followingQuery.getParameters());
+
+ const otherUsers = await otherQuery
+ .orderBy('user.updatedAt', 'DESC')
+ .take(ps.limit! - users.length)
+ .getMany();
+
+ users = users.concat(otherUsers);
+ }
+ } else {
+ users = await Users.createQueryBuilder('user')
+ .where('user.isSuspended = FALSE')
+ .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
.andWhere('user.updatedAt IS NOT NULL')
.orderBy('user.updatedAt', 'DESC')
.take(ps.limit! - users.length)
.getMany();
-
- users = users.concat(otherUsers);
}
return await Users.packMany(users, me, { detail: ps.detail });
diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts
index 8011d90b3d..9aa988d9ed 100644
--- a/src/server/api/endpoints/users/search.ts
+++ b/src/server/api/endpoints/users/search.ts
@@ -2,6 +2,7 @@ import $ from 'cafy';
import define from '../../define';
import { UserProfiles, Users } from '@/models/index';
import { User } from '@/models/entities/user';
+import { Brackets } from 'typeorm';
export const meta = {
tags: ['users'],
@@ -23,9 +24,9 @@ export const meta = {
default: 10,
},
- localOnly: {
- validator: $.optional.bool,
- default: false,
+ origin: {
+ validator: $.optional.str.or(['local', 'remote', 'combined']),
+ default: 'combined',
},
detail: {
@@ -46,63 +47,79 @@ export const meta = {
};
export default define(meta, async (ps, me) => {
+ const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
+
const isUsername = ps.query.startsWith('@');
let users: User[] = [];
if (isUsername) {
- users = await Users.createQueryBuilder('user')
- .where('user.host IS NULL')
- .andWhere('user.isSuspended = FALSE')
- .andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
- .andWhere('user.updatedAt IS NOT NULL')
- .orderBy('user.updatedAt', 'DESC')
+ const usernameQuery = Users.createQueryBuilder('user')
+ .where('user.usernameLower LIKE :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
+ .andWhere(new Brackets(qb => { qb
+ .where('user.updatedAt IS NULL')
+ .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+ }))
+ .andWhere('user.isSuspended = FALSE');
+
+ if (ps.origin === 'local') {
+ usernameQuery.andWhere('user.host IS NULL');
+ } else if (ps.origin === 'remote') {
+ usernameQuery.andWhere('user.host IS NOT NULL');
+ }
+
+ users = await usernameQuery
+ .orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.take(ps.limit!)
.skip(ps.offset)
.getMany();
+ } else {
+ const nameQuery = Users.createQueryBuilder('user')
+ .where('user.name ILIKE :query', { query: '%' + ps.query + '%' })
+ .andWhere(new Brackets(qb => { qb
+ .where('user.updatedAt IS NULL')
+ .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+ }))
+ .andWhere('user.isSuspended = FALSE');
- if (users.length < ps.limit! && !ps.localOnly) {
- const otherUsers = await Users.createQueryBuilder('user')
- .where('user.host IS NOT NULL')
- .andWhere('user.isSuspended = FALSE')
- .andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
- .andWhere('user.updatedAt IS NOT NULL')
- .orderBy('user.updatedAt', 'DESC')
- .take(ps.limit! - users.length)
- .getMany();
-
- users = users.concat(otherUsers);
+ if (ps.origin === 'local') {
+ nameQuery.andWhere('user.host IS NULL');
+ } else if (ps.origin === 'remote') {
+ nameQuery.andWhere('user.host IS NOT NULL');
}
- } else {
- const profQuery = UserProfiles.createQueryBuilder('prof')
- .select('prof.userId')
- .where('prof.userHost IS NULL')
- .andWhere('prof.description ilike :query', { query: '%' + ps.query + '%' });
- users = await Users.createQueryBuilder('user')
- .where(`user.id IN (${ profQuery.getQuery() })`)
- .setParameters(profQuery.getParameters())
- .andWhere('user.updatedAt IS NOT NULL')
- .orderBy('user.updatedAt', 'DESC')
+ users = await nameQuery
+ .orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.take(ps.limit!)
.skip(ps.offset)
.getMany();
- if (users.length < ps.limit! && !ps.localOnly) {
- const profQuery2 = UserProfiles.createQueryBuilder('prof')
+ if (users.length < ps.limit!) {
+ const profQuery = UserProfiles.createQueryBuilder('prof')
.select('prof.userId')
- .where('prof.userHost IS NOT NULL')
- .andWhere('prof.description ilike :query', { query: '%' + ps.query + '%' });
+ .where('prof.description ILIKE :query', { query: '%' + ps.query + '%' });
- const otherUsers = await Users.createQueryBuilder('user')
- .where(`user.id IN (${ profQuery2.getQuery() })`)
- .setParameters(profQuery2.getParameters())
- .andWhere('user.updatedAt IS NOT NULL')
- .orderBy('user.updatedAt', 'DESC')
- .take(ps.limit! - users.length)
- .getMany();
+ if (ps.origin === 'local') {
+ profQuery.andWhere('prof.userHost IS NULL');
+ } else if (ps.origin === 'remote') {
+ profQuery.andWhere('prof.userHost IS NOT NULL');
+ }
+
+ const query = Users.createQueryBuilder('user')
+ .where(`user.id IN (${ profQuery.getQuery() })`)
+ .andWhere(new Brackets(qb => { qb
+ .where('user.updatedAt IS NULL')
+ .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+ }))
+ .andWhere('user.isSuspended = FALSE')
+ .setParameters(profQuery.getParameters());
- users = users.concat(otherUsers);
+ users = users.concat(await query
+ .orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
+ .take(ps.limit!)
+ .skip(ps.offset)
+ .getMany()
+ );
}
}
diff --git a/src/server/api/stream/channels/antenna.ts b/src/server/api/stream/channels/antenna.ts
index bf9c53c453..3cbdfebb43 100644
--- a/src/server/api/stream/channels/antenna.ts
+++ b/src/server/api/stream/channels/antenna.ts
@@ -3,6 +3,7 @@ import Channel from '../channel';
import { Notes } from '@/models/index';
import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { StreamMessages } from '../types';
export default class extends Channel {
public readonly chName = 'antenna';
@@ -19,11 +20,9 @@ export default class extends Channel {
}
@autobind
- private async onEvent(data: any) {
- const { type, body } = data;
-
- if (type === 'note') {
- const note = await Notes.pack(body.id, this.user, { detail: true });
+ private async onEvent(data: StreamMessages['antenna']['payload']) {
+ if (data.type === 'note') {
+ const note = await Notes.pack(data.body.id, this.user, { detail: true });
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isMutedUserRelated(note, this.muting)) return;
@@ -34,7 +33,7 @@ export default class extends Channel {
this.send('note', note);
} else {
- this.send(type, body);
+ this.send(data.type, data.body);
}
}
diff --git a/src/server/api/stream/channels/channel.ts b/src/server/api/stream/channels/channel.ts
index 72ddbf93b4..bf7942f522 100644
--- a/src/server/api/stream/channels/channel.ts
+++ b/src/server/api/stream/channels/channel.ts
@@ -4,6 +4,7 @@ import { Notes, Users } from '@/models/index';
import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
import { User } from '@/models/entities/user';
+import { StreamMessages } from '../types';
import { Packed } from '@/misc/schema';
export default class extends Channel {
@@ -52,7 +53,7 @@ export default class extends Channel {
}
@autobind
- private onEvent(data: any) {
+ private onEvent(data: StreamMessages['channel']['payload']) {
if (data.type === 'typing') {
const id = data.body;
const begin = this.typers[id] == null;
diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts
index b99cb931da..131ac30472 100644
--- a/src/server/api/stream/channels/main.ts
+++ b/src/server/api/stream/channels/main.ts
@@ -11,35 +11,33 @@ export default class extends Channel {
public async init(params: any) {
// Subscribe main stream channel
this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
- const { type } = data;
- let { body } = data;
-
- switch (type) {
+ switch (data.type) {
case 'notification': {
- if (this.muting.has(body.userId)) return;
- if (body.note && body.note.isHidden) {
- const note = await Notes.pack(body.note.id, this.user, {
+ if (data.body.userId && this.muting.has(data.body.userId)) return;
+
+ if (data.body.note && data.body.note.isHidden) {
+ const note = await Notes.pack(data.body.note.id, this.user, {
detail: true
});
this.connection.cacheNote(note);
- body.note = note;
+ data.body.note = note;
}
break;
}
case 'mention': {
- if (this.muting.has(body.userId)) return;
- if (body.isHidden) {
- const note = await Notes.pack(body.id, this.user, {
+ if (this.muting.has(data.body.userId)) return;
+ if (data.body.isHidden) {
+ const note = await Notes.pack(data.body.id, this.user, {
detail: true
});
this.connection.cacheNote(note);
- body = note;
+ data.body = note;
}
break;
}
}
- this.send(type, body);
+ this.send(data.type, data.body);
});
}
}
diff --git a/src/server/api/stream/channels/messaging.ts b/src/server/api/stream/channels/messaging.ts
index 015b0a7650..c049e880b9 100644
--- a/src/server/api/stream/channels/messaging.ts
+++ b/src/server/api/stream/channels/messaging.ts
@@ -3,6 +3,8 @@ import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivit
import Channel from '../channel';
import { UserGroupJoinings, Users, MessagingMessages } from '@/models/index';
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
+import { UserGroup } from '@/models/entities/user-group';
+import { StreamMessages } from '../types';
export default class extends Channel {
public readonly chName = 'messaging';
@@ -12,7 +14,7 @@ export default class extends Channel {
private otherpartyId: string | null;
private otherparty: User | null;
private groupId: string | null;
- private subCh: string;
+ private subCh: `messagingStream:${User['id']}-${User['id']}` | `messagingStream:${UserGroup['id']}`;
private typers: Record<User['id'], Date> = {};
private emitTypersIntervalId: ReturnType<typeof setInterval>;
@@ -45,7 +47,7 @@ export default class extends Channel {
}
@autobind
- private onEvent(data: any) {
+ private onEvent(data: StreamMessages['messaging']['payload'] | StreamMessages['groupMessaging']['payload']) {
if (data.type === 'typing') {
const id = data.body;
const begin = this.typers[id] == null;
diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts
index ccd555e149..da4ea5ec99 100644
--- a/src/server/api/stream/index.ts
+++ b/src/server/api/stream/index.ts
@@ -14,6 +14,7 @@ import { AccessToken } from '@/models/entities/access-token';
import { UserProfile } from '@/models/entities/user-profile';
import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream';
import { UserGroup } from '@/models/entities/user-group';
+import { StreamEventEmitter, StreamMessages } from './types';
import { Packed } from '@/misc/schema';
/**
@@ -28,7 +29,7 @@ export default class Connection {
public followingChannels: Set<ChannelModel['id']> = new Set();
public token?: AccessToken;
private wsConnection: websocket.connection;
- public subscriber: EventEmitter;
+ public subscriber: StreamEventEmitter;
private channels: Channel[] = [];
private subscribingNotes: any = {};
private cachedNotes: Packed<'Note'>[] = [];
@@ -46,8 +47,8 @@ export default class Connection {
this.wsConnection.on('message', this.onWsConnectionMessage);
- this.subscriber.on('broadcast', async ({ type, body }) => {
- this.onBroadcastMessage(type, body);
+ this.subscriber.on('broadcast', data => {
+ this.onBroadcastMessage(data);
});
if (this.user) {
@@ -57,43 +58,41 @@ export default class Connection {
this.updateFollowingChannels();
this.updateUserProfile();
- this.subscriber.on(`user:${this.user.id}`, ({ type, body }) => {
- this.onUserEvent(type, body);
- });
+ this.subscriber.on(`user:${this.user.id}`, this.onUserEvent);
}
}
@autobind
- private onUserEvent(type: string, body: any) {
- switch (type) {
+ private onUserEvent(data: StreamMessages['user']['payload']) { // { type, body }と展開するとそれぞれ型が分離してしまう
+ switch (data.type) {
case 'follow':
- this.following.add(body.id);
+ this.following.add(data.body.id);
break;
case 'unfollow':
- this.following.delete(body.id);
+ this.following.delete(data.body.id);
break;
case 'mute':
- this.muting.add(body.id);
+ this.muting.add(data.body.id);
break;
case 'unmute':
- this.muting.delete(body.id);
+ this.muting.delete(data.body.id);
break;
// TODO: block events
case 'followChannel':
- this.followingChannels.add(body.id);
+ this.followingChannels.add(data.body.id);
break;
case 'unfollowChannel':
- this.followingChannels.delete(body.id);
+ this.followingChannels.delete(data.body.id);
break;
case 'updateUserProfile':
- this.userProfile = body;
+ this.userProfile = data.body;
break;
case 'terminate':
@@ -145,8 +144,8 @@ export default class Connection {
}
@autobind
- private onBroadcastMessage(type: string, body: any) {
- this.sendMessageToWs(type, body);
+ private onBroadcastMessage(data: StreamMessages['broadcast']['payload']) {
+ this.sendMessageToWs(data.type, data.body);
}
@autobind
@@ -249,7 +248,7 @@ export default class Connection {
}
@autobind
- private async onNoteStreamMessage(data: any) {
+ private async onNoteStreamMessage(data: StreamMessages['note']['payload']) {
this.sendMessageToWs('noteUpdated', {
id: data.body.id,
type: data.type,
diff --git a/src/server/api/stream/types.ts b/src/server/api/stream/types.ts
new file mode 100644
index 0000000000..70eb5c5ce5
--- /dev/null
+++ b/src/server/api/stream/types.ts
@@ -0,0 +1,299 @@
+import { EventEmitter } from 'events';
+import Emitter from 'strict-event-emitter-types';
+import { Channel } from '@/models/entities/channel';
+import { User } from '@/models/entities/user';
+import { UserProfile } from '@/models/entities/user-profile';
+import { Note } from '@/models/entities/note';
+import { Antenna } from '@/models/entities/antenna';
+import { DriveFile } from '@/models/entities/drive-file';
+import { DriveFolder } from '@/models/entities/drive-folder';
+import { Emoji } from '@/models/entities/emoji';
+import { UserList } from '@/models/entities/user-list';
+import { MessagingMessage } from '@/models/entities/messaging-message';
+import { UserGroup } from '@/models/entities/user-group';
+import { ReversiGame } from '@/models/entities/games/reversi/game';
+import { AbuseUserReport } from '@/models/entities/abuse-user-report';
+import { Signin } from '@/models/entities/signin';
+import { Page } from '@/models/entities/page';
+import { Packed } from '@/misc/schema';
+
+//#region Stream type-body definitions
+export interface InternalStreamTypes {
+ antennaCreated: Antenna;
+ antennaDeleted: Antenna;
+ antennaUpdated: Antenna;
+}
+
+export interface BroadcastTypes {
+ emojiAdded: {
+ emoji: Packed<'Emoji'>;
+ };
+}
+
+export interface UserStreamTypes {
+ terminate: {};
+ followChannel: Channel;
+ unfollowChannel: Channel;
+ updateUserProfile: UserProfile;
+ mute: User;
+ unmute: User;
+ follow: Packed<'User'>;
+ unfollow: Packed<'User'>;
+ userAdded: Packed<'User'>;
+}
+
+export interface MainStreamTypes {
+ notification: Packed<'Notification'>;
+ mention: Packed<'Note'>;
+ reply: Packed<'Note'>;
+ renote: Packed<'Note'>;
+ follow: Packed<'User'>;
+ followed: Packed<'User'>;
+ unfollow: Packed<'User'>;
+ meUpdated: Packed<'User'>;
+ pageEvent: {
+ pageId: Page['id'];
+ event: string;
+ var: any;
+ userId: User['id'];
+ user: Packed<'User'>;
+ };
+ urlUploadFinished: {
+ marker?: string | null;
+ file: Packed<'DriveFile'>;
+ };
+ readAllNotifications: undefined;
+ unreadNotification: Packed<'Notification'>;
+ unreadMention: Note['id'];
+ readAllUnreadMentions: undefined;
+ unreadSpecifiedNote: Note['id'];
+ readAllUnreadSpecifiedNotes: undefined;
+ readAllMessagingMessages: undefined;
+ messagingMessage: Packed<'MessagingMessage'>;
+ unreadMessagingMessage: Packed<'MessagingMessage'>;
+ readAllAntennas: undefined;
+ unreadAntenna: Antenna;
+ readAllAnnouncements: undefined;
+ readAllChannels: undefined;
+ unreadChannel: Note['id'];
+ myTokenRegenerated: undefined;
+ reversiNoInvites: undefined;
+ reversiInvited: Packed<'ReversiMatching'>;
+ signin: Signin;
+ registryUpdated: {
+ scope?: string[];
+ key: string;
+ value: any | null;
+ };
+ driveFileCreated: Packed<'DriveFile'>;
+ readAntenna: Antenna;
+}
+
+export interface DriveStreamTypes {
+ fileCreated: Packed<'DriveFile'>;
+ fileDeleted: DriveFile['id'];
+ fileUpdated: Packed<'DriveFile'>;
+ folderCreated: Packed<'DriveFolder'>;
+ folderDeleted: DriveFolder['id'];
+ folderUpdated: Packed<'DriveFolder'>;
+}
+
+export interface NoteStreamTypes {
+ pollVoted: {
+ choice: number;
+ userId: User['id'];
+ };
+ deleted: {
+ deletedAt: Date;
+ };
+ reacted: {
+ reaction: string;
+ emoji?: Emoji;
+ userId: User['id'];
+ };
+ unreacted: {
+ reaction: string;
+ userId: User['id'];
+ };
+}
+type NoteStreamEventTypes = {
+ [key in keyof NoteStreamTypes]: {
+ id: Note['id'];
+ body: NoteStreamTypes[key];
+ };
+};
+
+export interface ChannelStreamTypes {
+ typing: User['id'];
+}
+
+export interface UserListStreamTypes {
+ userAdded: Packed<'User'>;
+ userRemoved: Packed<'User'>;
+}
+
+export interface AntennaStreamTypes {
+ note: Note;
+}
+
+export interface MessagingStreamTypes {
+ read: MessagingMessage['id'][];
+ typing: User['id'];
+ message: Packed<'MessagingMessage'>;
+ deleted: MessagingMessage['id'];
+}
+
+export interface GroupMessagingStreamTypes {
+ read: {
+ ids: MessagingMessage['id'][];
+ userId: User['id'];
+ };
+ typing: User['id'];
+ message: Packed<'MessagingMessage'>;
+ deleted: MessagingMessage['id'];
+}
+
+export interface MessagingIndexStreamTypes {
+ read: MessagingMessage['id'][];
+ message: Packed<'MessagingMessage'>;
+}
+
+export interface ReversiStreamTypes {
+ matched: Packed<'ReversiGame'>;
+ invited: Packed<'ReversiMatching'>;
+}
+
+export interface ReversiGameStreamTypes {
+ started: Packed<'ReversiGame'>;
+ ended: {
+ winnerId?: User['id'] | null,
+ game: Packed<'ReversiGame'>;
+ };
+ updateSettings: {
+ key: string;
+ value: FIXME;
+ };
+ initForm: {
+ userId: User['id'];
+ form: FIXME;
+ };
+ updateForm: {
+ userId: User['id'];
+ id: string;
+ value: FIXME;
+ };
+ message: {
+ userId: User['id'];
+ message: FIXME;
+ };
+ changeAccepts: {
+ user1: boolean;
+ user2: boolean;
+ };
+ set: {
+ at: Date;
+ color: boolean;
+ pos: number;
+ next: boolean;
+ };
+ watching: User['id'];
+}
+
+export interface AdminStreamTypes {
+ newAbuseUserReport: {
+ id: AbuseUserReport['id'];
+ targetUserId: User['id'],
+ reporterId: User['id'],
+ comment: string;
+ };
+}
+//#endregion
+
+// 辞書(interface or type)から{ type, body }ユニオンを定義
+// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type
+// VS Codeの展開を防止するためにEvents型を定義
+type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
+type EventUnionFromDictionary<
+ T extends object,
+ U = Events<T>
+> = U[keyof U];
+
+// name/messages(spec) pairs dictionary
+export type StreamMessages = {
+ internal: {
+ name: 'internal';
+ payload: EventUnionFromDictionary<InternalStreamTypes>;
+ };
+ broadcast: {
+ name: 'broadcast';
+ payload: EventUnionFromDictionary<BroadcastTypes>;
+ };
+ user: {
+ name: `user:${User['id']}`;
+ payload: EventUnionFromDictionary<UserStreamTypes>;
+ };
+ main: {
+ name: `mainStream:${User['id']}`;
+ payload: EventUnionFromDictionary<MainStreamTypes>;
+ };
+ drive: {
+ name: `driveStream:${User['id']}`;
+ payload: EventUnionFromDictionary<DriveStreamTypes>;
+ };
+ note: {
+ name: `noteStream:${Note['id']}`;
+ payload: EventUnionFromDictionary<NoteStreamEventTypes>;
+ };
+ channel: {
+ name: `channelStream:${Channel['id']}`;
+ payload: EventUnionFromDictionary<ChannelStreamTypes>;
+ };
+ userList: {
+ name: `userListStream:${UserList['id']}`;
+ payload: EventUnionFromDictionary<UserListStreamTypes>;
+ };
+ antenna: {
+ name: `antennaStream:${Antenna['id']}`;
+ payload: EventUnionFromDictionary<AntennaStreamTypes>;
+ };
+ messaging: {
+ name: `messagingStream:${User['id']}-${User['id']}`;
+ payload: EventUnionFromDictionary<MessagingStreamTypes>;
+ };
+ groupMessaging: {
+ name: `messagingStream:${UserGroup['id']}`;
+ payload: EventUnionFromDictionary<GroupMessagingStreamTypes>;
+ };
+ messagingIndex: {
+ name: `messagingIndexStream:${User['id']}`;
+ payload: EventUnionFromDictionary<MessagingIndexStreamTypes>;
+ };
+ reversi: {
+ name: `reversiStream:${User['id']}`;
+ payload: EventUnionFromDictionary<ReversiStreamTypes>;
+ };
+ reversiGame: {
+ name: `reversiGameStream:${ReversiGame['id']}`;
+ payload: EventUnionFromDictionary<ReversiGameStreamTypes>;
+ };
+ admin: {
+ name: `adminStream:${User['id']}`;
+ payload: EventUnionFromDictionary<AdminStreamTypes>;
+ };
+ notes: {
+ name: 'notesStream';
+ payload: Packed<'Note'>;
+ };
+};
+
+// API event definitions
+// ストリームごとのEmitterの辞書を用意
+type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter<EventEmitter, { [y in StreamMessages[x]['name']]: (e: StreamMessages[x]['payload']) => void }> };
+// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
+type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
+// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする
+export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof StreamMessages]>;
+// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる
+
+// provide stream channels union
+export type StreamChannels = StreamMessages[keyof StreamMessages]['name'];
diff --git a/src/server/web/manifest.json b/src/server/web/manifest.json
index db97531bbf..48030a2980 100644
--- a/src/server/web/manifest.json
+++ b/src/server/web/manifest.json
@@ -2,7 +2,7 @@
"short_name": "Misskey",
"name": "Misskey",
"start_url": "/",
- "display": "minimal-ui",
+ "display": "standalone",
"background_color": "#313a42",
"theme_color": "#86b300",
"icons": [
diff --git a/src/services/blocking/create.ts b/src/services/blocking/create.ts
index defe377514..6aadc847a9 100644
--- a/src/services/blocking/create.ts
+++ b/src/services/blocking/create.ts
@@ -12,10 +12,6 @@ import { genId } from '@/misc/gen-id';
import { IdentifiableError } from '@/misc/identifiable-error';
export default async function(blocker: User, blockee: User) {
- if (blockee.isAdmin || blockee.isModerator) {
- throw new IdentifiableError('e42b7890-5e4d-9d9c-d54b-cf4dd30adfb5');
- }
-
await Promise.all([
cancelRequest(blocker, blockee),
cancelRequest(blockee, blocker),
diff --git a/src/services/logger.ts b/src/services/logger.ts
index 8e783e67f6..67ee441254 100644
--- a/src/services/logger.ts
+++ b/src/services/logger.ts
@@ -1,11 +1,7 @@
import * as cluster from 'cluster';
-import * as os from 'os';
import * as chalk from 'chalk';
import * as dateformat from 'dateformat';
import { envOption } from '../env';
-import { getRepository } from 'typeorm';
-import { Log } from '@/models/entities/log';
-import { genId } from '@/misc/gen-id';
import config from '@/config/index';
import * as SyslogPro from 'syslog-pro';
@@ -95,18 +91,6 @@ export default class Logger {
null as never;
send.bind(this.syslogClient)(message).catch(() => {});
- } else {
- const Logs = getRepository(Log);
- Logs.insert({
- id: genId(),
- createdAt: new Date(),
- machine: os.hostname(),
- worker: worker.toString(),
- domain: [this.domain].concat(subDomains).map(d => d.name),
- level: level,
- message: message.substr(0, 1000), // 1024を超えるとログが挿入できずエラーになり無限ループする
- data: data,
- } as Log).catch(() => {});
}
}
}
diff --git a/src/services/stream.ts b/src/services/stream.ts
index 4db1a77395..2c308a1b54 100644
--- a/src/services/stream.ts
+++ b/src/services/stream.ts
@@ -7,9 +7,28 @@ import { UserGroup } from '@/models/entities/user-group';
import config from '@/config/index';
import { Antenna } from '@/models/entities/antenna';
import { Channel } from '@/models/entities/channel';
+import {
+ StreamChannels,
+ AdminStreamTypes,
+ AntennaStreamTypes,
+ BroadcastTypes,
+ ChannelStreamTypes,
+ DriveStreamTypes,
+ GroupMessagingStreamTypes,
+ InternalStreamTypes,
+ MainStreamTypes,
+ MessagingIndexStreamTypes,
+ MessagingStreamTypes,
+ NoteStreamTypes,
+ ReversiGameStreamTypes,
+ ReversiStreamTypes,
+ UserListStreamTypes,
+ UserStreamTypes
+} from '@/server/api/stream/types';
+import { Packed } from '@/misc/schema';
class Publisher {
- private publish = (channel: string, type: string | null, value?: any): void => {
+ private publish = (channel: StreamChannels, type: string | null, value?: any): void => {
const message = type == null ? value : value == null ?
{ type: type, body: null } :
{ type: type, body: value };
@@ -20,70 +39,70 @@ class Publisher {
}));
}
- public publishInternalEvent = (type: string, value?: any): void => {
+ public publishInternalEvent = <K extends keyof InternalStreamTypes>(type: K, value?: InternalStreamTypes[K]): void => {
this.publish('internal', type, typeof value === 'undefined' ? null : value);
}
- public publishUserEvent = (userId: User['id'], type: string, value?: any): void => {
+ public publishUserEvent = <K extends keyof UserStreamTypes>(userId: User['id'], type: K, value?: UserStreamTypes[K]): void => {
this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishBroadcastStream = (type: string, value?: any): void => {
+ public publishBroadcastStream = <K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void => {
this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
}
- public publishMainStream = (userId: User['id'], type: string, value?: any): void => {
+ public publishMainStream = <K extends keyof MainStreamTypes>(userId: User['id'], type: K, value?: MainStreamTypes[K]): void => {
this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishDriveStream = (userId: User['id'], type: string, value?: any): void => {
+ public publishDriveStream = <K extends keyof DriveStreamTypes>(userId: User['id'], type: K, value?: DriveStreamTypes[K]): void => {
this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishNoteStream = (noteId: Note['id'], type: string, value: any): void => {
+ public publishNoteStream = <K extends keyof NoteStreamTypes>(noteId: Note['id'], type: K, value?: NoteStreamTypes[K]): void => {
this.publish(`noteStream:${noteId}`, type, {
id: noteId,
body: value
});
}
- public publishChannelStream = (channelId: Channel['id'], type: string, value?: any): void => {
+ public publishChannelStream = <K extends keyof ChannelStreamTypes>(channelId: Channel['id'], type: K, value?: ChannelStreamTypes[K]): void => {
this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishUserListStream = (listId: UserList['id'], type: string, value?: any): void => {
+ public publishUserListStream = <K extends keyof UserListStreamTypes>(listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void => {
this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishAntennaStream = (antennaId: Antenna['id'], type: string, value?: any): void => {
+ public publishAntennaStream = <K extends keyof AntennaStreamTypes>(antennaId: Antenna['id'], type: K, value?: AntennaStreamTypes[K]): void => {
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: string, value?: any): void => {
+ public publishMessagingStream = <K extends keyof MessagingStreamTypes>(userId: User['id'], otherpartyId: User['id'], type: K, value?: MessagingStreamTypes[K]): void => {
this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishGroupMessagingStream = (groupId: UserGroup['id'], type: string, value?: any): void => {
+ public publishGroupMessagingStream = <K extends keyof GroupMessagingStreamTypes>(groupId: UserGroup['id'], type: K, value?: GroupMessagingStreamTypes[K]): void => {
this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishMessagingIndexStream = (userId: User['id'], type: string, value?: any): void => {
+ public publishMessagingIndexStream = <K extends keyof MessagingIndexStreamTypes>(userId: User['id'], type: K, value?: MessagingIndexStreamTypes[K]): void => {
this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishReversiStream = (userId: User['id'], type: string, value?: any): void => {
+ public publishReversiStream = <K extends keyof ReversiStreamTypes>(userId: User['id'], type: K, value?: ReversiStreamTypes[K]): void => {
this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishReversiGameStream = (gameId: ReversiGame['id'], type: string, value?: any): void => {
+ public publishReversiGameStream = <K extends keyof ReversiGameStreamTypes>(gameId: ReversiGame['id'], type: K, value?: ReversiGameStreamTypes[K]): void => {
this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
}
- public publishNotesStream = (note: any): void => {
+ public publishNotesStream = (note: Packed<'Note'>): void => {
this.publish('notesStream', null, note);
}
- public publishAdminStream = (userId: User['id'], type: string, value?: any): void => {
+ public publishAdminStream = <K extends keyof AdminStreamTypes>(userId: User['id'], type: K, value?: AdminStreamTypes[K]): void => {
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
}