summaryrefslogtreecommitdiff
path: root/src/client/app/admin
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2018-11-02 23:05:53 +0900
committersyuilo <syuilotan@yahoo.co.jp>2018-11-02 23:05:53 +0900
commitf2e719b3612506493b0d27c4476fcd0879ed1eea (patch)
tree6d3af4d1fd886ea963f6a67821d669b4b1cd53fe /src/client/app/admin
parent[API] Implement admin/add-emoji (diff)
downloadsharkey-f2e719b3612506493b0d27c4476fcd0879ed1eea.tar.gz
sharkey-f2e719b3612506493b0d27c4476fcd0879ed1eea.tar.bz2
sharkey-f2e719b3612506493b0d27c4476fcd0879ed1eea.zip
[Client] Admin page improved
Diffstat (limited to 'src/client/app/admin')
-rw-r--r--src/client/app/admin/script.ts27
-rw-r--r--src/client/app/admin/style.styl6
-rw-r--r--src/client/app/admin/views/announcements.vue55
-rw-r--r--src/client/app/admin/views/cpu-memory.vue137
-rw-r--r--src/client/app/admin/views/dashboard.vue117
-rw-r--r--src/client/app/admin/views/emoji.vue48
-rw-r--r--src/client/app/admin/views/hashtags.vue46
-rw-r--r--src/client/app/admin/views/index.vue101
-rw-r--r--src/client/app/admin/views/instance.vue62
-rw-r--r--src/client/app/admin/views/users.vue129
10 files changed, 728 insertions, 0 deletions
diff --git a/src/client/app/admin/script.ts b/src/client/app/admin/script.ts
new file mode 100644
index 0000000000..4002734d3d
--- /dev/null
+++ b/src/client/app/admin/script.ts
@@ -0,0 +1,27 @@
+/**
+ * Admin
+ */
+
+import VueRouter from 'vue-router';
+
+// Style
+import './style.styl';
+
+import init from '../init';
+import Index from './views/index.vue';
+
+init(launch => {
+ document.title = 'Admin';
+
+ // Init router
+ const router = new VueRouter({
+ mode: 'history',
+ base: '/admin/',
+ routes: [
+ { path: '/', component: Index },
+ ]
+ });
+
+ // Launch the app
+ launch(router);
+});
diff --git a/src/client/app/admin/style.styl b/src/client/app/admin/style.styl
new file mode 100644
index 0000000000..5ea3950464
--- /dev/null
+++ b/src/client/app/admin/style.styl
@@ -0,0 +1,6 @@
+@import "../app"
+@import "../reset"
+
+html
+ height 100%
+ background #EBEBEB
diff --git a/src/client/app/admin/views/announcements.vue b/src/client/app/admin/views/announcements.vue
new file mode 100644
index 0000000000..049a8d5721
--- /dev/null
+++ b/src/client/app/admin/views/announcements.vue
@@ -0,0 +1,55 @@
+<template>
+<div>
+ <ui-card>
+ <div slot="title">%i18n:@announcements%</div>
+ <section>
+ <textarea class="qldxjjsrseehkusjuoooapmsprvfrxyl" v-model="broadcasts" placeholder='[ { "title": "Title1", "text": "Text1" }, { "title": "Title2", "text": "Text2" } ]'></textarea>
+ <ui-button @click="save">%i18n:@save%</ui-button>
+ </section>
+ </ui-card>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+
+export default Vue.extend({
+ data() {
+ return {
+ broadcasts: '',
+ };
+ },
+ created() {
+ (this as any).os.getMeta().then(meta => {
+ this.broadcasts = JSON.stringify(meta.broadcasts, null, ' ');
+ });
+ },
+ methods: {
+ save() {
+ let json;
+
+ try {
+ json = JSON.parse(this.broadcasts);
+ } catch (e) {
+ (this as any).os.apis.dialog({ text: `Failed: ${e}` });
+ return;
+ }
+
+ (this as any).api('admin/update-meta', {
+ broadcasts: json
+ }).then(() => {
+ (this as any).os.apis.dialog({ text: `Saved` });
+ }.catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed ${e}` });
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.qldxjjsrseehkusjuoooapmsprvfrxyl
+ width 100%
+ min-height 300px
+
+</style>
diff --git a/src/client/app/admin/views/cpu-memory.vue b/src/client/app/admin/views/cpu-memory.vue
new file mode 100644
index 0000000000..5d03b30ef4
--- /dev/null
+++ b/src/client/app/admin/views/cpu-memory.vue
@@ -0,0 +1,137 @@
+<template>
+<div class="zyknedwtlthezamcjlolyusmipqmjgxz">
+ <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
+ <defs>
+ <linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
+ <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
+ <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
+ </linearGradient>
+ <mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
+ <polygon
+ :points="cpuPolygonPoints"
+ fill="#fff"
+ fill-opacity="0.5"/>
+ <polyline
+ :points="cpuPolylinePoints"
+ fill="none"
+ stroke="#fff"
+ stroke-width="1"/>
+ </mask>
+ </defs>
+ <rect
+ x="0" y="0"
+ :width="viewBoxX" :height="viewBoxY"
+ :style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/>
+ <text x="1" y="12">CPU <tspan>{{ cpuP }}%</tspan></text>
+ </svg>
+ <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
+ <defs>
+ <linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
+ <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
+ <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
+ </linearGradient>
+ <mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
+ <polygon
+ :points="memPolygonPoints"
+ fill="#fff"
+ fill-opacity="0.5"/>
+ <polyline
+ :points="memPolylinePoints"
+ fill="none"
+ stroke="#fff"
+ stroke-width="1"/>
+ </mask>
+ </defs>
+ <rect
+ x="0" y="0"
+ :width="viewBoxX" :height="viewBoxY"
+ :style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/>
+ <text x="1" y="12">MEM <tspan>{{ memP }}%</tspan></text>
+ </svg>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as uuid from 'uuid';
+
+export default Vue.extend({
+ props: ['connection'],
+ data() {
+ return {
+ viewBoxX: 200,
+ viewBoxY: 70,
+ stats: [],
+ cpuGradientId: uuid(),
+ cpuMaskId: uuid(),
+ memGradientId: uuid(),
+ memMaskId: uuid(),
+ cpuPolylinePoints: '',
+ memPolylinePoints: '',
+ cpuPolygonPoints: '',
+ memPolygonPoints: '',
+ cpuP: '',
+ memP: ''
+ };
+ },
+ mounted() {
+ this.connection.on('stats', this.onStats);
+ this.connection.on('statsLog', this.onStatsLog);
+ this.connection.send('requestLog', {
+ id: Math.random().toString().substr(2, 8),
+ length: 200
+ });
+ },
+ beforeDestroy() {
+ this.connection.off('stats', this.onStats);
+ this.connection.off('statsLog', this.onStatsLog);
+ },
+ methods: {
+ onStats(stats) {
+ this.stats.push(stats);
+ if (this.stats.length > 200) this.stats.shift();
+
+ const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu_usage) * this.viewBoxY]);
+ const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.used / s.mem.total)) * this.viewBoxY]);
+ this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+ this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+
+ this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
+ this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
+
+ this.cpuP = (stats.cpu_usage * 100).toFixed(0);
+ this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0);
+ },
+ onStatsLog(statsLog) {
+ statsLog.reverse().forEach(stats => this.onStats(stats));
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.zyknedwtlthezamcjlolyusmipqmjgxz
+ > svg
+ display block
+ width 50%
+ float left
+
+ &:first-child
+ padding-right 5px
+
+ &:last-child
+ padding-left 5px
+
+ > text
+ font-size 10px
+ fill var(--chartCaption)
+
+ > tspan
+ opacity 0.5
+
+ &:after
+ content ""
+ display block
+ clear both
+
+</style>
diff --git a/src/client/app/admin/views/dashboard.vue b/src/client/app/admin/views/dashboard.vue
new file mode 100644
index 0000000000..5af5255e20
--- /dev/null
+++ b/src/client/app/admin/views/dashboard.vue
@@ -0,0 +1,117 @@
+<template>
+<div class="obdskegsannmntldydackcpzezagxqfy">
+ <div v-if="stats" class="stats">
+ <div>
+ <div>%fa:user%</div>
+ <div>
+ <span>%i18n:@original-users%</span>
+ <b>{{ stats.originalUsersCount | number }}</b>
+ </div>
+ </div>
+ <div>
+ <div>%fa:pencil-alt%</div>
+ <div>
+ <span>%i18n:@original-notes%</span>
+ <b>{{ stats.originalNotesCount | number }}</b>
+ </div>
+ </div>
+ <div>
+ <div>%fa:user%</div>
+ <div>
+ <span>%i18n:@all-users%</span>
+ <b>{{ stats.usersCount | number }}</b>
+ </div>
+ </div>
+ <div>
+ <div>%fa:pencil-alt%</div>
+ <div>
+ <span>%i18n:@all-notes%</span>
+ <b>{{ stats.notesCount | number }}</b>
+ </div>
+ </div>
+ </div>
+
+ <div class="cpu-memory">
+ <x-cpu-memory :connection="connection"/>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+import XCpuMemory from "./cpu-memory.vue";
+
+export default Vue.extend({
+ components: {
+ XCpuMemory
+ },
+ data() {
+ return {
+ stats: null,
+ connection: null
+ };
+ },
+ created() {
+ this.connection = (this as any).os.stream.useSharedConnection('serverStats');
+
+ (this as any).os.getMeta().then(meta => {
+ this.disableRegistration = meta.disableRegistration;
+ this.disableLocalTimeline = meta.disableLocalTimeline;
+ this.bannerUrl = meta.bannerUrl;
+ });
+
+ (this as any).api('stats').then(stats => {
+ this.stats = stats;
+ });
+ },
+ beforeDestroy() {
+ this.connection.dispose();
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.obdskegsannmntldydackcpzezagxqfy
+ > .stats
+ display flex
+ justify-content space-between
+ margin-bottom 16px
+
+ > div
+ display flex
+ align-items center
+ flex 1
+ max-width 300px
+ margin-right 16px
+ text-align center
+ color var(--text)
+ box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
+ background var(--face)
+ border-radius 8px
+
+ &:last-child
+ margin-right 0
+
+ > div:first-child
+ padding 16px 24px
+ font-size 28px
+
+ > div:last-child
+ flex 1
+ padding 16px 32px 16px 0
+ text-align right
+
+ > span
+ opacity 0.7
+
+ > b
+ display block
+
+ > .cpu-memory
+ margin-bottom 16px
+ padding 32px
+ box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
+ background var(--face)
+ border-radius 8px
+
+</style>
diff --git a/src/client/app/admin/views/emoji.vue b/src/client/app/admin/views/emoji.vue
new file mode 100644
index 0000000000..1446cc1a91
--- /dev/null
+++ b/src/client/app/admin/views/emoji.vue
@@ -0,0 +1,48 @@
+<template>
+<div>
+ <ui-card>
+ <div slot="title">%fa:plus% %i18n:@add-emoji.title%</div>
+ <section class="fit-top">
+ <ui-input v-model="name">
+ <span>%i18n:@add-emoji.name%</span>
+ <span slot="text">%i18n:@add-emoji.name-desc%</span>
+ </ui-input>
+ <ui-input v-model="aliases">
+ <span>%i18n:@add-emoji.aliases%</span>
+ <span slot="text">%i18n:@add-emoji.aliases-desc%</span>
+ </ui-input>
+ <ui-input v-model="url">
+ <span>%i18n:@add-emoji.url%</span>
+ </ui-input>
+ <ui-button @click="add">%i18n:@add-emoji.add%</ui-button>
+ </section>
+ </ui-card>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+
+export default Vue.extend({
+ data() {
+ return {
+ name: '',
+ url: '',
+ aliases: '',
+ };
+ },
+ methods: {
+ add() {
+ (this as any).api('admin/add-emoji', {
+ name: this.name,
+ url: this.url,
+ aliases: this.aliases.split(' ')
+ }).then(() => {
+ (this as any).os.apis.dialog({ text: `Added` });
+ }).catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed ${e}` });
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/app/admin/views/hashtags.vue b/src/client/app/admin/views/hashtags.vue
new file mode 100644
index 0000000000..be90cea1db
--- /dev/null
+++ b/src/client/app/admin/views/hashtags.vue
@@ -0,0 +1,46 @@
+<template>
+<div>
+ <ui-card>
+ <div slot="title">%i18n:@hided-tags%</div>
+ <section>
+ <textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hidedTags"></textarea>
+ <ui-button @click="save">%i18n:@save%</ui-button>
+ </section>
+ </ui-card>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+
+export default Vue.extend({
+ data() {
+ return {
+ hidedTags: '',
+ };
+ },
+ created() {
+ (this as any).os.getMeta().then(meta => {
+ this.hidedTags = meta.hidedTags.join('\n');
+ });
+ },
+ methods: {
+ save() {
+ (this as any).api('admin/update-meta', {
+ hidedTags: this.hidedTags.split('\n')
+ }).then(() => {
+ (this as any).os.apis.dialog({ text: `Saved` });
+ }).catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed ${e}` });
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.jdnqwkzlnxcfftthoybjxrebyolvoucw
+ width 100%
+ min-height 300px
+
+</style>
diff --git a/src/client/app/admin/views/index.vue b/src/client/app/admin/views/index.vue
new file mode 100644
index 0000000000..c674bee028
--- /dev/null
+++ b/src/client/app/admin/views/index.vue
@@ -0,0 +1,101 @@
+<template>
+<div class="mk-admin">
+ <nav>
+ <ul>
+ <li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }">%fa:home .fw%%i18n:@dashboard%</li>
+ <li @click="nav('instance')" :class="{ active: page == 'instance' }">%fa:cog .fw%%i18n:@instance%</li>
+ <li @click="nav('users')" :class="{ active: page == 'users' }">%fa:users .fw%%i18n:@users%</li>
+ <li @click="nav('emoji')" :class="{ active: page == 'emoji' }">%fa:grin R .fw%%i18n:@emoji%</li>
+ <li @click="nav('announcements')" :class="{ active: page == 'announcements' }">%fa:broadcast-tower .fw%%i18n:@announcements%</li>
+ <li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }">%fa:hashtag .fw%%i18n:@hashtags%</li>
+
+ <!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }">%fa:cloud .fw%%i18n:common.drive%</li> -->
+ <!-- <li @click="nav('update')" :class="{ active: page == 'update' }">%i18n:@update%</li> -->
+ </ul>
+ </nav>
+ <main>
+ <div v-show="page == 'dashboard'"><x-dashboard/></div>
+ <div v-show="page == 'instance'"><x-instance/></div>
+ <div v-if="page == 'users'"><x-users/></div>
+ <div v-show="page == 'emoji'"><x-emoji/></div>
+ <div v-show="page == 'announcements'"><x-announcements/></div>
+ <div v-show="page == 'hashtags'"><x-hashtags/></div>
+ <div v-if="page == 'drive'"></div>
+ <div v-if="page == 'update'"></div>
+ </main>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+import XDashboard from "./dashboard.vue";
+import XInstance from "./instance.vue";
+import XEmoji from "./emoji.vue";
+import XAnnouncements from "./announcements.vue";
+import XHashtags from "./hashtags.vue";
+import XUsers from "./users.vue";
+
+export default Vue.extend({
+ components: {
+ XDashboard,
+ XInstance,
+ XEmoji,
+ XAnnouncements,
+ XHashtags,
+ XUsers
+ },
+ data() {
+ return {
+ page: 'dashboard'
+ };
+ },
+ methods: {
+ nav(page: string) {
+ this.page = page;
+ }
+ }
+});
+</script>
+
+<style lang="stylus">
+.mk-admin
+ display flex
+ height 100%
+
+ > nav
+ position fixed
+ z-index 10000
+ top 0
+ left 0
+ width 250px
+ height 100vh
+ padding 16px 0 0 0
+ overflow auto
+ background #333
+ color #fff
+
+ > ul
+ margin 0
+ padding 0
+ list-style none
+
+ > li
+ display block
+ padding 10px 16px
+ margin 0
+ cursor pointer
+ user-select none
+ transition margin-left 0.2s ease
+
+ > [data-fa]
+ margin-right 4px
+
+ &.active
+ margin-left 8px
+ color var(--primary) !important
+
+ > main
+ width 100%
+ padding 32px 32px 32px calc(32px + 250px)
+
+</style>
diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue
new file mode 100644
index 0000000000..542a0d54ad
--- /dev/null
+++ b/src/client/app/admin/views/instance.vue
@@ -0,0 +1,62 @@
+<template>
+<div>
+ <ui-card>
+ <div slot="title">%i18n:@banner-url%</div>
+ <section class="fit-top">
+ <ui-input v-model="bannerUrl"/>
+ <ui-button @click="updateMeta">%i18n:@save%</ui-button>
+ </section>
+ </ui-card>
+
+ <ui-card>
+ <div slot="title">%i18n:@disable-registration%</div>
+ <section>
+ <input type="checkbox" v-model="disableRegistration" @change="updateMeta">
+ <button class="ui" @click="invite">%i18n:@invite%</button>
+ <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
+ </section>
+ </ui-card>
+
+ <ui-card>
+ <div slot="title">%i18n:@disable-local-timeline%</div>
+ <section>
+ <input type="checkbox" v-model="disableLocalTimeline" @change="updateMeta">
+ </section>
+ </ui-card>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+
+export default Vue.extend({
+ data() {
+ return {
+ disableRegistration: false,
+ disableLocalTimeline: false,
+ bannerUrl: null,
+ inviteCode: null,
+ };
+ },
+ methods: {
+ invite() {
+ (this as any).api('admin/invite').then(x => {
+ this.inviteCode = x.code;
+ }).catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed ${e}` });
+ });
+ },
+ updateMeta() {
+ (this as any).api('admin/update-meta', {
+ disableRegistration: this.disableRegistration,
+ disableLocalTimeline: this.disableLocalTimeline,
+ bannerUrl: this.bannerUrl
+ }).then(() => {
+ (this as any).os.apis.dialog({ text: `Saved` });
+ }).catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed ${e}` });
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue
new file mode 100644
index 0000000000..3c59943688
--- /dev/null
+++ b/src/client/app/admin/views/users.vue
@@ -0,0 +1,129 @@
+<template>
+<div>
+ <ui-card>
+ <div slot="title">%i18n:@verify-user%</div>
+ <section class="fit-top">
+ <ui-input v-model="verifyUsername" type="text">
+ <span slot="prefix">@</span>
+ </ui-input>
+ <ui-button @click="verifyUser" :disabled="verifying">%i18n:@verify%</ui-button>
+ </section>
+ </ui-card>
+
+ <ui-card>
+ <div slot="title">%i18n:@unverify-user%</div>
+ <section class="fit-top">
+ <ui-input v-model="unverifyUsername" type="text">
+ <span slot="prefix">@</span>
+ </ui-input>
+ <ui-button @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</ui-button>
+ </section>
+ </ui-card>
+
+ <ui-card>
+ <div slot="title">%i18n:@suspend-user%</div>
+ <section class="fit-top">
+ <ui-input v-model="suspendUsername" type="text">
+ <span slot="prefix">@</span>
+ </ui-input>
+ <ui-button @click="suspendUser" :disabled="suspending">%i18n:@suspend%</ui-button>
+ </section>
+ </ui-card>
+
+ <ui-card>
+ <div slot="title">%i18n:@unsuspend-user%</div>
+ <section class="fit-top">
+ <ui-input v-model="unsuspendUsername" type="text">
+ <span slot="prefix">@</span>
+ </ui-input>
+ <ui-button @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</ui-button>
+ </section>
+ </ui-card>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+import parseAcct from "../../../../misc/acct/parse";
+
+export default Vue.extend({
+ data() {
+ return {
+ verifyUsername: null,
+ verifying: false,
+ unverifyUsername: null,
+ unverifying: false,
+ suspendUsername: null,
+ suspending: false,
+ unsuspendUsername: null,
+ unsuspending: false
+ };
+ },
+
+ methods: {
+ async verifyUser() {
+ this.verifying = true;
+
+ const process = async () => {
+ const user = await (this as any).os.api('users/show', parseAcct(this.verifyUsername));
+ await (this as any).os.api('admin/verify-user', { userId: user.id });
+ (this as any).os.apis.dialog({ text: '%i18n:@verified%' });
+ };
+
+ await process().catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed: ${e}` });
+ });
+
+ this.verifying = false;
+ },
+
+ async unverifyUser() {
+ this.unverifying = true;
+
+ const process = async () => {
+ const user = await (this as any).os.api('users/show', parseAcct(this.unverifyUsername));
+ await (this as any).os.api('admin/unverify-user', { userId: user.id });
+ (this as any).os.apis.dialog({ text: '%i18n:@unverified%' });
+ };
+
+ await process().catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed: ${e}` });
+ });
+
+ this.unverifying = false;
+ },
+
+ async suspendUser() {
+ this.suspending = true;
+
+ const process = async () => {
+ const user = await (this as any).os.api('users/show', parseAcct(this.suspendUsername));
+ await (this as any).os.api('admin/suspend-user', { userId: user.id });
+ (this as any).os.apis.dialog({ text: '%i18n:@suspended%' });
+ };
+
+ await process().catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed: ${e}` });
+ });
+
+ this.suspending = false;
+ },
+
+ async unsuspendUser() {
+ this.unsuspending = true;
+
+ const process = async () => {
+ const user = await (this as any).os.api('users/show', parseAcct(this.unsuspendUsername));
+ await (this as any).os.api('admin/unsuspend-user', { userId: user.id });
+ (this as any).os.apis.dialog({ text: '%i18n:@unsuspended%' });
+ };
+
+ await process().catch(e => {
+ (this as any).os.apis.dialog({ text: `Failed: ${e}` });
+ });
+
+ this.unsuspending = false;
+ }
+ }
+});
+</script>