forked from FoundKeyGang/FoundKey
詳細ユーザー情報ページなど
This commit is contained in:
parent
f32cad2667
commit
e5fbc68e0e
14 changed files with 614 additions and 16 deletions
|
@ -719,6 +719,9 @@ quitFullView: "フルビュー解除"
|
||||||
addDescription: "説明を追加"
|
addDescription: "説明を追加"
|
||||||
userPagePinTip: "個々のノートのメニューから「ピン留め」を選択することで、ここにノートを表示しておくことができます。"
|
userPagePinTip: "個々のノートのメニューから「ピン留め」を選択することで、ここにノートを表示しておくことができます。"
|
||||||
notSpecifiedMentionWarning: "宛先に含まれていないメンションがあります"
|
notSpecifiedMentionWarning: "宛先に含まれていないメンションがあります"
|
||||||
|
info: "情報"
|
||||||
|
userInfo: "ユーザー情報"
|
||||||
|
unknown: "不明"
|
||||||
|
|
||||||
_email:
|
_email:
|
||||||
_follow:
|
_follow:
|
||||||
|
|
|
@ -40,16 +40,16 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
._form_group {
|
._form_group {
|
||||||
> * {
|
> *:not(._formNoConcat) {
|
||||||
&:not(:first-child) {
|
&:not(:last-child):not(._formNoConcatPrev) {
|
||||||
&._formPanel, ._formPanel {
|
&._formPanel, ._formPanel {
|
||||||
border-top: none;
|
border-bottom: solid 0.5px var(--divider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:first-child):not(._formNoConcatNext) {
|
||||||
&._formPanel, ._formPanel {
|
&._formPanel, ._formPanel {
|
||||||
border-bottom: solid 0.5px var(--divider);
|
border-top: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<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="_formLabel"><slot name="label"></slot></div>
|
||||||
<div class="main _form_group">
|
<div class="main _form_group" ref="child">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="_formCaption"><slot name="caption"></slot></div>
|
<div class="_formCaption"><slot name="caption"></slot></div>
|
||||||
|
@ -9,27 +9,63 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.vrtktovg {
|
.vrtktovg {
|
||||||
> .main {
|
> .main {
|
||||||
> ::v-deep(*) {
|
> ::v-deep(*):not(._formNoConcat) {
|
||||||
margin: 0;
|
&:not(._formNoConcatNext) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child):not(._formNoConcatPrev) {
|
||||||
&._formPanel, ._formPanel {
|
&._formPanel, ._formPanel {
|
||||||
border-bottom: solid 0.5px var(--divider);
|
border-bottom: solid 0.5px var(--divider);
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:first-child) {
|
&:not(:first-child):not(._formNoConcatNext) {
|
||||||
&._formPanel, ._formPanel {
|
&._formPanel, ._formPanel {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
|
|
|
@ -23,7 +23,7 @@ export default defineComponent({
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
|
|
||||||
> .key {
|
> .key {
|
||||||
margin-right: 8px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .value {
|
> .value {
|
||||||
|
|
102
src/client/components/form/object-view.vue
Normal file
102
src/client/components/form/object-view.vue
Normal file
|
@ -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>
|
76
src/client/components/form/suspense.vue
Normal file
76
src/client/components/form/suspense.vue
Normal file
|
@ -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>
|
124
src/client/pages/instance-info.vue
Normal file
124
src/client/pages/instance-info.vue
Normal file
|
@ -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>
|
122
src/client/pages/user-ap-info.vue
Normal file
122
src/client/pages/user-ap-info.vue
Normal file
|
@ -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>
|
87
src/client/pages/user-info.vue
Normal file
87
src/client/pages/user-info.vue
Normal file
|
@ -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>
|
|
@ -72,6 +72,9 @@ export const router = createRouter({
|
||||||
{ path: '/instance/abuses', component: page('instance/abuses') },
|
{ path: '/instance/abuses', component: page('instance/abuses') },
|
||||||
{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
|
{ 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: '/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', component: page('reversi/index') },
|
||||||
{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },
|
{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },
|
||||||
{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') },
|
{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') },
|
||||||
|
|
|
@ -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 { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { i18n } from '@client/i18n';
|
import { i18n } from '@client/i18n';
|
||||||
import copyToClipboard from '@client/scripts/copy-to-clipboard';
|
import copyToClipboard from '@client/scripts/copy-to-clipboard';
|
||||||
|
@ -126,6 +126,12 @@ export function getUserMenu(user) {
|
||||||
action: () => {
|
action: () => {
|
||||||
copyToClipboard(`@${user.username}@${user.host || host}`);
|
copyToClipboard(`@${user.username}@${user.host || host}`);
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
icon: faInfoCircle,
|
||||||
|
text: i18n.locale.info,
|
||||||
|
action: () => {
|
||||||
|
os.pageWindow(`/user-info/${user.id}`);
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
icon: faEnvelope,
|
icon: faEnvelope,
|
||||||
text: i18n.locale.sendMessage,
|
text: i18n.locale.sendMessage,
|
||||||
|
|
|
@ -455,7 +455,7 @@ hr {
|
||||||
}
|
}
|
||||||
|
|
||||||
._monospace {
|
._monospace {
|
||||||
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
|
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
._code {
|
._code {
|
||||||
|
|
|
@ -195,6 +195,7 @@ export class UserRepository extends Repository<User> {
|
||||||
|
|
||||||
...(opts.detail ? {
|
...(opts.detail ? {
|
||||||
url: profile!.url,
|
url: profile!.url,
|
||||||
|
uri: user.uri,
|
||||||
createdAt: user.createdAt.toISOString(),
|
createdAt: user.createdAt.toISOString(),
|
||||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||||
bannerUrl: user.bannerUrl,
|
bannerUrl: user.bannerUrl,
|
||||||
|
|
38
src/server/api/endpoints/ap/get.ts
Normal file
38
src/server/api/endpoints/ap/get.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import $ from 'cafy';
|
||||||
|
import define from '../../define';
|
||||||
|
import Resolver from '../../../../remote/activitypub/resolver';
|
||||||
|
import { ApiError } from '../../error';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['federation'],
|
||||||
|
|
||||||
|
desc: {
|
||||||
|
'ja-JP': 'URIを指定してActivityPubオブジェクトを参照します。',
|
||||||
|
'en-US': 'Browse to the ActivityPub object by specifying the URI.'
|
||||||
|
},
|
||||||
|
|
||||||
|
requireCredential: false as const,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
uri: {
|
||||||
|
validator: $.str,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': 'ActivityPubオブジェクトのURI'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
},
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'object' as const,
|
||||||
|
optional: false as const, nullable: false as const,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async (ps) => {
|
||||||
|
const resolver = new Resolver();
|
||||||
|
const object = await resolver.resolve(ps.uri);
|
||||||
|
return object;
|
||||||
|
});
|
Loading…
Reference in a new issue