Merge branch 'misskey-dev:develop' into guest-button
This commit is contained in:
		
						commit
						3e5bd701d3
					
				
					 17 changed files with 889 additions and 727 deletions
				
			
		|  | @ -11,6 +11,11 @@ You should also include the user name that made the change. | ||||||
| 
 | 
 | ||||||
| ## 12.x.x (unreleased) | ## 12.x.x (unreleased) | ||||||
| 
 | 
 | ||||||
|  | ### Changes | ||||||
|  | - ハイライトがみつけるに統合されました | ||||||
|  | - カスタム絵文字ページはインスタンス情報ページに統合されました | ||||||
|  | - 連合ページはインスタンス情報ページに統合されました | ||||||
|  | 
 | ||||||
| ### Improvements | ### Improvements | ||||||
| - Server: Allow GET method for some endpoints @syuilo | - Server: Allow GET method for some endpoints @syuilo | ||||||
| - Server: Add rate limit to i/notifications @tamaina | - Server: Add rate limit to i/notifications @tamaina | ||||||
|  | @ -19,6 +24,7 @@ You should also include the user name that made the change. | ||||||
| - Client: Add instance-cloud widget @syuilo | - Client: Add instance-cloud widget @syuilo | ||||||
| - Client: Add rss-marquee widget @syuilo | - Client: Add rss-marquee widget @syuilo | ||||||
| - Client: Removing entries from a clip @futchitwo | - Client: Removing entries from a clip @futchitwo | ||||||
|  | - Client: Poll highlights in explore page @syuilo | ||||||
| - Make possible to delete an account by admin @syuilo | - Make possible to delete an account by admin @syuilo | ||||||
| - Improve player detection in URL preview @mei23 | - Improve player detection in URL preview @mei23 | ||||||
| - Add Badge Image to Push Notification #8012 @tamaina | - Add Badge Image to Push Notification #8012 @tamaina | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <XModalWindow ref="dialog" | <XModalWindow | ||||||
|  | 	ref="dialog" | ||||||
| 	:width="450" | 	:width="450" | ||||||
| 	:can-close="false" | 	:can-close="false" | ||||||
| 	:with-ok-button="true" | 	:with-ok-button="true" | ||||||
|  | @ -37,7 +38,7 @@ | ||||||
| 					<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option> | 					<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option> | ||||||
| 				</FormSelect> | 				</FormSelect> | ||||||
| 				<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]" class="_formBlock"> | 				<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]" class="_formBlock"> | ||||||
| 					<template #caption><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> | 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> | ||||||
| 					<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option> | 					<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option> | ||||||
| 				</FormRadios> | 				</FormRadios> | ||||||
| 				<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock"> | 				<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock"> | ||||||
|  | @ -55,7 +56,6 @@ | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import XModalWindow from '@/components/ui/modal-window.vue'; |  | ||||||
| import FormInput from './form/input.vue'; | import FormInput from './form/input.vue'; | ||||||
| import FormTextarea from './form/textarea.vue'; | import FormTextarea from './form/textarea.vue'; | ||||||
| import FormSwitch from './form/switch.vue'; | import FormSwitch from './form/switch.vue'; | ||||||
|  | @ -63,6 +63,7 @@ import FormSelect from './form/select.vue'; | ||||||
| import FormRange from './form/range.vue'; | import FormRange from './form/range.vue'; | ||||||
| import MkButton from './ui/button.vue'; | import MkButton from './ui/button.vue'; | ||||||
| import FormRadios from './form/radios.vue'; | import FormRadios from './form/radios.vue'; | ||||||
|  | import XModalWindow from '@/components/ui/modal-window.vue'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
|  | @ -91,7 +92,7 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			values: {} | 			values: {}, | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -104,18 +105,18 @@ export default defineComponent({ | ||||||
| 	methods: { | 	methods: { | ||||||
| 		ok() { | 		ok() { | ||||||
| 			this.$emit('done', { | 			this.$emit('done', { | ||||||
| 				result: this.values | 				result: this.values, | ||||||
| 			}); | 			}); | ||||||
| 			this.$refs.dialog.close(); | 			this.$refs.dialog.close(); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		cancel() { | 		cancel() { | ||||||
| 			this.$emit('done', { | 			this.$emit('done', { | ||||||
| 				canceled: true | 				canceled: true, | ||||||
| 			}); | 			}); | ||||||
| 			this.$refs.dialog.close(); | 			this.$refs.dialog.close(); | ||||||
| 		} | 		}, | ||||||
| 	} | 	}, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed, onMounted, onUnmounted, ref, inject, watch } from 'vue'; | import { computed, onMounted, onUnmounted, ref, inject, watch, shallowReactive, nextTick, reactive } from 'vue'; | ||||||
| import tinycolor from 'tinycolor2'; | import tinycolor from 'tinycolor2'; | ||||||
| import { popupMenu } from '@/os'; | import { popupMenu } from '@/os'; | ||||||
| import { scrollToTop } from '@/scripts/scroll'; | import { scrollToTop } from '@/scripts/scroll'; | ||||||
|  | @ -137,16 +137,18 @@ onMounted(() => { | ||||||
| 	calcBg(); | 	calcBg(); | ||||||
| 	globalEvents.on('themeChanged', calcBg); | 	globalEvents.on('themeChanged', calcBg); | ||||||
| 
 | 
 | ||||||
| 	watch(() => props.tab, () => { | 	watch(() => [props.tab, props.tabs], () => { | ||||||
| 		const tabEl = tabRefs[props.tab]; | 		nextTick(() => { | ||||||
| 		if (tabEl && tabHighlightEl) { | 			const tabEl = tabRefs[props.tab]; | ||||||
| 			// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある | 			if (tabEl && tabHighlightEl) { | ||||||
| 			// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4 | 				// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある | ||||||
| 			const parentRect = tabEl.parentElement.getBoundingClientRect(); | 				// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4 | ||||||
| 			const rect = tabEl.getBoundingClientRect(); | 				const parentRect = tabEl.parentElement.getBoundingClientRect(); | ||||||
| 			tabHighlightEl.style.width = rect.width + 'px'; | 				const rect = tabEl.getBoundingClientRect(); | ||||||
| 			tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px'; | 				tabHighlightEl.style.width = rect.width + 'px'; | ||||||
| 		} | 				tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px'; | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
| 	}, { | 	}, { | ||||||
| 		immediate: true, | 		immediate: true, | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
|  | @ -119,6 +119,7 @@ function createDoughnut(chartEl, tooltip, data) { | ||||||
| 			}], | 			}], | ||||||
| 		}, | 		}, | ||||||
| 		options: { | 		options: { | ||||||
|  | 			maintainAspectRatio: false, | ||||||
| 			layout: { | 			layout: { | ||||||
| 				padding: { | 				padding: { | ||||||
| 					left: 16, | 					left: 16, | ||||||
|  | @ -195,10 +196,13 @@ onMounted(() => { | ||||||
| 		gap: 16px; | 		gap: 16px; | ||||||
| 
 | 
 | ||||||
| 		> .sub, > .pub { | 		> .sub, > .pub { | ||||||
|  | 			flex: 1; | ||||||
|  | 			min-width: 0; | ||||||
| 			position: relative; | 			position: relative; | ||||||
| 			background: var(--panel); | 			background: var(--panel); | ||||||
| 			border-radius: var(--radius); | 			border-radius: var(--radius); | ||||||
| 			padding: 24px; | 			padding: 24px; | ||||||
|  | 			max-height: 300px; | ||||||
| 
 | 
 | ||||||
| 			> .title { | 			> .title { | ||||||
| 				position: absolute; | 				position: absolute; | ||||||
|  | @ -206,6 +210,10 @@ onMounted(() => { | ||||||
| 				left: 24px; | 				left: 24px; | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		@media (max-width: 600px) { | ||||||
|  | 			flex-direction: column; | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -143,7 +143,6 @@ function onContextmenu(ev: MouseEvent) { | ||||||
| 		background: var(--windowHeader); | 		background: var(--windowHeader); | ||||||
| 		-webkit-backdrop-filter: var(--blur, blur(15px)); | 		-webkit-backdrop-filter: var(--blur, blur(15px)); | ||||||
| 		backdrop-filter: var(--blur, blur(15px)); | 		backdrop-filter: var(--blur, blur(15px)); | ||||||
| 		box-shadow: 0px 1px var(--divider); |  | ||||||
| 
 | 
 | ||||||
| 		> button { | 		> button { | ||||||
| 			height: $height; | 			height: $height; | ||||||
|  |  | ||||||
|  | @ -105,7 +105,6 @@ defineExpose({ | ||||||
| 		background: var(--windowHeader); | 		background: var(--windowHeader); | ||||||
| 		-webkit-backdrop-filter: var(--blur, blur(15px)); | 		-webkit-backdrop-filter: var(--blur, blur(15px)); | ||||||
| 		backdrop-filter: var(--blur, blur(15px)); | 		backdrop-filter: var(--blur, blur(15px)); | ||||||
| 		box-shadow: 0px 1px var(--divider); |  | ||||||
| 
 | 
 | ||||||
| 		> button { | 		> button { | ||||||
| 			height: $height; | 			height: $height; | ||||||
|  |  | ||||||
|  | @ -35,11 +35,6 @@ export const menuDef = reactive({ | ||||||
| 		indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest), | 		indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest), | ||||||
| 		to: '/my/follow-requests', | 		to: '/my/follow-requests', | ||||||
| 	}, | 	}, | ||||||
| 	featured: { |  | ||||||
| 		title: 'featured', |  | ||||||
| 		icon: 'fas fa-fire-alt', |  | ||||||
| 		to: '/featured', |  | ||||||
| 	}, |  | ||||||
| 	explore: { | 	explore: { | ||||||
| 		title: 'explore', | 		title: 'explore', | ||||||
| 		icon: 'fas fa-hashtag', | 		icon: 'fas fa-hashtag', | ||||||
|  |  | ||||||
|  | @ -28,7 +28,7 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed, onMounted, onUnmounted, ref, inject, watch } from 'vue'; | import { computed, onMounted, onUnmounted, ref, inject, watch, nextTick } from 'vue'; | ||||||
| import tinycolor from 'tinycolor2'; | import tinycolor from 'tinycolor2'; | ||||||
| import { popupMenu } from '@/os'; | import { popupMenu } from '@/os'; | ||||||
| import { url } from '@/config'; | import { url } from '@/config'; | ||||||
|  | @ -126,16 +126,18 @@ onMounted(() => { | ||||||
| 	calcBg(); | 	calcBg(); | ||||||
| 	globalEvents.on('themeChanged', calcBg); | 	globalEvents.on('themeChanged', calcBg); | ||||||
| 
 | 
 | ||||||
| 	watch(() => props.tab, () => { | 	watch(() => [props.tab, props.tabs], () => { | ||||||
| 		const tabEl = tabRefs[props.tab]; | 		nextTick(() => { | ||||||
| 		if (tabEl && tabHighlightEl) { | 			const tabEl = tabRefs[props.tab]; | ||||||
| 			// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある | 			if (tabEl && tabHighlightEl) { | ||||||
| 			// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4 | 				// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある | ||||||
| 			const parentRect = tabEl.parentElement.getBoundingClientRect(); | 				// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4 | ||||||
| 			const rect = tabEl.getBoundingClientRect(); | 				const parentRect = tabEl.parentElement.getBoundingClientRect(); | ||||||
| 			tabHighlightEl.style.width = rect.width + 'px'; | 				const rect = tabEl.getBoundingClientRect(); | ||||||
| 			tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px'; | 				tabHighlightEl.style.width = rect.width + 'px'; | ||||||
| 		} | 				tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px'; | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
| 	}, { | 	}, { | ||||||
| 		immediate: true, | 		immediate: true, | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
							
								
								
									
										30
									
								
								packages/client/src/pages/explore.featured.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								packages/client/src/pages/explore.featured.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | ||||||
|  | <template> | ||||||
|  | <MkSpacer :content-max="800"> | ||||||
|  | 	<MkTab v-model="tab"> | ||||||
|  | 		<option value="notes">{{ i18n.ts.notes }}</option> | ||||||
|  | 		<option value="polls">{{ i18n.ts.poll }}</option> | ||||||
|  | 	</MkTab> | ||||||
|  | 	<XNotes v-if="tab === 'notes'" :pagination="paginationForNotes"/> | ||||||
|  | 	<XNotes v-else-if="tab === 'polls'" :pagination="paginationForPolls"/> | ||||||
|  | </MkSpacer> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import XNotes from '@/components/notes.vue'; | ||||||
|  | import MkTab from '@/components/tab.vue'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
|  | 
 | ||||||
|  | const paginationForNotes = { | ||||||
|  | 	endpoint: 'notes/featured' as const, | ||||||
|  | 	limit: 10, | ||||||
|  | 	offsetMode: true, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const paginationForPolls = { | ||||||
|  | 	endpoint: 'notes/polls/recommendation' as const, | ||||||
|  | 	limit: 10, | ||||||
|  | 	offsetMode: true, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | let tab = $ref('notes'); | ||||||
|  | </script> | ||||||
							
								
								
									
										143
									
								
								packages/client/src/pages/explore.users.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								packages/client/src/pages/explore.users.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,143 @@ | ||||||
|  | <template> | ||||||
|  | <MkSpacer :content-max="1200"> | ||||||
|  | 	<div v-if="origin === 'local'"> | ||||||
|  | 		<template v-if="tag == null"> | ||||||
|  | 			<MkFolder class="_gap" persist-key="explore-pinned-users"> | ||||||
|  | 				<template #header><i class="fas fa-bookmark fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.pinnedUsers }}</template> | ||||||
|  | 				<XUserList :pagination="pinnedUsers"/> | ||||||
|  | 			</MkFolder> | ||||||
|  | 			<MkFolder class="_gap" persist-key="explore-popular-users"> | ||||||
|  | 				<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template> | ||||||
|  | 				<XUserList :pagination="popularUsers"/> | ||||||
|  | 			</MkFolder> | ||||||
|  | 			<MkFolder class="_gap" persist-key="explore-recently-updated-users"> | ||||||
|  | 				<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template> | ||||||
|  | 				<XUserList :pagination="recentlyUpdatedUsers"/> | ||||||
|  | 			</MkFolder> | ||||||
|  | 			<MkFolder class="_gap" persist-key="explore-recently-registered-users"> | ||||||
|  | 				<template #header><i class="fas fa-plus fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyRegisteredUsers }}</template> | ||||||
|  | 				<XUserList :pagination="recentlyRegisteredUsers"/> | ||||||
|  | 			</MkFolder> | ||||||
|  | 		</template> | ||||||
|  | 	</div> | ||||||
|  | 	<div v-else> | ||||||
|  | 		<MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_gap"> | ||||||
|  | 			<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template> | ||||||
|  | 
 | ||||||
|  | 			<div class="vxjfqztj"> | ||||||
|  | 				<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/explore/tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA> | ||||||
|  | 				<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/explore/tags/${tag.tag}`">{{ tag.tag }}</MkA> | ||||||
|  | 			</div> | ||||||
|  | 		</MkFolder> | ||||||
|  | 
 | ||||||
|  | 		<MkFolder v-if="tag != null" :key="`${tag}`" class="_gap"> | ||||||
|  | 			<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template> | ||||||
|  | 			<XUserList :pagination="tagUsers"/> | ||||||
|  | 		</MkFolder> | ||||||
|  | 
 | ||||||
|  | 		<template v-if="tag == null"> | ||||||
|  | 			<MkFolder class="_gap"> | ||||||
|  | 				<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template> | ||||||
|  | 				<XUserList :pagination="popularUsersF"/> | ||||||
|  | 			</MkFolder> | ||||||
|  | 			<MkFolder class="_gap"> | ||||||
|  | 				<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template> | ||||||
|  | 				<XUserList :pagination="recentlyUpdatedUsersF"/> | ||||||
|  | 			</MkFolder> | ||||||
|  | 			<MkFolder class="_gap"> | ||||||
|  | 				<template #header><i class="fas fa-rocket fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyDiscoveredUsers }}</template> | ||||||
|  | 				<XUserList :pagination="recentlyRegisteredUsersF"/> | ||||||
|  | 			</MkFolder> | ||||||
|  | 		</template> | ||||||
|  | 	</div> | ||||||
|  | </MkSpacer> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import { computed, watch } from 'vue'; | ||||||
|  | import XUserList from '@/components/user-list.vue'; | ||||||
|  | import MkFolder from '@/components/ui/folder.vue'; | ||||||
|  | import number from '@/filters/number'; | ||||||
|  | import * as os from '@/os'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
|  | import { instance } from '@/instance'; | ||||||
|  | 
 | ||||||
|  | const props = defineProps<{ | ||||||
|  | 	origin: 'local' | 'remote'; | ||||||
|  | 	tag?: string; | ||||||
|  | }>(); | ||||||
|  | 
 | ||||||
|  | let tagsEl = $ref<InstanceType<typeof MkFolder>>(); | ||||||
|  | let tagsLocal = $ref([]); | ||||||
|  | let tagsRemote = $ref([]); | ||||||
|  | 
 | ||||||
|  | watch(() => props.tag, () => { | ||||||
|  | 	if (tagsEl) tagsEl.toggleContent(props.tag == null); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const tagUsers = $computed(() => ({ | ||||||
|  | 	endpoint: 'hashtags/users' as const, | ||||||
|  | 	limit: 30, | ||||||
|  | 	params: { | ||||||
|  | 		tag: props.tag, | ||||||
|  | 		origin: 'combined', | ||||||
|  | 		sort: '+follower', | ||||||
|  | 	}, | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const pinnedUsers = { endpoint: 'pinned-users' }; | ||||||
|  | const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||||
|  | 	state: 'alive', | ||||||
|  | 	origin: 'local', | ||||||
|  | 	sort: '+follower', | ||||||
|  | } }; | ||||||
|  | const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||||
|  | 	origin: 'local', | ||||||
|  | 	sort: '+updatedAt', | ||||||
|  | } }; | ||||||
|  | const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||||
|  | 	origin: 'local', | ||||||
|  | 	state: 'alive', | ||||||
|  | 	sort: '+createdAt', | ||||||
|  | } }; | ||||||
|  | const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||||
|  | 	state: 'alive', | ||||||
|  | 	origin: 'remote', | ||||||
|  | 	sort: '+follower', | ||||||
|  | } }; | ||||||
|  | const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||||
|  | 	origin: 'combined', | ||||||
|  | 	sort: '+updatedAt', | ||||||
|  | } }; | ||||||
|  | const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||||
|  | 	origin: 'combined', | ||||||
|  | 	sort: '+createdAt', | ||||||
|  | } }; | ||||||
|  | 
 | ||||||
|  | os.api('hashtags/list', { | ||||||
|  | 	sort: '+attachedLocalUsers', | ||||||
|  | 	attachedToLocalUserOnly: true, | ||||||
|  | 	limit: 30, | ||||||
|  | }).then(tags => { | ||||||
|  | 	tagsLocal = tags; | ||||||
|  | }); | ||||||
|  | os.api('hashtags/list', { | ||||||
|  | 	sort: '+attachedRemoteUsers', | ||||||
|  | 	attachedToRemoteUserOnly: true, | ||||||
|  | 	limit: 30, | ||||||
|  | }).then(tags => { | ||||||
|  | 	tagsRemote = tags; | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .vxjfqztj { | ||||||
|  | 	> * { | ||||||
|  | 		margin-right: 16px; | ||||||
|  | 
 | ||||||
|  | 		&.local { | ||||||
|  | 			font-weight: bold; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -1,90 +1,39 @@ | ||||||
| <template> | <template> | ||||||
| <MkStickyContainer> | <MkStickyContainer> | ||||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
| 	<MkSpacer :content-max="1200"> | 	<div class="lznhrdub"> | ||||||
| 		<div class="lznhrdub"> | 		<div v-if="tab === 'featured'"> | ||||||
| 			<div v-if="tab === 'local'"> | 			<XFeatured/> | ||||||
| 				<div v-if="instance && stats && tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: instance.bannerUrl ? `url(${instance.bannerUrl})` : null }"> |  | ||||||
| 					<header><span>{{ $t('explore', { host: instance.name || 'Misskey' }) }}</span></header> |  | ||||||
| 					<div><span>{{ $t('exploreUsersCount', { count: number(stats.originalUsersCount) }) }}</span></div> |  | ||||||
| 				</div> |  | ||||||
| 
 |  | ||||||
| 				<template v-if="tag == null"> |  | ||||||
| 					<MkFolder class="_gap" persist-key="explore-pinned-users"> |  | ||||||
| 						<template #header><i class="fas fa-bookmark fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.pinnedUsers }}</template> |  | ||||||
| 						<XUserList :pagination="pinnedUsers"/> |  | ||||||
| 					</MkFolder> |  | ||||||
| 					<MkFolder class="_gap" persist-key="explore-popular-users"> |  | ||||||
| 						<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template> |  | ||||||
| 						<XUserList :pagination="popularUsers"/> |  | ||||||
| 					</MkFolder> |  | ||||||
| 					<MkFolder class="_gap" persist-key="explore-recently-updated-users"> |  | ||||||
| 						<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template> |  | ||||||
| 						<XUserList :pagination="recentlyUpdatedUsers"/> |  | ||||||
| 					</MkFolder> |  | ||||||
| 					<MkFolder class="_gap" persist-key="explore-recently-registered-users"> |  | ||||||
| 						<template #header><i class="fas fa-plus fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyRegisteredUsers }}</template> |  | ||||||
| 						<XUserList :pagination="recentlyRegisteredUsers"/> |  | ||||||
| 					</MkFolder> |  | ||||||
| 				</template> |  | ||||||
| 			</div> |  | ||||||
| 			<div v-else-if="tab === 'remote'"> |  | ||||||
| 				<div v-if="tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: `url(/client-assets/fedi.jpg)` }"> |  | ||||||
| 					<header><span>{{ $ts.exploreFediverse }}</span></header> |  | ||||||
| 				</div> |  | ||||||
| 
 |  | ||||||
| 				<MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_gap"> |  | ||||||
| 					<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template> |  | ||||||
| 
 |  | ||||||
| 					<div class="vxjfqztj"> |  | ||||||
| 						<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/explore/tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA> |  | ||||||
| 						<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/explore/tags/${tag.tag}`">{{ tag.tag }}</MkA> |  | ||||||
| 					</div> |  | ||||||
| 				</MkFolder> |  | ||||||
| 
 |  | ||||||
| 				<MkFolder v-if="tag != null" :key="`${tag}`" class="_gap"> |  | ||||||
| 					<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template> |  | ||||||
| 					<XUserList :pagination="tagUsers"/> |  | ||||||
| 				</MkFolder> |  | ||||||
| 
 |  | ||||||
| 				<template v-if="tag == null"> |  | ||||||
| 					<MkFolder class="_gap"> |  | ||||||
| 						<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template> |  | ||||||
| 						<XUserList :pagination="popularUsersF"/> |  | ||||||
| 					</MkFolder> |  | ||||||
| 					<MkFolder class="_gap"> |  | ||||||
| 						<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template> |  | ||||||
| 						<XUserList :pagination="recentlyUpdatedUsersF"/> |  | ||||||
| 					</MkFolder> |  | ||||||
| 					<MkFolder class="_gap"> |  | ||||||
| 						<template #header><i class="fas fa-rocket fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyDiscoveredUsers }}</template> |  | ||||||
| 						<XUserList :pagination="recentlyRegisteredUsersF"/> |  | ||||||
| 					</MkFolder> |  | ||||||
| 				</template> |  | ||||||
| 			</div> |  | ||||||
| 			<div v-else-if="tab === 'search'"> |  | ||||||
| 				<div class="_isolated"> |  | ||||||
| 					<MkInput v-model="searchQuery" :debounce="true" type="search"> |  | ||||||
| 						<template #prefix><i class="fas fa-search"></i></template> |  | ||||||
| 						<template #label>{{ $ts.searchUser }}</template> |  | ||||||
| 					</MkInput> |  | ||||||
| 					<MkRadios v-model="searchOrigin"> |  | ||||||
| 						<option value="combined">{{ $ts.all }}</option> |  | ||||||
| 						<option value="local">{{ $ts.local }}</option> |  | ||||||
| 						<option value="remote">{{ $ts.remote }}</option> |  | ||||||
| 					</MkRadios> |  | ||||||
| 				</div> |  | ||||||
| 
 |  | ||||||
| 				<XUserList v-if="searchQuery" ref="searchEl" class="_gap" :pagination="searchPagination"/> |  | ||||||
| 			</div> |  | ||||||
| 		</div> | 		</div> | ||||||
| 	</MkSpacer> | 		<div v-else-if="tab === 'localUsers'"> | ||||||
|  | 			<XUsers origin="local"/> | ||||||
|  | 		</div> | ||||||
|  | 		<div v-else-if="tab === 'remoteUsers'"> | ||||||
|  | 			<XUsers origin="remote"/> | ||||||
|  | 		</div> | ||||||
|  | 		<div v-else-if="tab === 'search'"> | ||||||
|  | 			<div class="_isolated"> | ||||||
|  | 				<MkInput v-model="searchQuery" :debounce="true" type="search"> | ||||||
|  | 					<template #prefix><i class="fas fa-search"></i></template> | ||||||
|  | 					<template #label>{{ $ts.searchUser }}</template> | ||||||
|  | 				</MkInput> | ||||||
|  | 				<MkRadios v-model="searchOrigin"> | ||||||
|  | 					<option value="combined">{{ $ts.all }}</option> | ||||||
|  | 					<option value="local">{{ $ts.local }}</option> | ||||||
|  | 					<option value="remote">{{ $ts.remote }}</option> | ||||||
|  | 				</MkRadios> | ||||||
|  | 			</div> | ||||||
|  | 
 | ||||||
|  | 			<XUserList v-if="searchQuery" ref="searchEl" class="_gap" :pagination="searchPagination"/> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
| </MkStickyContainer> | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed, defineComponent, watch } from 'vue'; | import { computed, watch } from 'vue'; | ||||||
| import XUserList from '@/components/user-list.vue'; | import XFeatured from './explore.featured.vue'; | ||||||
|  | import XUsers from './explore.users.vue'; | ||||||
| import MkFolder from '@/components/ui/folder.vue'; | import MkFolder from '@/components/ui/folder.vue'; | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import MkRadios from '@/components/form/radios.vue'; | import MkRadios from '@/components/form/radios.vue'; | ||||||
|  | @ -98,11 +47,8 @@ const props = defineProps<{ | ||||||
| 	tag?: string; | 	tag?: string; | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
| let tab = $ref('local'); | let tab = $ref('featured'); | ||||||
| let tagsEl = $ref<InstanceType<typeof MkFolder>>(); | let tagsEl = $ref<InstanceType<typeof MkFolder>>(); | ||||||
| let tagsLocal = $ref([]); |  | ||||||
| let tagsRemote = $ref([]); |  | ||||||
| let stats = $ref(null); |  | ||||||
| let searchQuery = $ref(null); | let searchQuery = $ref(null); | ||||||
| let searchOrigin = $ref('combined'); | let searchOrigin = $ref('combined'); | ||||||
| 
 | 
 | ||||||
|  | @ -110,44 +56,6 @@ watch(() => props.tag, () => { | ||||||
| 	if (tagsEl) tagsEl.toggleContent(props.tag == null); | 	if (tagsEl) tagsEl.toggleContent(props.tag == null); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const tagUsers = $computed(() => ({ |  | ||||||
| 	endpoint: 'hashtags/users' as const, |  | ||||||
| 	limit: 30, |  | ||||||
| 	params: { |  | ||||||
| 		tag: props.tag, |  | ||||||
| 		origin: 'combined', |  | ||||||
| 		sort: '+follower', |  | ||||||
| 	}, |  | ||||||
| })); |  | ||||||
| 
 |  | ||||||
| const pinnedUsers = { endpoint: 'pinned-users' }; |  | ||||||
| const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { |  | ||||||
| 	state: 'alive', |  | ||||||
| 	origin: 'local', |  | ||||||
| 	sort: '+follower', |  | ||||||
| } }; |  | ||||||
| const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { |  | ||||||
| 	origin: 'local', |  | ||||||
| 	sort: '+updatedAt', |  | ||||||
| } }; |  | ||||||
| const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { |  | ||||||
| 	origin: 'local', |  | ||||||
| 	state: 'alive', |  | ||||||
| 	sort: '+createdAt', |  | ||||||
| } }; |  | ||||||
| const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: { |  | ||||||
| 	state: 'alive', |  | ||||||
| 	origin: 'remote', |  | ||||||
| 	sort: '+follower', |  | ||||||
| } }; |  | ||||||
| const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: { |  | ||||||
| 	origin: 'combined', |  | ||||||
| 	sort: '+updatedAt', |  | ||||||
| } }; |  | ||||||
| const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: { |  | ||||||
| 	origin: 'combined', |  | ||||||
| 	sort: '+createdAt', |  | ||||||
| } }; |  | ||||||
| const searchPagination = { | const searchPagination = { | ||||||
| 	endpoint: 'users/search' as const, | 	endpoint: 'users/search' as const, | ||||||
| 	limit: 10, | 	limit: 10, | ||||||
|  | @ -157,31 +65,19 @@ const searchPagination = { | ||||||
| 	} : null), | 	} : null), | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| os.api('hashtags/list', { |  | ||||||
| 	sort: '+attachedLocalUsers', |  | ||||||
| 	attachedToLocalUserOnly: true, |  | ||||||
| 	limit: 30, |  | ||||||
| }).then(tags => { |  | ||||||
| 	tagsLocal = tags; |  | ||||||
| }); |  | ||||||
| os.api('hashtags/list', { |  | ||||||
| 	sort: '+attachedRemoteUsers', |  | ||||||
| 	attachedToRemoteUserOnly: true, |  | ||||||
| 	limit: 30, |  | ||||||
| }).then(tags => { |  | ||||||
| 	tagsRemote = tags; |  | ||||||
| }); |  | ||||||
| os.api('stats').then(_stats => { |  | ||||||
| 	stats = _stats; |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const headerActions = $computed(() => []); | const headerActions = $computed(() => []); | ||||||
| 
 | 
 | ||||||
| const headerTabs = $computed(() => [{ | const headerTabs = $computed(() => [{ | ||||||
| 	key: 'local', | 	key: 'featured', | ||||||
| 	title: i18n.ts.local, | 	icon: 'fas fa-bolt', | ||||||
|  | 	title: i18n.ts.featured, | ||||||
| }, { | }, { | ||||||
| 	key: 'remote', | 	key: 'localUsers', | ||||||
|  | 	icon: 'fas fa-users', | ||||||
|  | 	title: i18n.ts.users, | ||||||
|  | }, { | ||||||
|  | 	key: 'remoteUsers', | ||||||
|  | 	icon: 'fas fa-users', | ||||||
| 	title: i18n.ts.remote, | 	title: i18n.ts.remote, | ||||||
| }, { | }, { | ||||||
| 	key: 'search', | 	key: 'search', | ||||||
|  | @ -194,46 +90,3 @@ definePageMetadata(computed(() => ({ | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| }))); | }))); | ||||||
| </script> | </script> | ||||||
| 
 |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .localfedi7 { |  | ||||||
| 	color: #fff; |  | ||||||
| 	padding: 16px; |  | ||||||
| 	height: 80px; |  | ||||||
| 	background-position: 50%; |  | ||||||
| 	background-size: cover; |  | ||||||
| 	margin-bottom: var(--margin); |  | ||||||
| 
 |  | ||||||
| 	> * { |  | ||||||
| 		&:not(:last-child) { |  | ||||||
| 			margin-bottom: 8px; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> span { |  | ||||||
| 			display: inline-block; |  | ||||||
| 			padding: 6px 8px; |  | ||||||
| 			background: rgba(0, 0, 0, 0.7); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> header { |  | ||||||
| 		font-size: 20px; |  | ||||||
| 		font-weight: bold; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> div { |  | ||||||
| 		font-size: 14px; |  | ||||||
| 		opacity: 0.8; |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .vxjfqztj { |  | ||||||
| 	> * { |  | ||||||
| 		margin-right: 16px; |  | ||||||
| 
 |  | ||||||
| 		&.local { |  | ||||||
| 			font-weight: bold; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  |  | ||||||
|  | @ -1,26 +0,0 @@ | ||||||
| <template> |  | ||||||
| <MkStickyContainer> |  | ||||||
| 	<template #header><MkPageHeader/></template> |  | ||||||
| 	<MkSpacer :content-max="800"> |  | ||||||
| 		<XNotes ref="notes" :pagination="pagination"/> |  | ||||||
| 	</MkSpacer> |  | ||||||
| </MkStickyContainer> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script lang="ts" setup> |  | ||||||
| import XNotes from '@/components/notes.vue'; |  | ||||||
| import { i18n } from '@/i18n'; |  | ||||||
| import { definePageMetadata } from '@/scripts/page-metadata'; |  | ||||||
| 
 |  | ||||||
| const pagination = { |  | ||||||
| 	endpoint: 'notes/featured' as const, |  | ||||||
| 	limit: 10, |  | ||||||
| 	offsetMode: true, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| definePageMetadata({ |  | ||||||
| 	title: i18n.ts.featured, |  | ||||||
| 	icon: 'fas fa-fire-alt', |  | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
							
								
								
									
										62
									
								
								packages/client/src/pages/user/followers.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								packages/client/src/pages/user/followers.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | ||||||
|  | <template> | ||||||
|  | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="1000"> | ||||||
|  | 		<transition name="fade" mode="out-in"> | ||||||
|  | 			<div v-if="user"> | ||||||
|  | 				<XFollowList :user="user" type="following"/> | ||||||
|  | 			</div> | ||||||
|  | 			<MkError v-else-if="error" @retry="fetch()"/> | ||||||
|  | 			<MkLoading v-else/> | ||||||
|  | 		</transition> | ||||||
|  | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import { defineAsyncComponent, computed, inject, onMounted, onUnmounted, watch } from 'vue'; | ||||||
|  | import * as Acct from 'misskey-js/built/acct'; | ||||||
|  | import * as misskey from 'misskey-js'; | ||||||
|  | import XFollowList from './follow-list.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults(defineProps<{ | ||||||
|  | 	acct: string; | ||||||
|  | }>(), { | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | let user = $ref<null | misskey.entities.UserDetailed>(null); | ||||||
|  | let error = $ref(null); | ||||||
|  | 
 | ||||||
|  | function fetchUser(): void { | ||||||
|  | 	if (props.acct == null) return; | ||||||
|  | 	user = null; | ||||||
|  | 	os.api('users/show', Acct.parse(props.acct)).then(u => { | ||||||
|  | 		user = u; | ||||||
|  | 	}).catch(err => { | ||||||
|  | 		error = err; | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | watch(() => props.acct, fetchUser, { | ||||||
|  | 	immediate: true, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const headerActions = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata(computed(() => user ? { | ||||||
|  | 	icon: 'fas fa-user', | ||||||
|  | 	title: user.name ? `${user.name} (@${user.username})` : `@${user.username}`, | ||||||
|  | 	subtitle: i18n.ts.followers, | ||||||
|  | 	userName: user, | ||||||
|  | 	avatar: user, | ||||||
|  | 	bg: 'var(--bg)', | ||||||
|  | } : null)); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | </style> | ||||||
							
								
								
									
										62
									
								
								packages/client/src/pages/user/following.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								packages/client/src/pages/user/following.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | ||||||
|  | <template> | ||||||
|  | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="1000"> | ||||||
|  | 		<transition name="fade" mode="out-in"> | ||||||
|  | 			<div v-if="user"> | ||||||
|  | 				<XFollowList :user="user" type="following"/> | ||||||
|  | 			</div> | ||||||
|  | 			<MkError v-else-if="error" @retry="fetch()"/> | ||||||
|  | 			<MkLoading v-else/> | ||||||
|  | 		</transition> | ||||||
|  | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import { defineAsyncComponent, computed, inject, onMounted, onUnmounted, watch } from 'vue'; | ||||||
|  | import * as Acct from 'misskey-js/built/acct'; | ||||||
|  | import * as misskey from 'misskey-js'; | ||||||
|  | import XFollowList from './follow-list.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults(defineProps<{ | ||||||
|  | 	acct: string; | ||||||
|  | }>(), { | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | let user = $ref<null | misskey.entities.UserDetailed>(null); | ||||||
|  | let error = $ref(null); | ||||||
|  | 
 | ||||||
|  | function fetchUser(): void { | ||||||
|  | 	if (props.acct == null) return; | ||||||
|  | 	user = null; | ||||||
|  | 	os.api('users/show', Acct.parse(props.acct)).then(u => { | ||||||
|  | 		user = u; | ||||||
|  | 	}).catch(err => { | ||||||
|  | 		error = err; | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | watch(() => props.acct, fetchUser, { | ||||||
|  | 	immediate: true, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const headerActions = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata(computed(() => user ? { | ||||||
|  | 	icon: 'fas fa-user', | ||||||
|  | 	title: user.name ? `${user.name} (@${user.username})` : `@${user.username}`, | ||||||
|  | 	subtitle: i18n.ts.following, | ||||||
|  | 	userName: user, | ||||||
|  | 	avatar: user, | ||||||
|  | 	bg: 'var(--bg)', | ||||||
|  | } : null)); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | </style> | ||||||
							
								
								
									
										478
									
								
								packages/client/src/pages/user/home.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										478
									
								
								packages/client/src/pages/user/home.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,478 @@ | ||||||
|  | <template> | ||||||
|  | <MkSpacer :content-max="narrow ? 800 : 1100"> | ||||||
|  | 	<div ref="rootEl" v-size="{ max: [500] }" class="ftskorzw" :class="{ wide: !narrow }"> | ||||||
|  | 		<div class="main"> | ||||||
|  | 			<!-- TODO --> | ||||||
|  | 			<!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSuspended }}</div> --> | ||||||
|  | 			<!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSilenced }}</div> --> | ||||||
|  | 
 | ||||||
|  | 			<div class="profile"> | ||||||
|  | 				<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/> | ||||||
|  | 
 | ||||||
|  | 				<div :key="user.id" class="_block main"> | ||||||
|  | 					<div class="banner-container" :style="style"> | ||||||
|  | 						<div ref="bannerEl" class="banner" :style="style"></div> | ||||||
|  | 						<div class="fade"></div> | ||||||
|  | 						<div class="title"> | ||||||
|  | 							<MkUserName class="name" :user="user" :nowrap="true"/> | ||||||
|  | 							<div class="bottom"> | ||||||
|  | 								<span class="username"><MkAcct :user="user" :detail="true"/></span> | ||||||
|  | 								<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span> | ||||||
|  | 								<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span> | ||||||
|  | 								<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span> | ||||||
|  | 								<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span> | ||||||
|  | 							</div> | ||||||
|  | 						</div> | ||||||
|  | 						<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span> | ||||||
|  | 						<div v-if="$i" class="actions"> | ||||||
|  | 							<button class="menu _button" @click="menu"><i class="fas fa-ellipsis-h"></i></button> | ||||||
|  | 							<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 					<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/> | ||||||
|  | 					<div class="title"> | ||||||
|  | 						<MkUserName :user="user" :nowrap="false" class="name"/> | ||||||
|  | 						<div class="bottom"> | ||||||
|  | 							<span class="username"><MkAcct :user="user" :detail="true"/></span> | ||||||
|  | 							<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span> | ||||||
|  | 							<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span> | ||||||
|  | 							<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span> | ||||||
|  | 							<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 					<div class="description"> | ||||||
|  | 						<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/> | ||||||
|  | 						<p v-else class="empty">{{ $ts.noAccountDescription }}</p> | ||||||
|  | 					</div> | ||||||
|  | 					<div class="fields system"> | ||||||
|  | 						<dl v-if="user.location" class="field"> | ||||||
|  | 							<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt> | ||||||
|  | 							<dd class="value">{{ user.location }}</dd> | ||||||
|  | 						</dl> | ||||||
|  | 						<dl v-if="user.birthday" class="field"> | ||||||
|  | 							<dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt> | ||||||
|  | 							<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd> | ||||||
|  | 						</dl> | ||||||
|  | 						<dl class="field"> | ||||||
|  | 							<dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt> | ||||||
|  | 							<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd> | ||||||
|  | 						</dl> | ||||||
|  | 					</div> | ||||||
|  | 					<div v-if="user.fields.length > 0" class="fields"> | ||||||
|  | 						<dl v-for="(field, i) in user.fields" :key="i" class="field"> | ||||||
|  | 							<dt class="name"> | ||||||
|  | 								<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/> | ||||||
|  | 							</dt> | ||||||
|  | 							<dd class="value"> | ||||||
|  | 								<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/> | ||||||
|  | 							</dd> | ||||||
|  | 						</dl> | ||||||
|  | 					</div> | ||||||
|  | 					<div class="status"> | ||||||
|  | 						<MkA v-click-anime :to="userPage(user)" :class="{ active: page === 'index' }"> | ||||||
|  | 							<b>{{ number(user.notesCount) }}</b> | ||||||
|  | 							<span>{{ $ts.notes }}</span> | ||||||
|  | 						</MkA> | ||||||
|  | 						<MkA v-click-anime :to="userPage(user, 'following')" :class="{ active: page === 'following' }"> | ||||||
|  | 							<b>{{ number(user.followingCount) }}</b> | ||||||
|  | 							<span>{{ $ts.following }}</span> | ||||||
|  | 						</MkA> | ||||||
|  | 						<MkA v-click-anime :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }"> | ||||||
|  | 							<b>{{ number(user.followersCount) }}</b> | ||||||
|  | 							<span>{{ $ts.followers }}</span> | ||||||
|  | 						</MkA> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 
 | ||||||
|  | 			<div class="contents"> | ||||||
|  | 				<div v-if="user.pinnedNotes.length > 0" class="_gap"> | ||||||
|  | 					<XNote v-for="note in user.pinnedNotes" :key="note.id" class="note _block" :note="note" :pinned="true"/> | ||||||
|  | 				</div> | ||||||
|  | 				<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo> | ||||||
|  | 				<template v-if="narrow"> | ||||||
|  | 					<XPhotos :key="user.id" :user="user"/> | ||||||
|  | 					<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/> | ||||||
|  | 				</template> | ||||||
|  | 			</div> | ||||||
|  | 			<div> | ||||||
|  | 				<XUserTimeline :user="user"/> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 		<div v-if="!narrow" class="sub"> | ||||||
|  | 			<XPhotos :key="user.id" :user="user"/> | ||||||
|  | 			<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | </MkSpacer> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import { defineAsyncComponent, computed, inject, onMounted, onUnmounted, watch } from 'vue'; | ||||||
|  | import calcAge from 's-age'; | ||||||
|  | import * as misskey from 'misskey-js'; | ||||||
|  | import XUserTimeline from './index.timeline.vue'; | ||||||
|  | import XNote from '@/components/note.vue'; | ||||||
|  | import MkFollowButton from '@/components/follow-button.vue'; | ||||||
|  | import MkContainer from '@/components/ui/container.vue'; | ||||||
|  | import MkFolder from '@/components/ui/folder.vue'; | ||||||
|  | import MkRemoteCaution from '@/components/remote-caution.vue'; | ||||||
|  | import MkTab from '@/components/tab.vue'; | ||||||
|  | import MkInfo from '@/components/ui/info.vue'; | ||||||
|  | import { getScrollPosition } from '@/scripts/scroll'; | ||||||
|  | import { getUserMenu } from '@/scripts/get-user-menu'; | ||||||
|  | import number from '@/filters/number'; | ||||||
|  | import { userPage, acct as getAcct } from '@/filters/user'; | ||||||
|  | import * as os from '@/os'; | ||||||
|  | import { useRouter } from '@/router'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
|  | import { $i } from '@/account'; | ||||||
|  | 
 | ||||||
|  | const XPhotos = defineAsyncComponent(() => import('./index.photos.vue')); | ||||||
|  | const XActivity = defineAsyncComponent(() => import('./index.activity.vue')); | ||||||
|  | 
 | ||||||
|  | const props = withDefaults(defineProps<{ | ||||||
|  | 	user: misskey.entities.UserDetailed; | ||||||
|  | }>(), { | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const router = useRouter(); | ||||||
|  | 
 | ||||||
|  | let parallaxAnimationId = $ref<null | number>(null); | ||||||
|  | let narrow = $ref<null | boolean>(null); | ||||||
|  | let rootEl = $ref<null | HTMLElement>(null); | ||||||
|  | let bannerEl = $ref<null | HTMLElement>(null); | ||||||
|  | 
 | ||||||
|  | const style = $computed(() => { | ||||||
|  | 	if (props.user.bannerUrl == null) return {}; | ||||||
|  | 	return { | ||||||
|  | 		backgroundImage: `url(${ props.user.bannerUrl })`, | ||||||
|  | 	}; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const age = $computed(() => { | ||||||
|  | 	return calcAge(props.user.birthday); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function menu(ev) { | ||||||
|  | 	os.popupMenu(getUserMenu(props.user), ev.currentTarget ?? ev.target); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function parallaxLoop() { | ||||||
|  | 	parallaxAnimationId = window.requestAnimationFrame(parallaxLoop); | ||||||
|  | 	parallax(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function parallax() { | ||||||
|  | 	const banner = bannerEl as any; | ||||||
|  | 	if (banner == null) return; | ||||||
|  | 
 | ||||||
|  | 	const top = getScrollPosition(rootEl); | ||||||
|  | 
 | ||||||
|  | 	if (top < 0) return; | ||||||
|  | 
 | ||||||
|  | 	const z = 1.75; // 奥行き(小さいほど奥) | ||||||
|  | 	const pos = -(top / z); | ||||||
|  | 	banner.style.backgroundPosition = `center calc(50% - ${pos}px)`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | onMounted(() => { | ||||||
|  | 	window.requestAnimationFrame(parallaxLoop); | ||||||
|  | 	narrow = rootEl!.clientWidth < 1000; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | onUnmounted(() => { | ||||||
|  | 	if (parallaxAnimationId) { | ||||||
|  | 		window.cancelAnimationFrame(parallaxAnimationId); | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .ftskorzw { | ||||||
|  | 
 | ||||||
|  | 	> .main { | ||||||
|  | 
 | ||||||
|  | 		> .punished { | ||||||
|  | 			font-size: 0.8em; | ||||||
|  | 			padding: 16px; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		> .profile { | ||||||
|  | 
 | ||||||
|  | 			> .main { | ||||||
|  | 				position: relative; | ||||||
|  | 				overflow: hidden; | ||||||
|  | 
 | ||||||
|  | 				> .banner-container { | ||||||
|  | 					position: relative; | ||||||
|  | 					height: 250px; | ||||||
|  | 					overflow: hidden; | ||||||
|  | 					background-size: cover; | ||||||
|  | 					background-position: center; | ||||||
|  | 
 | ||||||
|  | 					> .banner { | ||||||
|  | 						height: 100%; | ||||||
|  | 						background-color: #4c5e6d; | ||||||
|  | 						background-size: cover; | ||||||
|  | 						background-position: center; | ||||||
|  | 						box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset; | ||||||
|  | 						will-change: background-position; | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					> .fade { | ||||||
|  | 						position: absolute; | ||||||
|  | 						bottom: 0; | ||||||
|  | 						left: 0; | ||||||
|  | 						width: 100%; | ||||||
|  | 						height: 78px; | ||||||
|  | 						background: linear-gradient(transparent, rgba(#000, 0.7)); | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					> .followed { | ||||||
|  | 						position: absolute; | ||||||
|  | 						top: 12px; | ||||||
|  | 						left: 12px; | ||||||
|  | 						padding: 4px 8px; | ||||||
|  | 						color: #fff; | ||||||
|  | 						background: rgba(0, 0, 0, 0.7); | ||||||
|  | 						font-size: 0.7em; | ||||||
|  | 						border-radius: 6px; | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					> .actions { | ||||||
|  | 						position: absolute; | ||||||
|  | 						top: 12px; | ||||||
|  | 						right: 12px; | ||||||
|  | 						-webkit-backdrop-filter: var(--blur, blur(8px)); | ||||||
|  | 						backdrop-filter: var(--blur, blur(8px)); | ||||||
|  | 						background: rgba(0, 0, 0, 0.2); | ||||||
|  | 						padding: 8px; | ||||||
|  | 						border-radius: 24px; | ||||||
|  | 
 | ||||||
|  | 						> .menu { | ||||||
|  | 							vertical-align: bottom; | ||||||
|  | 							height: 31px; | ||||||
|  | 							width: 31px; | ||||||
|  | 							color: #fff; | ||||||
|  | 							text-shadow: 0 0 8px #000; | ||||||
|  | 							font-size: 16px; | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						> .koudoku { | ||||||
|  | 							margin-left: 4px; | ||||||
|  | 							vertical-align: bottom; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					> .title { | ||||||
|  | 						position: absolute; | ||||||
|  | 						bottom: 0; | ||||||
|  | 						left: 0; | ||||||
|  | 						width: 100%; | ||||||
|  | 						padding: 0 0 8px 154px; | ||||||
|  | 						box-sizing: border-box; | ||||||
|  | 						color: #fff; | ||||||
|  | 
 | ||||||
|  | 						> .name { | ||||||
|  | 							display: block; | ||||||
|  | 							margin: 0; | ||||||
|  | 							line-height: 32px; | ||||||
|  | 							font-weight: bold; | ||||||
|  | 							font-size: 1.8em; | ||||||
|  | 							text-shadow: 0 0 8px #000; | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						> .bottom { | ||||||
|  | 							> * { | ||||||
|  | 								display: inline-block; | ||||||
|  | 								margin-right: 16px; | ||||||
|  | 								line-height: 20px; | ||||||
|  | 								opacity: 0.8; | ||||||
|  | 
 | ||||||
|  | 								&.username { | ||||||
|  | 									font-weight: bold; | ||||||
|  | 								} | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .title { | ||||||
|  | 					display: none; | ||||||
|  | 					text-align: center; | ||||||
|  | 					padding: 50px 8px 16px 8px; | ||||||
|  | 					font-weight: bold; | ||||||
|  | 					border-bottom: solid 0.5px var(--divider); | ||||||
|  | 
 | ||||||
|  | 					> .bottom { | ||||||
|  | 						> * { | ||||||
|  | 							display: inline-block; | ||||||
|  | 							margin-right: 8px; | ||||||
|  | 							opacity: 0.8; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .avatar { | ||||||
|  | 					display: block; | ||||||
|  | 					position: absolute; | ||||||
|  | 					top: 170px; | ||||||
|  | 					left: 16px; | ||||||
|  | 					z-index: 2; | ||||||
|  | 					width: 120px; | ||||||
|  | 					height: 120px; | ||||||
|  | 					box-shadow: 1px 1px 3px rgba(#000, 0.2); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .description { | ||||||
|  | 					padding: 24px 24px 24px 154px; | ||||||
|  | 					font-size: 0.95em; | ||||||
|  | 
 | ||||||
|  | 					> .empty { | ||||||
|  | 						margin: 0; | ||||||
|  | 						opacity: 0.5; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .fields { | ||||||
|  | 					padding: 24px; | ||||||
|  | 					font-size: 0.9em; | ||||||
|  | 					border-top: solid 0.5px var(--divider); | ||||||
|  | 
 | ||||||
|  | 					> .field { | ||||||
|  | 						display: flex; | ||||||
|  | 						padding: 0; | ||||||
|  | 						margin: 0; | ||||||
|  | 						align-items: center; | ||||||
|  | 
 | ||||||
|  | 						&:not(:last-child) { | ||||||
|  | 							margin-bottom: 8px; | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						> .name { | ||||||
|  | 							width: 30%; | ||||||
|  | 							overflow: hidden; | ||||||
|  | 							white-space: nowrap; | ||||||
|  | 							text-overflow: ellipsis; | ||||||
|  | 							font-weight: bold; | ||||||
|  | 							text-align: center; | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						> .value { | ||||||
|  | 							width: 70%; | ||||||
|  | 							overflow: hidden; | ||||||
|  | 							white-space: nowrap; | ||||||
|  | 							text-overflow: ellipsis; | ||||||
|  | 							margin: 0; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					&.system > .field > .name { | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .status { | ||||||
|  | 					display: flex; | ||||||
|  | 					padding: 24px; | ||||||
|  | 					border-top: solid 0.5px var(--divider); | ||||||
|  | 
 | ||||||
|  | 					> a { | ||||||
|  | 						flex: 1; | ||||||
|  | 						text-align: center; | ||||||
|  | 
 | ||||||
|  | 						&.active { | ||||||
|  | 							color: var(--accent); | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						&:hover { | ||||||
|  | 							text-decoration: none; | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						> b { | ||||||
|  | 							display: block; | ||||||
|  | 							line-height: 16px; | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						> span { | ||||||
|  | 							font-size: 70%; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		> .contents { | ||||||
|  | 			> .content { | ||||||
|  | 				margin-bottom: var(--margin); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	&.max-width_500px { | ||||||
|  | 		> .main { | ||||||
|  | 			> .profile > .main { | ||||||
|  | 				> .banner-container { | ||||||
|  | 					height: 140px; | ||||||
|  | 
 | ||||||
|  | 					> .fade { | ||||||
|  | 						display: none; | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					> .title { | ||||||
|  | 						display: none; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .title { | ||||||
|  | 					display: block; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .avatar { | ||||||
|  | 					top: 90px; | ||||||
|  | 					left: 0; | ||||||
|  | 					right: 0; | ||||||
|  | 					width: 92px; | ||||||
|  | 					height: 92px; | ||||||
|  | 					margin: auto; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .description { | ||||||
|  | 					padding: 16px; | ||||||
|  | 					text-align: center; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .fields { | ||||||
|  | 					padding: 16px; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .status { | ||||||
|  | 					padding: 16px; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			> .contents { | ||||||
|  | 				> .nav { | ||||||
|  | 					font-size: 80%; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	&.wide { | ||||||
|  | 		display: flex; | ||||||
|  | 		width: 100%; | ||||||
|  | 
 | ||||||
|  | 		> .main { | ||||||
|  | 			width: 100%; | ||||||
|  | 			min-width: 0; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		> .sub { | ||||||
|  | 			max-width: 350px; | ||||||
|  | 			min-width: 350px; | ||||||
|  | 			margin-left: var(--margin); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -1,124 +1,15 @@ | ||||||
| <template> | <template> | ||||||
| <MkStickyContainer> | <MkStickyContainer> | ||||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
| 	<div ref="rootEl"> | 	<div> | ||||||
| 		<transition name="fade" mode="out-in"> | 		<transition name="fade" mode="out-in"> | ||||||
| 			<MkSpacer v-if="user" :content-max="narrow ? 800 : 1100"> | 			<div v-if="user"> | ||||||
| 				<div v-size="{ max: [500] }" class="ftskorzw" :class="{ wide: !narrow }"> | 				<XHome v-if="tab === 'home'" :user="user"/> | ||||||
| 					<div class="main"> | 				<XReactions v-else-if="tab === 'reactions'" :user="user"/> | ||||||
| 						<!-- TODO --> | 				<XClips v-else-if="tab === 'clips'" :user="user"/> | ||||||
| 						<!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSuspended }}</div> --> | 				<XPages v-else-if="tab === 'pages'" :user="user"/> | ||||||
| 						<!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSilenced }}</div> --> | 				<XGallery v-else-if="tab === 'gallery'" :user="user"/> | ||||||
| 
 | 			</div> | ||||||
| 						<div class="profile"> |  | ||||||
| 							<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/> |  | ||||||
| 
 |  | ||||||
| 							<div :key="user.id" class="_block main"> |  | ||||||
| 								<div class="banner-container" :style="style"> |  | ||||||
| 									<div ref="bannerEl" class="banner" :style="style"></div> |  | ||||||
| 									<div class="fade"></div> |  | ||||||
| 									<div class="title"> |  | ||||||
| 										<MkUserName class="name" :user="user" :nowrap="true"/> |  | ||||||
| 										<div class="bottom"> |  | ||||||
| 											<span class="username"><MkAcct :user="user" :detail="true"/></span> |  | ||||||
| 											<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span> |  | ||||||
| 											<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span> |  | ||||||
| 											<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span> |  | ||||||
| 											<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span> |  | ||||||
| 										</div> |  | ||||||
| 									</div> |  | ||||||
| 									<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span> |  | ||||||
| 									<div v-if="$i" class="actions"> |  | ||||||
| 										<button class="menu _button" @click="menu"><i class="fas fa-ellipsis-h"></i></button> |  | ||||||
| 										<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> |  | ||||||
| 									</div> |  | ||||||
| 								</div> |  | ||||||
| 								<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/> |  | ||||||
| 								<div class="title"> |  | ||||||
| 									<MkUserName :user="user" :nowrap="false" class="name"/> |  | ||||||
| 									<div class="bottom"> |  | ||||||
| 										<span class="username"><MkAcct :user="user" :detail="true"/></span> |  | ||||||
| 										<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span> |  | ||||||
| 										<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span> |  | ||||||
| 										<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span> |  | ||||||
| 										<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span> |  | ||||||
| 									</div> |  | ||||||
| 								</div> |  | ||||||
| 								<div class="description"> |  | ||||||
| 									<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/> |  | ||||||
| 									<p v-else class="empty">{{ $ts.noAccountDescription }}</p> |  | ||||||
| 								</div> |  | ||||||
| 								<div class="fields system"> |  | ||||||
| 									<dl v-if="user.location" class="field"> |  | ||||||
| 										<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt> |  | ||||||
| 										<dd class="value">{{ user.location }}</dd> |  | ||||||
| 									</dl> |  | ||||||
| 									<dl v-if="user.birthday" class="field"> |  | ||||||
| 										<dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt> |  | ||||||
| 										<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd> |  | ||||||
| 									</dl> |  | ||||||
| 									<dl class="field"> |  | ||||||
| 										<dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt> |  | ||||||
| 										<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd> |  | ||||||
| 									</dl> |  | ||||||
| 								</div> |  | ||||||
| 								<div v-if="user.fields.length > 0" class="fields"> |  | ||||||
| 									<dl v-for="(field, i) in user.fields" :key="i" class="field"> |  | ||||||
| 										<dt class="name"> |  | ||||||
| 											<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/> |  | ||||||
| 										</dt> |  | ||||||
| 										<dd class="value"> |  | ||||||
| 											<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/> |  | ||||||
| 										</dd> |  | ||||||
| 									</dl> |  | ||||||
| 								</div> |  | ||||||
| 								<div class="status"> |  | ||||||
| 									<MkA v-click-anime :to="userPage(user)" :class="{ active: page === 'index' }"> |  | ||||||
| 										<b>{{ number(user.notesCount) }}</b> |  | ||||||
| 										<span>{{ $ts.notes }}</span> |  | ||||||
| 									</MkA> |  | ||||||
| 									<MkA v-click-anime :to="userPage(user, 'following')" :class="{ active: page === 'following' }"> |  | ||||||
| 										<b>{{ number(user.followingCount) }}</b> |  | ||||||
| 										<span>{{ $ts.following }}</span> |  | ||||||
| 									</MkA> |  | ||||||
| 									<MkA v-click-anime :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }"> |  | ||||||
| 										<b>{{ number(user.followersCount) }}</b> |  | ||||||
| 										<span>{{ $ts.followers }}</span> |  | ||||||
| 									</MkA> |  | ||||||
| 								</div> |  | ||||||
| 							</div> |  | ||||||
| 						</div> |  | ||||||
| 
 |  | ||||||
| 						<div class="contents"> |  | ||||||
| 							<template v-if="page === 'index'"> |  | ||||||
| 								<div> |  | ||||||
| 									<div v-if="user.pinnedNotes.length > 0" class="_gap"> |  | ||||||
| 										<XNote v-for="note in user.pinnedNotes" :key="note.id" class="note _block" :note="note" :pinned="true"/> |  | ||||||
| 									</div> |  | ||||||
| 									<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo> |  | ||||||
| 									<template v-if="narrow"> |  | ||||||
| 										<XPhotos :key="user.id" :user="user"/> |  | ||||||
| 										<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/> |  | ||||||
| 									</template> |  | ||||||
| 								</div> |  | ||||||
| 								<div> |  | ||||||
| 									<XUserTimeline :user="user"/> |  | ||||||
| 								</div> |  | ||||||
| 							</template> |  | ||||||
| 							<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/> |  | ||||||
| 							<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/> |  | ||||||
| 							<XReactions v-else-if="page === 'reactions'" :user="user" class="_gap"/> |  | ||||||
| 							<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/> |  | ||||||
| 							<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/> |  | ||||||
| 							<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/> |  | ||||||
| 						</div> |  | ||||||
| 					</div> |  | ||||||
| 					<div v-if="!narrow" class="sub"> |  | ||||||
| 						<XPhotos :key="user.id" :user="user"/> |  | ||||||
| 						<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/> |  | ||||||
| 					</div> |  | ||||||
| 				</div> |  | ||||||
| 			</MkSpacer> |  | ||||||
| 			<MkError v-else-if="error" @retry="fetch()"/> | 			<MkError v-else-if="error" @retry="fetch()"/> | ||||||
| 			<MkLoading v-else/> | 			<MkLoading v-else/> | ||||||
| 		</transition> | 		</transition> | ||||||
|  | @ -131,14 +22,6 @@ import { defineAsyncComponent, computed, inject, onMounted, onUnmounted, watch } | ||||||
| import calcAge from 's-age'; | import calcAge from 's-age'; | ||||||
| import * as Acct from 'misskey-js/built/acct'; | import * as Acct from 'misskey-js/built/acct'; | ||||||
| import * as misskey from 'misskey-js'; | import * as misskey from 'misskey-js'; | ||||||
| import XUserTimeline from './index.timeline.vue'; |  | ||||||
| import XNote from '@/components/note.vue'; |  | ||||||
| import MkFollowButton from '@/components/follow-button.vue'; |  | ||||||
| import MkContainer from '@/components/ui/container.vue'; |  | ||||||
| import MkFolder from '@/components/ui/folder.vue'; |  | ||||||
| import MkRemoteCaution from '@/components/remote-caution.vue'; |  | ||||||
| import MkTab from '@/components/tab.vue'; |  | ||||||
| import MkInfo from '@/components/ui/info.vue'; |  | ||||||
| import { getScrollPosition } from '@/scripts/scroll'; | import { getScrollPosition } from '@/scripts/scroll'; | ||||||
| import { getUserMenu } from '@/scripts/get-user-menu'; | import { getUserMenu } from '@/scripts/get-user-menu'; | ||||||
| import number from '@/filters/number'; | import number from '@/filters/number'; | ||||||
|  | @ -149,41 +32,24 @@ import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { $i } from '@/account'; | import { $i } from '@/account'; | ||||||
| 
 | 
 | ||||||
| const XFollowList = defineAsyncComponent(() => import('./follow-list.vue')); | const XHome = defineAsyncComponent(() => import('./home.vue')); | ||||||
| const XReactions = defineAsyncComponent(() => import('./reactions.vue')); | const XReactions = defineAsyncComponent(() => import('./reactions.vue')); | ||||||
| const XClips = defineAsyncComponent(() => import('./clips.vue')); | const XClips = defineAsyncComponent(() => import('./clips.vue')); | ||||||
| const XPages = defineAsyncComponent(() => import('./pages.vue')); | const XPages = defineAsyncComponent(() => import('./pages.vue')); | ||||||
| const XGallery = defineAsyncComponent(() => import('./gallery.vue')); | const XGallery = defineAsyncComponent(() => import('./gallery.vue')); | ||||||
| const XPhotos = defineAsyncComponent(() => import('./index.photos.vue')); |  | ||||||
| const XActivity = defineAsyncComponent(() => import('./index.activity.vue')); |  | ||||||
| 
 | 
 | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	acct: string; | 	acct: string; | ||||||
| 	page?: string; | 	page?: string; | ||||||
| }>(), { | }>(), { | ||||||
| 	page: 'index', | 	page: 'home', | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const router = useRouter(); | const router = useRouter(); | ||||||
| 
 | 
 | ||||||
|  | let tab = $ref(props.page); | ||||||
| let user = $ref<null | misskey.entities.UserDetailed>(null); | let user = $ref<null | misskey.entities.UserDetailed>(null); | ||||||
| let error = $ref(null); | let error = $ref(null); | ||||||
| let parallaxAnimationId = $ref<null | number>(null); |  | ||||||
| let narrow = $ref<null | boolean>(null); |  | ||||||
| let rootEl = $ref<null | HTMLElement>(null); |  | ||||||
| let bannerEl = $ref<null | HTMLElement>(null); |  | ||||||
| 
 |  | ||||||
| const style = $computed(() => { |  | ||||||
| 	if (user?.bannerUrl == null) return {}; |  | ||||||
| 	return { |  | ||||||
| 		backgroundImage: `url(${ user.bannerUrl })`, |  | ||||||
| 	}; |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const age = $computed(() => { |  | ||||||
| 	if (user == null) return null; |  | ||||||
| 	return calcAge(user.birthday); |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| function fetchUser(): void { | function fetchUser(): void { | ||||||
| 	if (props.acct == null) return; | 	if (props.acct == null) return; | ||||||
|  | @ -203,62 +69,28 @@ function menu(ev) { | ||||||
| 	os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target); | 	os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function parallaxLoop() { |  | ||||||
| 	parallaxAnimationId = window.requestAnimationFrame(parallaxLoop); |  | ||||||
| 	parallax(); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function parallax() { |  | ||||||
| 	const banner = bannerEl as any; |  | ||||||
| 	if (banner == null) return; |  | ||||||
| 
 |  | ||||||
| 	const top = getScrollPosition(rootEl); |  | ||||||
| 
 |  | ||||||
| 	if (top < 0) return; |  | ||||||
| 
 |  | ||||||
| 	const z = 1.75; // 奥行き(小さいほど奥) |  | ||||||
| 	const pos = -(top / z); |  | ||||||
| 	banner.style.backgroundPosition = `center calc(50% - ${pos}px)`; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| onMounted(() => { |  | ||||||
| 	window.requestAnimationFrame(parallaxLoop); |  | ||||||
| 	narrow = rootEl!.clientWidth < 1000; |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| onUnmounted(() => { |  | ||||||
| 	if (parallaxAnimationId) { |  | ||||||
| 		window.cancelAnimationFrame(parallaxAnimationId); |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const headerActions = $computed(() => []); | const headerActions = $computed(() => []); | ||||||
| 
 | 
 | ||||||
| const headerTabs = $computed(() => user ? [{ | const headerTabs = $computed(() => user ? [{ | ||||||
| 	active: props.page === 'index', | 	key: 'home', | ||||||
| 	title: i18n.ts.overview, | 	title: i18n.ts.overview, | ||||||
| 	icon: 'fas fa-home', | 	icon: 'fas fa-home', | ||||||
| 	onClick: () => { router.push('/@' + getAcct(user)); }, |  | ||||||
| }, ...($i && ($i.id === user.id)) || user.publicReactions ? [{ | }, ...($i && ($i.id === user.id)) || user.publicReactions ? [{ | ||||||
| 	active: props.page === 'reactions', | 	key: 'reactions', | ||||||
| 	title: i18n.ts.reaction, | 	title: i18n.ts.reaction, | ||||||
| 	icon: 'fas fa-laugh', | 	icon: 'fas fa-laugh', | ||||||
| 	onClick: () => { router.push('/@' + getAcct(user) + '/reactions'); }, |  | ||||||
| }] : [], { | }] : [], { | ||||||
| 	active: props.page === 'clips', | 	key: 'clips', | ||||||
| 	title: i18n.ts.clips, | 	title: i18n.ts.clips, | ||||||
| 	icon: 'fas fa-paperclip', | 	icon: 'fas fa-paperclip', | ||||||
| 	onClick: () => { router.push('/@' + getAcct(user) + '/clips'); }, |  | ||||||
| }, { | }, { | ||||||
| 	active: props.page === 'pages', | 	key: 'pages', | ||||||
| 	title: i18n.ts.pages, | 	title: i18n.ts.pages, | ||||||
| 	icon: 'fas fa-file-alt', | 	icon: 'fas fa-file-alt', | ||||||
| 	onClick: () => { router.push('/@' + getAcct(user) + '/pages'); }, |  | ||||||
| }, { | }, { | ||||||
| 	active: props.page === 'gallery', | 	key: 'gallery', | ||||||
| 	title: i18n.ts.gallery, | 	title: i18n.ts.gallery, | ||||||
| 	icon: 'fas fa-icons', | 	icon: 'fas fa-icons', | ||||||
| 	onClick: () => { router.push('/@' + getAcct(user) + '/gallery'); }, |  | ||||||
| }] : null); | }] : null); | ||||||
| 
 | 
 | ||||||
| definePageMetadata(computed(() => user ? { | definePageMetadata(computed(() => user ? { | ||||||
|  | @ -284,291 +116,4 @@ definePageMetadata(computed(() => user ? { | ||||||
| .fade-leave-to { | .fade-leave-to { | ||||||
| 	opacity: 0; | 	opacity: 0; | ||||||
| } | } | ||||||
| 
 |  | ||||||
| .ftskorzw { |  | ||||||
| 
 |  | ||||||
| 	> .main { |  | ||||||
| 
 |  | ||||||
| 		> .punished { |  | ||||||
| 			font-size: 0.8em; |  | ||||||
| 			padding: 16px; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> .profile { |  | ||||||
| 
 |  | ||||||
| 			> .main { |  | ||||||
| 				position: relative; |  | ||||||
| 				overflow: hidden; |  | ||||||
| 
 |  | ||||||
| 				> .banner-container { |  | ||||||
| 					position: relative; |  | ||||||
| 					height: 250px; |  | ||||||
| 					overflow: hidden; |  | ||||||
| 					background-size: cover; |  | ||||||
| 					background-position: center; |  | ||||||
| 
 |  | ||||||
| 					> .banner { |  | ||||||
| 						height: 100%; |  | ||||||
| 						background-color: #4c5e6d; |  | ||||||
| 						background-size: cover; |  | ||||||
| 						background-position: center; |  | ||||||
| 						box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset; |  | ||||||
| 						will-change: background-position; |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					> .fade { |  | ||||||
| 						position: absolute; |  | ||||||
| 						bottom: 0; |  | ||||||
| 						left: 0; |  | ||||||
| 						width: 100%; |  | ||||||
| 						height: 78px; |  | ||||||
| 						background: linear-gradient(transparent, rgba(#000, 0.7)); |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					> .followed { |  | ||||||
| 						position: absolute; |  | ||||||
| 						top: 12px; |  | ||||||
| 						left: 12px; |  | ||||||
| 						padding: 4px 8px; |  | ||||||
| 						color: #fff; |  | ||||||
| 						background: rgba(0, 0, 0, 0.7); |  | ||||||
| 						font-size: 0.7em; |  | ||||||
| 						border-radius: 6px; |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					> .actions { |  | ||||||
| 						position: absolute; |  | ||||||
| 						top: 12px; |  | ||||||
| 						right: 12px; |  | ||||||
| 						-webkit-backdrop-filter: var(--blur, blur(8px)); |  | ||||||
| 						backdrop-filter: var(--blur, blur(8px)); |  | ||||||
| 						background: rgba(0, 0, 0, 0.2); |  | ||||||
| 						padding: 8px; |  | ||||||
| 						border-radius: 24px; |  | ||||||
| 
 |  | ||||||
| 						> .menu { |  | ||||||
| 							vertical-align: bottom; |  | ||||||
| 							height: 31px; |  | ||||||
| 							width: 31px; |  | ||||||
| 							color: #fff; |  | ||||||
| 							text-shadow: 0 0 8px #000; |  | ||||||
| 							font-size: 16px; |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						> .koudoku { |  | ||||||
| 							margin-left: 4px; |  | ||||||
| 							vertical-align: bottom; |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					> .title { |  | ||||||
| 						position: absolute; |  | ||||||
| 						bottom: 0; |  | ||||||
| 						left: 0; |  | ||||||
| 						width: 100%; |  | ||||||
| 						padding: 0 0 8px 154px; |  | ||||||
| 						box-sizing: border-box; |  | ||||||
| 						color: #fff; |  | ||||||
| 
 |  | ||||||
| 						> .name { |  | ||||||
| 							display: block; |  | ||||||
| 							margin: 0; |  | ||||||
| 							line-height: 32px; |  | ||||||
| 							font-weight: bold; |  | ||||||
| 							font-size: 1.8em; |  | ||||||
| 							text-shadow: 0 0 8px #000; |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						> .bottom { |  | ||||||
| 							> * { |  | ||||||
| 								display: inline-block; |  | ||||||
| 								margin-right: 16px; |  | ||||||
| 								line-height: 20px; |  | ||||||
| 								opacity: 0.8; |  | ||||||
| 
 |  | ||||||
| 								&.username { |  | ||||||
| 									font-weight: bold; |  | ||||||
| 								} |  | ||||||
| 							} |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> .title { |  | ||||||
| 					display: none; |  | ||||||
| 					text-align: center; |  | ||||||
| 					padding: 50px 8px 16px 8px; |  | ||||||
| 					font-weight: bold; |  | ||||||
| 					border-bottom: solid 0.5px var(--divider); |  | ||||||
| 
 |  | ||||||
| 					> .bottom { |  | ||||||
| 						> * { |  | ||||||
| 							display: inline-block; |  | ||||||
| 							margin-right: 8px; |  | ||||||
| 							opacity: 0.8; |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> .avatar { |  | ||||||
| 					display: block; |  | ||||||
| 					position: absolute; |  | ||||||
| 					top: 170px; |  | ||||||
| 					left: 16px; |  | ||||||
| 					z-index: 2; |  | ||||||
| 					width: 120px; |  | ||||||
| 					height: 120px; |  | ||||||
| 					box-shadow: 1px 1px 3px rgba(#000, 0.2); |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> .description { |  | ||||||
| 					padding: 24px 24px 24px 154px; |  | ||||||
| 					font-size: 0.95em; |  | ||||||
| 
 |  | ||||||
| 					> .empty { |  | ||||||
| 						margin: 0; |  | ||||||
| 						opacity: 0.5; |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> .fields { |  | ||||||
| 					padding: 24px; |  | ||||||
| 					font-size: 0.9em; |  | ||||||
| 					border-top: solid 0.5px var(--divider); |  | ||||||
| 
 |  | ||||||
| 					> .field { |  | ||||||
| 						display: flex; |  | ||||||
| 						padding: 0; |  | ||||||
| 						margin: 0; |  | ||||||
| 						align-items: center; |  | ||||||
| 
 |  | ||||||
| 						&:not(:last-child) { |  | ||||||
| 							margin-bottom: 8px; |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						> .name { |  | ||||||
| 							width: 30%; |  | ||||||
| 							overflow: hidden; |  | ||||||
| 							white-space: nowrap; |  | ||||||
| 							text-overflow: ellipsis; |  | ||||||
| 							font-weight: bold; |  | ||||||
| 							text-align: center; |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						> .value { |  | ||||||
| 							width: 70%; |  | ||||||
| 							overflow: hidden; |  | ||||||
| 							white-space: nowrap; |  | ||||||
| 							text-overflow: ellipsis; |  | ||||||
| 							margin: 0; |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					&.system > .field > .name { |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> .status { |  | ||||||
| 					display: flex; |  | ||||||
| 					padding: 24px; |  | ||||||
| 					border-top: solid 0.5px var(--divider); |  | ||||||
| 
 |  | ||||||
| 					> a { |  | ||||||
| 						flex: 1; |  | ||||||
| 						text-align: center; |  | ||||||
| 
 |  | ||||||
| 						&.active { |  | ||||||
| 							color: var(--accent); |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						&:hover { |  | ||||||
| 							text-decoration: none; |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						> b { |  | ||||||
| 							display: block; |  | ||||||
| 							line-height: 16px; |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						> span { |  | ||||||
| 							font-size: 70%; |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> .contents { |  | ||||||
| 			> .content { |  | ||||||
| 				margin-bottom: var(--margin); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	&.max-width_500px { |  | ||||||
| 		> .main { |  | ||||||
| 			> .profile > .main { |  | ||||||
| 				> .banner-container { |  | ||||||
| 					height: 140px; |  | ||||||
| 
 |  | ||||||
| 					> .fade { |  | ||||||
| 						display: none; |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					> .title { |  | ||||||
| 						display: none; |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> .title { |  | ||||||
| 					display: block; |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> .avatar { |  | ||||||
| 					top: 90px; |  | ||||||
| 					left: 0; |  | ||||||
| 					right: 0; |  | ||||||
| 					width: 92px; |  | ||||||
| 					height: 92px; |  | ||||||
| 					margin: auto; |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> .description { |  | ||||||
| 					padding: 16px; |  | ||||||
| 					text-align: center; |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> .fields { |  | ||||||
| 					padding: 16px; |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> .status { |  | ||||||
| 					padding: 16px; |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .contents { |  | ||||||
| 				> .nav { |  | ||||||
| 					font-size: 80%; |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	&.wide { |  | ||||||
| 		display: flex; |  | ||||||
| 		width: 100%; |  | ||||||
| 
 |  | ||||||
| 		> .main { |  | ||||||
| 			width: 100%; |  | ||||||
| 			min-width: 0; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> .sub { |  | ||||||
| 			max-width: 350px; |  | ||||||
| 			min-width: 350px; |  | ||||||
| 			margin-left: var(--margin); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -12,15 +12,21 @@ const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({ | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| export const routes = [{ | export const routes = [{ | ||||||
| 	name: 'user', |  | ||||||
| 	path: '/@:acct/:page?', |  | ||||||
| 	component: page(() => import('./pages/user/index.vue')), |  | ||||||
| }, { |  | ||||||
| 	path: '/@:initUser/pages/:initPageName/view-source', | 	path: '/@:initUser/pages/:initPageName/view-source', | ||||||
| 	component: page(() => import('./pages/page-editor/page-editor.vue')), | 	component: page(() => import('./pages/page-editor/page-editor.vue')), | ||||||
| }, { | }, { | ||||||
| 	path: '/@:username/pages/:pageName', | 	path: '/@:username/pages/:pageName', | ||||||
| 	component: page(() => import('./pages/page.vue')), | 	component: page(() => import('./pages/page.vue')), | ||||||
|  | }, { | ||||||
|  | 	path: '/@:acct/following', | ||||||
|  | 	component: page(() => import('./pages/user/following.vue')), | ||||||
|  | }, { | ||||||
|  | 	path: '/@:acct/followers', | ||||||
|  | 	component: page(() => import('./pages/user/followers.vue')), | ||||||
|  | }, { | ||||||
|  | 	name: 'user', | ||||||
|  | 	path: '/@:acct/:page?', | ||||||
|  | 	component: page(() => import('./pages/user/index.vue')), | ||||||
| }, { | }, { | ||||||
| 	name: 'note', | 	name: 'note', | ||||||
| 	path: '/notes/:noteId', | 	path: '/notes/:noteId', | ||||||
|  | @ -55,9 +61,6 @@ export const routes = [{ | ||||||
| }, { | }, { | ||||||
| 	path: '/about-misskey', | 	path: '/about-misskey', | ||||||
| 	component: page(() => import('./pages/about-misskey.vue')), | 	component: page(() => import('./pages/about-misskey.vue')), | ||||||
| }, { |  | ||||||
| 	path: '/featured', |  | ||||||
| 	component: page(() => import('./pages/featured.vue')), |  | ||||||
| }, { | }, { | ||||||
| 	path: '/theme-editor', | 	path: '/theme-editor', | ||||||
| 	component: page(() => import('./pages/theme-editor.vue')), | 	component: page(() => import('./pages/theme-editor.vue')), | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue