diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-04-16 17:34:06 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2021-04-16 17:34:06 +0900 |
| commit | e5fbc68e0e0b06cc620a7cb2494d6c03139d9627 (patch) | |
| tree | 5d57ac602d9f169a6af7d85f5e8e87c4e0652390 /src/client | |
| parent | Tweak UI (diff) | |
| download | misskey-e5fbc68e0e0b06cc620a7cb2494d6c03139d9627.tar.gz misskey-e5fbc68e0e0b06cc620a7cb2494d6c03139d9627.tar.bz2 misskey-e5fbc68e0e0b06cc620a7cb2494d6c03139d9627.zip | |
詳細ユーザー情報ページなど
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/components/form/base.vue | 10 | ||||
| -rw-r--r-- | src/client/components/form/group.vue | 52 | ||||
| -rw-r--r-- | src/client/components/form/key-value-view.vue | 2 | ||||
| -rw-r--r-- | src/client/components/form/object-view.vue | 102 | ||||
| -rw-r--r-- | src/client/components/form/suspense.vue | 76 | ||||
| -rw-r--r-- | src/client/pages/instance-info.vue | 124 | ||||
| -rw-r--r-- | src/client/pages/user-ap-info.vue | 122 | ||||
| -rw-r--r-- | src/client/pages/user-info.vue | 87 | ||||
| -rw-r--r-- | src/client/router.ts | 3 | ||||
| -rw-r--r-- | src/client/scripts/get-user-menu.ts | 8 | ||||
| -rw-r--r-- | src/client/style.scss | 2 |
11 files changed, 572 insertions, 16 deletions
diff --git a/src/client/components/form/base.vue b/src/client/components/form/base.vue index 84438a5b32..de46d1bd19 100644 --- a/src/client/components/form/base.vue +++ b/src/client/components/form/base.vue @@ -40,16 +40,16 @@ export default defineComponent({ } ._form_group { - > * { - &:not(:first-child) { + > *:not(._formNoConcat) { + &:not(:last-child):not(._formNoConcatPrev) { &._formPanel, ._formPanel { - border-top: none; + border-bottom: solid 0.5px var(--divider); } } - &:not(:last-child) { + &:not(:first-child):not(._formNoConcatNext) { &._formPanel, ._formPanel { - border-bottom: solid 0.5px var(--divider); + border-top: none; } } } diff --git a/src/client/components/form/group.vue b/src/client/components/form/group.vue index 9af33013a1..34ccaeff07 100644 --- a/src/client/components/form/group.vue +++ b/src/client/components/form/group.vue @@ -1,7 +1,7 @@ <template> -<div class="vrtktovg _formItem" v-size="{ max: [500] }" v-sticky-container> +<div class="vrtktovg _formItem _formNoConcat" v-size="{ max: [500] }" v-sticky-container> <div class="_formLabel"><slot name="label"></slot></div> - <div class="main _form_group"> + <div class="main _form_group" ref="child"> <slot></slot> </div> <div class="_formCaption"><slot name="caption"></slot></div> @@ -9,27 +9,63 @@ </template> <script lang="ts"> -import { defineComponent } from 'vue'; +import { defineComponent, onMounted, ref } from 'vue'; export default defineComponent({ + setup(props, context) { + const child = ref<HTMLElement | null>(null); + + const scanChild = () => { + if (child.value == null) return; + const els = Array.from(child.value.children); + for (let i = 0; i < els.length; i++) { + const el = els[i]; + if (el.classList.contains('_formNoConcat')) { + if (els[i - 1]) els[i - 1].classList.add('_formNoConcatPrev'); + if (els[i + 1]) els[i + 1].classList.add('_formNoConcatNext'); + } + } + }; + + onMounted(() => { + scanChild(); + + const observer = new MutationObserver(records => { + scanChild(); + }); + + observer.observe(child.value, { + childList: true, + subtree: false, + attributes: false, + characterData: false, + }); + }); + + return { + child + }; + } }); </script> <style lang="scss" scoped> .vrtktovg { > .main { - > ::v-deep(*) { - margin: 0; + > ::v-deep(*):not(._formNoConcat) { + &:not(._formNoConcatNext) { + margin: 0; + } - &:not(:last-child) { + &:not(:last-child):not(._formNoConcatPrev) { &._formPanel, ._formPanel { border-bottom: solid 0.5px var(--divider); border-bottom-left-radius: 0; border-bottom-right-radius: 0; } } - - &:not(:first-child) { + + &:not(:first-child):not(._formNoConcatNext) { &._formPanel, ._formPanel { border-top: none; border-top-left-radius: 0; diff --git a/src/client/components/form/key-value-view.vue b/src/client/components/form/key-value-view.vue index 75627c6537..ebe9b6d049 100644 --- a/src/client/components/form/key-value-view.vue +++ b/src/client/components/form/key-value-view.vue @@ -23,7 +23,7 @@ export default defineComponent({ padding: 14px 16px; > .key { - margin-right: 8px; + margin-right: 12px; } > .value { diff --git a/src/client/components/form/object-view.vue b/src/client/components/form/object-view.vue new file mode 100644 index 0000000000..cbd4186e56 --- /dev/null +++ b/src/client/components/form/object-view.vue @@ -0,0 +1,102 @@ +<template> +<FormGroup class="_formItem"> + <template #label><slot></slot></template> + <div class="drooglns _formItem" :class="{ tall }"> + <div class="input _formPanel"> + <textarea class="_monospace" + v-model="v" + readonly + :spellcheck="false" + ></textarea> + </div> + </div> + <template #caption><slot name="desc"></slot></template> +</FormGroup> +</template> + +<script lang="ts"> +import { defineComponent, ref, toRefs, watch } from 'vue'; +import * as JSON5 from 'json5'; +import './form.scss'; +import FormGroup from './group.vue'; + +export default defineComponent({ + components: { + FormGroup, + }, + props: { + value: { + required: false + }, + tall: { + type: Boolean, + required: false, + default: false + }, + pre: { + type: Boolean, + required: false, + default: false + }, + manualSave: { + type: Boolean, + required: false, + default: false + }, + }, + setup(props, context) { + const { value } = toRefs(props); + const v = ref(''); + + watch(() => value, newValue => { + v.value = JSON5.stringify(newValue.value, null, '\t'); + }, { + immediate: true + }); + + return { + v, + }; + } +}); +</script> + +<style lang="scss" scoped> +.drooglns { + position: relative; + + > .input { + position: relative; + + > textarea { + display: block; + width: 100%; + min-width: 100%; + max-width: 100%; + min-height: 130px; + margin: 0; + padding: 16px; + box-sizing: border-box; + font: inherit; + font-weight: normal; + font-size: 1em; + background: transparent; + border: none; + border-radius: 0; + outline: none; + box-shadow: none; + color: var(--fg); + tab-size: 2; + white-space: pre; + } + } + + &.tall { + > .input { + > textarea { + min-height: 200px; + } + } + } +} +</style> diff --git a/src/client/components/form/suspense.vue b/src/client/components/form/suspense.vue new file mode 100644 index 0000000000..4b47cb959b --- /dev/null +++ b/src/client/components/form/suspense.vue @@ -0,0 +1,76 @@ +<template> +<div class="_formItem" v-if="pending"> + <div class="_formPanel"> + pending + </div> +</div> +<slot v-else-if="resolved" :result="result"></slot> +<div class="_formItem" v-else> + <div class="_formPanel"> + error! + <button @click="retry">retry</button> + </div> +</div> +</template> + +<script lang="ts"> +import { defineComponent, PropType, ref, watch } from 'vue'; +import './form.scss'; + +export default defineComponent({ + props: { + p: { + type: Function as PropType<() => Promise<any>>, + required: true, + } + }, + + setup(props, context) { + const pending = ref(true); + const resolved = ref(false); + const rejected = ref(false); + const result = ref(null); + + const process = () => { + if (props.p == null) { + return; + } + const promise = props.p(); + pending.value = true; + resolved.value = false; + rejected.value = false; + promise.then((_result) => { + pending.value = false; + resolved.value = true; + result.value = _result; + }); + promise.catch(() => { + pending.value = false; + rejected.value = true; + }); + }; + + watch(() => props.p, () => { + process(); + }, { + immediate: true + }); + + const retry = () => { + process(); + }; + + return { + pending, + resolved, + rejected, + result, + retry, + }; + } +}); +</script> + +<style lang="scss" scoped> + +</style> diff --git a/src/client/pages/instance-info.vue b/src/client/pages/instance-info.vue new file mode 100644 index 0000000000..420ecc31b8 --- /dev/null +++ b/src/client/pages/instance-info.vue @@ -0,0 +1,124 @@ +<template> +<FormBase> + <FormGroup v-if="instance"> + <template #label>{{ instance.host }}</template> + <FormKeyValueView> + <template #key>Name</template> + <template #value><span class="_monospace">{{ instance.name || `(${$ts.unknown})` }}</span></template> + </FormKeyValueView> + + <FormGroup> + <FormKeyValueView> + <template #key>Software Name</template> + <template #value><span class="_monospace">{{ instance.softwareName || `(${$ts.unknown})` }}</span></template> + </FormKeyValueView> + <FormKeyValueView> + <template #key>Software Version</template> + <template #value><span class="_monospace">{{ instance.softwareVersion || `(${$ts.unknown})` }}</span></template> + </FormKeyValueView> + </FormGroup> + <FormGroup> + <FormKeyValueView> + <template #key>Maintainer Name</template> + <template #value><span class="_monospace">{{ instance.maintainerName || `(${$ts.unknown})` }}</span></template> + </FormKeyValueView> + <FormKeyValueView> + <template #key>Maintainer Contact</template> + <template #value><span class="_monospace">{{ instance.maintainerEmail || `(${$ts.unknown})` }}</span></template> + </FormKeyValueView> + </FormGroup> + <FormGroup> + <FormKeyValueView> + <template #key>{{ $ts.latestRequestSentAt }}</template> + <template #value><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></template> + </FormKeyValueView> + <FormKeyValueView> + <template #key>{{ $ts.latestStatus }}</template> + <template #value>{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</template> + </FormKeyValueView> + <FormKeyValueView> + <template #key>{{ $ts.latestRequestReceivedAt }}</template> + <template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template> + </FormKeyValueView> + </FormGroup> + <FormGroup> + <FormKeyValueView> + <template #key>Open Registrations</template> + <template #value>{{ instance.openRegistrations ? $ts.yes : $ts.no }}</template> + </FormKeyValueView> + </FormGroup> + <FormGroup> + <FormKeyValueView> + <template #key>{{ $ts.registeredAt }}</template> + <template #value><MkTime mode="detail" :time="instance.caughtAt"/></template> + </FormKeyValueView> + </FormGroup> + <FormObjectView tall :value="instance"> + <span>Raw</span> + </FormObjectView> + </FormGroup> +</FormBase> +</template> + +<script lang="ts"> +import { defineAsyncComponent, defineComponent } from 'vue'; +import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; +import FormObjectView from '@client/components/form/object-view.vue'; +import FormTextarea from '@client/components/form/textarea.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'; + +export default defineComponent({ + components: { + FormBase, + FormTextarea, + FormObjectView, + FormButton, + FormLink, + FormGroup, + FormKeyValueView, + FormSuspense, + }, + + props: { + host: { + type: String, + required: true + } + }, + + data() { + return { + [symbols.PAGE_INFO]: { + title: this.$ts.instanceInfo, + icon: faInfoCircle + }, + instance: null, + } + }, + + mounted() { + this.fetch(); + }, + + methods: { + number, + bytes, + + async fetch() { + this.instance = await os.api('federation/show-instance', { + host: this.host + }); + } + } +}); +</script> diff --git a/src/client/pages/user-ap-info.vue b/src/client/pages/user-ap-info.vue new file mode 100644 index 0000000000..d86437830d --- /dev/null +++ b/src/client/pages/user-ap-info.vue @@ -0,0 +1,122 @@ +<template> +<FormBase> + <FormGroup> + <template #label>ActivityPub</template> + <FormSuspense :p="apPromiseFactory" v-slot="{ result: ap }"> + <FormKeyValueView> + <template #key>Type</template> + <template #value><span class="_monospace">{{ ap.type }}</span></template> + </FormKeyValueView> + <FormKeyValueView> + <template #key>URI</template> + <template #value><span class="_monospace">{{ ap.id }}</span></template> + </FormKeyValueView> + <FormKeyValueView> + <template #key>URL</template> + <template #value><span class="_monospace">{{ ap.url }}</span></template> + </FormKeyValueView> + <FormGroup> + <FormKeyValueView> + <template #key>Inbox</template> + <template #value><span class="_monospace">{{ ap.inbox }}</span></template> + </FormKeyValueView> + <FormKeyValueView> + <template #key>Shared Inbox</template> + <template #value><span class="_monospace">{{ ap.sharedInbox }}</span></template> + </FormKeyValueView> + <FormKeyValueView> + <template #key>Outbox</template> + <template #value><span class="_monospace">{{ ap.outbox }}</span></template> + </FormKeyValueView> + </FormGroup> + <FormTextarea readonly tall code pre :value="ap.publicKey.publicKeyPem"> + <span>Public Key</span> + </FormTextarea> + <FormKeyValueView> + <template #key>Discoverable</template> + <template #value>{{ ap.discoverable ? $ts.yes : $ts.no }}</template> + </FormKeyValueView> + <FormKeyValueView> + <template #key>ManuallyApprovesFollowers</template> + <template #value>{{ ap.manuallyApprovesFollowers ? $ts.yes : $ts.no }}</template> + </FormKeyValueView> + <FormObjectView tall :value="ap"> + <span>Raw</span> + </FormObjectView> + <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> + </FormSuspense> + </FormGroup> +</FormBase> +</template> + +<script lang="ts"> +import { defineAsyncComponent, defineComponent } from 'vue'; +import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; +import FormObjectView from '@client/components/form/object-view.vue'; +import FormTextarea from '@client/components/form/textarea.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'; + +export default defineComponent({ + components: { + FormBase, + FormTextarea, + FormObjectView, + FormButton, + FormLink, + FormGroup, + FormKeyValueView, + FormSuspense, + }, + + props: { + userId: { + type: String, + required: true + } + }, + + data() { + return { + [symbols.PAGE_INFO]: { + title: this.$ts.userInfo, + icon: faInfoCircle + }, + user: null, + apPromiseFactory: null, + } + }, + + mounted() { + this.fetch(); + }, + + methods: { + number, + bytes, + + async fetch() { + this.user = await os.api('users/show', { + userId: this.userId + }); + + this.apPromiseFactory = () => os.api('ap/get', { + uri: this.user.uri || `${url}/users/${this.user.id}` + }); + } + } +}); +</script> diff --git a/src/client/pages/user-info.vue b/src/client/pages/user-info.vue new file mode 100644 index 0000000000..a1ff561060 --- /dev/null +++ b/src/client/pages/user-info.vue @@ -0,0 +1,87 @@ +<template> +<FormBase> + <template v-if="user"> + <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> + + <FormObjectView tall :value="user"> + <span>Raw</span> + </FormObjectView> + </template> +</FormBase> +</template> + +<script lang="ts"> +import { defineAsyncComponent, defineComponent } from 'vue'; +import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; +import FormObjectView from '@client/components/form/object-view.vue'; +import FormTextarea from '@client/components/form/textarea.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'; + +export default defineComponent({ + components: { + FormBase, + FormTextarea, + FormObjectView, + FormButton, + FormLink, + FormGroup, + FormKeyValueView, + FormSuspense, + }, + + props: { + userId: { + type: String, + required: true + } + }, + + data() { + return { + [symbols.PAGE_INFO]: { + title: this.$ts.userInfo, + icon: faInfoCircle + }, + user: null, + } + }, + + mounted() { + this.fetch(); + }, + + methods: { + number, + bytes, + + async fetch() { + this.user = await os.api('users/show', { + userId: this.userId + }); + } + } +}); +</script> diff --git a/src/client/router.ts b/src/client/router.ts index 3effb2edbe..bf45c806e2 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -72,6 +72,9 @@ export const router = createRouter({ { path: '/instance/abuses', component: page('instance/abuses') }, { path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) }, { path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) }, + { path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) }, + { path: '/user-ap-info/:user', component: page('user-ap-info'), props: route => ({ userId: route.params.user }) }, + { path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) }, { path: '/games/reversi', component: page('reversi/index') }, { path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) }, { path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') }, diff --git a/src/client/scripts/get-user-menu.ts b/src/client/scripts/get-user-menu.ts index 163eff619c..0496e87502 100644 --- a/src/client/scripts/get-user-menu.ts +++ b/src/client/scripts/get-user-menu.ts @@ -1,4 +1,4 @@ -import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash, faPlug, faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; +import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash, faPlug, faExclamationCircle, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons'; import { i18n } from '@client/i18n'; import copyToClipboard from '@client/scripts/copy-to-clipboard'; @@ -127,6 +127,12 @@ export function getUserMenu(user) { copyToClipboard(`@${user.username}@${user.host || host}`); } }, { + icon: faInfoCircle, + text: i18n.locale.info, + action: () => { + os.pageWindow(`/user-info/${user.id}`); + } + }, { icon: faEnvelope, text: i18n.locale.sendMessage, action: () => { diff --git a/src/client/style.scss b/src/client/style.scss index b12299422c..eadf56bf37 100644 --- a/src/client/style.scss +++ b/src/client/style.scss @@ -455,7 +455,7 @@ hr { } ._monospace { - font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; + font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important; } ._code { |