diff --git a/src/client/components/form/suspense.vue b/src/client/components/form/suspense.vue index 2a48faccb3..d04dc07624 100644 --- a/src/client/components/form/suspense.vue +++ b/src/client/components/form/suspense.vue @@ -9,9 +9,9 @@ <slot :result="result"></slot> </div> <div class="_formItem" v-else> - <div class="_formPanel"> - error! - <button @click="retry">retry</button> + <div class="_formPanel eiurkvay"> + <div><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</div> + <MkButton inline @click="retry" class="retry"><i class="fas fa-redo-alt"></i> {{ $ts.retry }}</MkButton> </div> </div> </transition> @@ -20,8 +20,13 @@ <script lang="ts"> import { defineComponent, PropType, ref, watch } from 'vue'; import './form.scss'; +import MkButton from '@client/components/ui/button.vue'; export default defineComponent({ + components: { + MkButton + }, + props: { p: { type: Function as PropType<() => Promise<any>>, @@ -84,4 +89,13 @@ export default defineComponent({ .fade-leave-to { opacity: 0; } + +.eiurkvay { + padding: 16px; + text-align: center; + + > .retry { + margin-top: 16px; + } +} </style> diff --git a/src/client/pages/instance/file-dialog.vue b/src/client/pages/instance/file-dialog.vue index ae6755465c..74a755fa15 100644 --- a/src/client/pages/instance/file-dialog.vue +++ b/src/client/pages/instance/file-dialog.vue @@ -82,7 +82,7 @@ export default defineComponent({ }, showUser() { - os.pageWindow(`/instance/user/${this.file.userId}`); + os.pageWindow(`/user-info/${this.file.userId}`); }, async del() { diff --git a/src/client/pages/instance/user.vue b/src/client/pages/instance/user.vue deleted file mode 100644 index fbc10a3672..0000000000 --- a/src/client/pages/instance/user.vue +++ /dev/null @@ -1,229 +0,0 @@ -<template> -<FormBase> - <FormSuspense :p="init"> - <div class="_formItem aeakzknw"> - <MkAvatar class="avatar" :user="user" :show-indicator="true"/> - </div> - - <FormLink :to="userPage(user)">Profile</FormLink> - - <FormGroup> - <FormKeyValueView> - <template #key>Acct</template> - <template #value><span class="_monospace">{{ acct(user) }}</span></template> - </FormKeyValueView> - - <FormKeyValueView> - <template #key>ID</template> - <template #value><span class="_monospace">{{ user.id }}</span></template> - </FormKeyValueView> - </FormGroup> - - <FormGroup> - <FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" @update:value="toggleModerator" v-model:value="moderator">{{ $ts.moderator }}</FormSwitch> - <FormSwitch @update:value="toggleSilence" v-model:value="silenced">{{ $ts.silence }}</FormSwitch> - <FormSwitch @update:value="toggleSuspend" v-model:value="suspended">{{ $ts.suspend }}</FormSwitch> - </FormGroup> - - <FormGroup> - <FormButton v-if="user.host != null" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton> - <FormButton v-if="user.host == null" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton> - </FormGroup> - - <FormGroup> - <FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink> - - <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink> - <FormKeyValueView v-else> - <template #key>{{ $ts.instanceInfo }}</template> - <template #value>(Local user)</template> - </FormKeyValueView> - </FormGroup> - - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts.updatedAt }}</template> - <template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template> - </FormKeyValueView> - </FormGroup> - - <FormObjectView tall :value="user"> - <span>Raw</span> - </FormObjectView> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { computed, defineAsyncComponent, defineComponent } from 'vue'; -import FormObjectView from '@client/components/form/object-view.vue'; -import FormSwitch from '@client/components/form/switch.vue'; -import FormLink from '@client/components/form/link.vue'; -import FormBase from '@client/components/form/base.vue'; -import FormGroup from '@client/components/form/group.vue'; -import FormButton from '@client/components/form/button.vue'; -import FormKeyValueView from '@client/components/form/key-value-view.vue'; -import FormSuspense from '@client/components/form/suspense.vue'; -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 { url } from '@client/config'; -import { userPage, acct } from '@client/filters/user'; - -export default defineComponent({ - components: { - FormBase, - FormSwitch, - FormObjectView, - FormButton, - FormLink, - FormGroup, - FormKeyValueView, - FormSuspense, - }, - - props: { - userId: { - type: String, - required: true - } - }, - - data() { - return { - [symbols.PAGE_INFO]: computed(() => ({ - title: this.$ts.userInfo, - icon: 'fas fa-info-circle', - actions: this.user ? [this.user.url ? { - text: this.user.url, - icon: 'fas fa-external-link-alt', - handler: () => { - window.open(this.user.url, '_blank'); - } - } : undefined].filter(x => x !== undefined) : [], - })), - init: null, - user: null, - info: null, - moderator: false, - silenced: false, - suspended: false, - } - }, - - watch: { - userId: { - handler() { - this.init = this.createFetcher(); - }, - immediate: true - } - }, - - methods: { - number, - bytes, - userPage, - acct, - - createFetcher() { - return () => Promise.all([os.api('users/show', { - userId: this.userId - }), os.api('admin/show-user', { - userId: this.userId - })]).then(([user, info]) => { - this.user = user; - this.info = info; - this.moderator = this.info.isModerator; - this.silenced = this.info.isSilenced; - this.suspended = this.info.isSuspended; - }); - }, - - refreshUser() { - this.init = this.createFetcher(); - }, - - async updateRemoteUser() { - await os.apiWithDialog('admin/update-remote-user', { userId: this.user.id }); - this.refreshUser(); - }, - - async resetPassword() { - os.apiWithDialog('admin/reset-password', { - userId: this.user.id, - }, undefined, ({ password }) => { - os.dialog({ - type: 'success', - text: this.$t('newPasswordIs', { password }) - }); - }); - }, - - async toggleSilence(v) { - const confirm = await os.dialog({ - type: 'warning', - showCancelButton: true, - text: v ? this.$ts.silenceConfirm : this.$ts.unsilenceConfirm, - }); - if (confirm.canceled) { - this.silenced = !v; - } else { - await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id }); - await this.refreshUser(); - } - }, - - async toggleSuspend(v) { - const confirm = await os.dialog({ - type: 'warning', - showCancelButton: true, - text: v ? this.$ts.suspendConfirm : this.$ts.unsuspendConfirm, - }); - if (confirm.canceled) { - this.suspended = !v; - } else { - await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id }); - await this.refreshUser(); - } - }, - - async toggleModerator(v) { - await os.api(v ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: this.user.id }); - await this.refreshUser(); - }, - - async deleteAllFiles() { - const confirm = await os.dialog({ - type: 'warning', - showCancelButton: true, - text: this.$ts.deleteAllFilesConfirm, - }); - if (confirm.canceled) return; - const process = async () => { - await os.api('admin/delete-all-files-of-a-user', { userId: this.user.id }); - os.success(); - }; - await process().catch(e => { - os.dialog({ - type: 'error', - text: e.toString() - }); - }); - await this.refreshUser(); - }, - } -}); -</script> - -<style lang="scss" scoped> -.aeakzknw { - > .avatar { - display: block; - margin: 0 auto; - width: 64px; - height: 64px; - } -} -</style> diff --git a/src/client/pages/instance/users.vue b/src/client/pages/instance/users.vue index 452886abde..2808b70fba 100644 --- a/src/client/pages/instance/users.vue +++ b/src/client/pages/instance/users.vue @@ -162,7 +162,7 @@ export default defineComponent({ }, show(user) { - os.pageWindow(`/instance/user/${user.id}`); + os.pageWindow(`/user-info/${user.id}`); }, acct diff --git a/src/client/pages/user-info.vue b/src/client/pages/user-info.vue index 378fbb7b50..090e8cdda8 100644 --- a/src/client/pages/user-info.vue +++ b/src/client/pages/user-info.vue @@ -1,35 +1,55 @@ <template> <FormBase> <FormSuspense :p="init"> + <div class="_formItem aeakzknw"> + <MkAvatar class="avatar" :user="user" :show-indicator="true"/> + </div> + + <FormLink :to="userPage(user)">Profile</FormLink> + <FormGroup> - <template #label><MkAcct :user="user"/></template> + <FormKeyValueView> + <template #key>Acct</template> + <template #value><span class="_monospace">{{ acct(user) }}</span></template> + </FormKeyValueView> <FormKeyValueView> <template #key>ID</template> <template #value><span class="_monospace">{{ user.id }}</span></template> </FormKeyValueView> - - <FormGroup> - <FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink> - - <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink> - <FormKeyValueView v-else> - <template #key>{{ $ts.instanceInfo }}</template> - <template #value>(Local user)</template> - </FormKeyValueView> - </FormGroup> - - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts.updatedAt }}</template> - <template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template> - </FormKeyValueView> - </FormGroup> - - <FormObjectView tall :value="user"> - <span>Raw</span> - </FormObjectView> </FormGroup> + + <FormGroup v-if="iAmModerator"> + <FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" @update:value="toggleModerator" v-model:value="moderator">{{ $ts.moderator }}</FormSwitch> + <FormSwitch @update:value="toggleSilence" v-model:value="silenced">{{ $ts.silence }}</FormSwitch> + <FormSwitch @update:value="toggleSuspend" v-model:value="suspended">{{ $ts.suspend }}</FormSwitch> + </FormGroup> + + <FormGroup> + <FormButton v-if="user.host != null" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton> + <FormButton v-if="user.host == null && iAmModerator" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton> + </FormGroup> + + <FormGroup> + <FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink> + + <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink> + <FormKeyValueView v-else> + <template #key>{{ $ts.instanceInfo }}</template> + <template #value>(Local user)</template> + </FormKeyValueView> + </FormGroup> + + <FormGroup> + <FormKeyValueView> + <template #key>{{ $ts.updatedAt }}</template> + <template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template> + </FormKeyValueView> + </FormGroup> + + <FormObjectView tall :value="user"> + <span>Raw</span> + </FormObjectView> </FormSuspense> </FormBase> </template> @@ -38,6 +58,7 @@ import { computed, defineAsyncComponent, defineComponent } from 'vue'; import FormObjectView from '@client/components/form/object-view.vue'; import FormTextarea from '@client/components/form/textarea.vue'; +import FormSwitch from '@client/components/form/switch.vue'; import FormLink from '@client/components/form/link.vue'; import FormBase from '@client/components/form/base.vue'; import FormGroup from '@client/components/form/group.vue'; @@ -49,11 +70,13 @@ import number from '@client/filters/number'; import bytes from '@client/filters/bytes'; import * as symbols from '@client/symbols'; import { url } from '@client/config'; +import { userPage, acct } from '@client/filters/user'; export default defineComponent({ components: { FormBase, FormTextarea, + FormSwitch, FormObjectView, FormButton, FormLink, @@ -72,7 +95,7 @@ export default defineComponent({ data() { return { [symbols.PAGE_INFO]: computed(() => ({ - title: this.$ts.userInfo, + title: this.user ? acct(this.user) : this.$ts.userInfo, icon: 'fas fa-info-circle', actions: this.user ? [this.user.url ? { text: this.user.url, @@ -84,17 +107,23 @@ export default defineComponent({ })), init: null, user: null, + info: null, + moderator: false, + silenced: false, + suspended: false, + } + }, + + computed: { + iAmModerator(): boolean { + return this.$i && (this.$i.isAdmin || this.$i.isModerator); } }, watch: { userId: { handler() { - this.init = () => os.api('users/show', { - userId: this.userId - }).then(user => { - this.user = user; - }); + this.init = this.createFetcher(); }, immediate: true } @@ -103,6 +132,114 @@ export default defineComponent({ methods: { number, bytes, + userPage, + acct, + + createFetcher() { + if (this.iAmModerator) { + return () => Promise.all([os.api('users/show', { + userId: this.userId + }), os.api('admin/show-user', { + userId: this.userId + })]).then(([user, info]) => { + this.user = user; + this.info = info; + this.moderator = this.info.isModerator; + this.silenced = this.info.isSilenced; + this.suspended = this.info.isSuspended; + }); + } else { + return () => os.api('users/show', { + userId: this.userId + }).then((user) => { + this.user = user; + }); + } + }, + + refreshUser() { + this.init = this.createFetcher(); + }, + + async updateRemoteUser() { + await os.apiWithDialog('admin/update-remote-user', { userId: this.user.id }); + this.refreshUser(); + }, + + async resetPassword() { + os.apiWithDialog('admin/reset-password', { + userId: this.user.id, + }, undefined, ({ password }) => { + os.dialog({ + type: 'success', + text: this.$t('newPasswordIs', { password }) + }); + }); + }, + + async toggleSilence(v) { + const confirm = await os.dialog({ + type: 'warning', + showCancelButton: true, + text: v ? this.$ts.silenceConfirm : this.$ts.unsilenceConfirm, + }); + if (confirm.canceled) { + this.silenced = !v; + } else { + await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id }); + await this.refreshUser(); + } + }, + + async toggleSuspend(v) { + const confirm = await os.dialog({ + type: 'warning', + showCancelButton: true, + text: v ? this.$ts.suspendConfirm : this.$ts.unsuspendConfirm, + }); + if (confirm.canceled) { + this.suspended = !v; + } else { + await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id }); + await this.refreshUser(); + } + }, + + async toggleModerator(v) { + await os.api(v ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: this.user.id }); + await this.refreshUser(); + }, + + async deleteAllFiles() { + const confirm = await os.dialog({ + type: 'warning', + showCancelButton: true, + text: this.$ts.deleteAllFilesConfirm, + }); + if (confirm.canceled) return; + const process = async () => { + await os.api('admin/delete-all-files-of-a-user', { userId: this.user.id }); + os.success(); + }; + await process().catch(e => { + os.dialog({ + type: 'error', + text: e.toString() + }); + }); + await this.refreshUser(); + }, } }); </script> + +<style lang="scss" scoped> +.aeakzknw { + > .avatar { + display: block; + margin: 0 auto; + width: 64px; + height: 64px; + } +} +</style> diff --git a/src/client/router.ts b/src/client/router.ts index 93de287ea5..26a4dac499 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -59,7 +59,6 @@ export const router = createRouter({ { path: '/my/antennas', component: page('my-antennas/index') }, { path: '/my/clips', component: page('my-clips/index') }, { path: '/scratchpad', component: page('scratchpad') }, - { path: '/instance/user/:user', component: page('instance/user'), props: route => ({ userId: route.params.user }) }, { path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) }, { path: '/instance', component: page('instance/index') }, { path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) }, diff --git a/src/client/scripts/get-user-menu.ts b/src/client/scripts/get-user-menu.ts index 9a003b5c38..ceb2bfe173 100644 --- a/src/client/scripts/get-user-menu.ts +++ b/src/client/scripts/get-user-menu.ts @@ -124,13 +124,7 @@ export function getUserMenu(user) { action: () => { copyToClipboard(`@${user.username}@${user.host || host}`); } - }, ($i && ($i.isAdmin || $i.isModerator)) ? { - icon: 'fas fa-info-circle', - text: i18n.locale.info, - action: () => { - os.pageWindow(`/instance/user/${user.id}`); - } - } : { + }, { icon: 'fas fa-info-circle', text: i18n.locale.info, action: () => { diff --git a/src/client/scripts/lookup-user.ts b/src/client/scripts/lookup-user.ts index 1bcfd8e9db..269777d874 100644 --- a/src/client/scripts/lookup-user.ts +++ b/src/client/scripts/lookup-user.ts @@ -10,7 +10,7 @@ export async function lookupUser() { if (canceled) return; const show = (user) => { - os.pageWindow(`/instance/user/${user.id}`); + os.pageWindow(`/user-info/${user.id}`); }; const usernamePromise = os.api('users/show', parseAcct(result));