詳細ユーザー情報ページなど
This commit is contained in:
		
							parent
							
								
									f32cad2667
								
							
						
					
					
						commit
						e5fbc68e0e
					
				
					 14 changed files with 614 additions and 16 deletions
				
			
		|  | @ -719,6 +719,9 @@ quitFullView: "フルビュー解除" | |||
| addDescription: "説明を追加" | ||||
| userPagePinTip: "個々のノートのメニューから「ピン留め」を選択することで、ここにノートを表示しておくことができます。" | ||||
| notSpecifiedMentionWarning: "宛先に含まれていないメンションがあります" | ||||
| info: "情報" | ||||
| userInfo: "ユーザー情報" | ||||
| unknown: "不明" | ||||
| 
 | ||||
| _email: | ||||
|   _follow: | ||||
|  |  | |||
|  | @ -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; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
|  |  | |||
|  | @ -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,19 +9,55 @@ | |||
| </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; | ||||
|  | @ -29,7 +65,7 @@ export default defineComponent({ | |||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			&:not(:first-child) { | ||||
| 			&:not(:first-child):not(._formNoConcatNext) { | ||||
| 				&._formPanel, ._formPanel { | ||||
| 					border-top: none; | ||||
| 					border-top-left-radius: 0; | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ export default defineComponent({ | |||
| 	padding: 14px 16px; | ||||
| 
 | ||||
| 	> .key { | ||||
| 		margin-right: 8px; | ||||
| 		margin-right: 12px; | ||||
| 	} | ||||
| 
 | ||||
| 	> .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: '/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') }, | ||||
|  |  | |||
|  | @ -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'; | ||||
|  | @ -126,6 +126,12 @@ export function getUserMenu(user) { | |||
| 		action: () => { | ||||
| 			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, | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -195,6 +195,7 @@ export class UserRepository extends Repository<User> { | |||
| 
 | ||||
| 			...(opts.detail ? { | ||||
| 				url: profile!.url, | ||||
| 				uri: user.uri, | ||||
| 				createdAt: user.createdAt.toISOString(), | ||||
| 				updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, | ||||
| 				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…
	
	Add table
		Add a link
		
	
		Reference in a new issue