diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-10-23 01:08:45 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2021-10-23 01:08:45 +0900 |
| commit | d0d5068f728e13f3ebe1dc227ddaacf380817ec4 (patch) | |
| tree | 7bb95207e01bff1bee9877829c0556d3ecf62176 /src/client/pages | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.93.0 (diff) | |
| download | misskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.tar.gz misskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.tar.bz2 misskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.zip | |
Merge branch 'develop'
Diffstat (limited to 'src/client/pages')
| -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.vue | 242 | ||||
| -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.vue | 102 | ||||
| -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.vue | 19 | ||||
| -rw-r--r-- | src/client/pages/instance-info.vue | 268 | ||||
| -rw-r--r-- | src/client/pages/instance/logs.vue | 97 | ||||
| -rw-r--r-- | src/client/pages/instance/overview.vue | 127 | ||||
| -rw-r--r-- | src/client/pages/instance/queue.chart.vue | 218 | ||||
| -rw-r--r-- | src/client/pages/settings/import-export.vue | 20 | ||||
| -rw-r--r-- | src/client/pages/settings/privacy.vue | 7 | ||||
| -rw-r--r-- | src/client/pages/settings/theme.manage.vue | 14 | ||||
| -rw-r--r-- | src/client/pages/share.vue | 158 | ||||
| -rw-r--r-- | src/client/pages/user/index.timeline.vue | 7 | ||||
| -rw-r--r-- | src/client/pages/user/index.vue | 9 | ||||
| -rw-r--r-- | src/client/pages/user/reactions.vue | 81 |
42 files changed, 726 insertions, 1052 deletions
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> |