summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-04-16 17:34:06 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-04-16 17:34:06 +0900
commite5fbc68e0e0b06cc620a7cb2494d6c03139d9627 (patch)
tree5d57ac602d9f169a6af7d85f5e8e87c4e0652390 /src/client
parentTweak UI (diff)
downloadmisskey-e5fbc68e0e0b06cc620a7cb2494d6c03139d9627.tar.gz
misskey-e5fbc68e0e0b06cc620a7cb2494d6c03139d9627.tar.bz2
misskey-e5fbc68e0e0b06cc620a7cb2494d6c03139d9627.zip
詳細ユーザー情報ページなど
Diffstat (limited to 'src/client')
-rw-r--r--src/client/components/form/base.vue10
-rw-r--r--src/client/components/form/group.vue52
-rw-r--r--src/client/components/form/key-value-view.vue2
-rw-r--r--src/client/components/form/object-view.vue102
-rw-r--r--src/client/components/form/suspense.vue76
-rw-r--r--src/client/pages/instance-info.vue124
-rw-r--r--src/client/pages/user-ap-info.vue122
-rw-r--r--src/client/pages/user-info.vue87
-rw-r--r--src/client/router.ts3
-rw-r--r--src/client/scripts/get-user-menu.ts8
-rw-r--r--src/client/style.scss2
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 {