v12 (#5712)
Co-authored-by: MeiMei <30769358+mei23@users.noreply.github.com> Co-authored-by: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										140
									
								
								src/client/pages/user/follow-list.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/client/pages/user/follow-list.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,140 @@
 | 
			
		||||
<template>
 | 
			
		||||
<mk-pagination :pagination="pagination" #default="{items}" class="mk-following-or-followers" ref="list">
 | 
			
		||||
	<div class="user _panel" v-for="(user, i) in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" :data-index="i">
 | 
			
		||||
		<mk-avatar class="avatar" :user="user"/>
 | 
			
		||||
		<div class="body">
 | 
			
		||||
			<div class="name">
 | 
			
		||||
				<router-link class="name" :to="user | userPage" v-user-preview="user.id"><mk-user-name :user="user"/></router-link>
 | 
			
		||||
				<p class="acct">@{{ user | acct }}</p>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="description" v-if="user.description" :title="user.description">
 | 
			
		||||
				<mfm :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :plain="true" :nowrap="true"/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<x-follow-button class="koudoku-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</mk-pagination>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import parseAcct from '../../../misc/acct/parse';
 | 
			
		||||
import i18n from '../../i18n';
 | 
			
		||||
import XFollowButton from '../../components/follow-button.vue';
 | 
			
		||||
import MkPagination from '../../components/ui/pagination.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n,
 | 
			
		||||
 | 
			
		||||
	components: {
 | 
			
		||||
		MkPagination,
 | 
			
		||||
		XFollowButton,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		type: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: true
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			pagination: {
 | 
			
		||||
				endpoint: () => this.type === 'following' ? 'users/following' : 'users/followers',
 | 
			
		||||
				limit: 20,
 | 
			
		||||
				params: {
 | 
			
		||||
					...parseAcct(this.$route.params.user),
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		type() {
 | 
			
		||||
			this.$refs.list.reload();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		'$route'() {
 | 
			
		||||
			this.$refs.list.reload();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.mk-following-or-followers {
 | 
			
		||||
	> .user {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		padding: 16px;
 | 
			
		||||
 | 
			
		||||
		> .avatar {
 | 
			
		||||
			display: block;
 | 
			
		||||
			flex-shrink: 0;
 | 
			
		||||
			margin: 0 12px 0 0;
 | 
			
		||||
			width: 42px;
 | 
			
		||||
			height: 42px;
 | 
			
		||||
			border-radius: 8px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .body {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			width: calc(100% - 54px);
 | 
			
		||||
			position: relative;
 | 
			
		||||
 | 
			
		||||
			> .name {
 | 
			
		||||
				width: 45%;
 | 
			
		||||
 | 
			
		||||
				@media (max-width: 500px) {
 | 
			
		||||
					width: 100%;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .name,
 | 
			
		||||
				> .acct {
 | 
			
		||||
					display: block;
 | 
			
		||||
					white-space: nowrap;
 | 
			
		||||
					text-overflow: ellipsis;
 | 
			
		||||
					overflow: hidden;
 | 
			
		||||
					margin: 0;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .name {
 | 
			
		||||
					font-size: 16px;
 | 
			
		||||
					line-height: 24px;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .acct {
 | 
			
		||||
					font-size: 15px;
 | 
			
		||||
					line-height: 16px;
 | 
			
		||||
					opacity: 0.7;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .description {
 | 
			
		||||
				width: 55%;
 | 
			
		||||
				line-height: 42px;
 | 
			
		||||
				white-space: nowrap;
 | 
			
		||||
				overflow: hidden;
 | 
			
		||||
				text-overflow: ellipsis;
 | 
			
		||||
				opacity: 0.7;
 | 
			
		||||
				font-size: 14px;
 | 
			
		||||
				padding-right: 40px;
 | 
			
		||||
				padding-left: 8px;
 | 
			
		||||
				box-sizing: border-box;
 | 
			
		||||
 | 
			
		||||
				@media (max-width: 500px) {
 | 
			
		||||
					display: none;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .koudoku-button {
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				top: 0;
 | 
			
		||||
				bottom: 0;
 | 
			
		||||
				right: 0;
 | 
			
		||||
				margin: auto 0;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										114
									
								
								src/client/pages/user/index.activity.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/client/pages/user/index.activity.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<div ref="chart"></div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import ApexCharts from 'apexcharts';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	props: {
 | 
			
		||||
		user: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		limit: {
 | 
			
		||||
			type: Number,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: 40
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			fetching: true,
 | 
			
		||||
			data: [],
 | 
			
		||||
			peak: null
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$root.api('charts/user/notes', {
 | 
			
		||||
			userId: this.user.id,
 | 
			
		||||
			span: 'day',
 | 
			
		||||
			limit: this.limit
 | 
			
		||||
		}).then(stats => {
 | 
			
		||||
			const normal = [];
 | 
			
		||||
			const reply = [];
 | 
			
		||||
			const renote = [];
 | 
			
		||||
 | 
			
		||||
			const now = new Date();
 | 
			
		||||
			const y = now.getFullYear();
 | 
			
		||||
			const m = now.getMonth();
 | 
			
		||||
			const d = now.getDate();
 | 
			
		||||
 | 
			
		||||
			for (let i = 0; i < this.limit; i++) {
 | 
			
		||||
				const x = new Date(y, m, d - i);
 | 
			
		||||
				normal.push([
 | 
			
		||||
					x,
 | 
			
		||||
					stats.diffs.normal[i]
 | 
			
		||||
				]);
 | 
			
		||||
				reply.push([
 | 
			
		||||
					x,
 | 
			
		||||
					stats.diffs.reply[i]
 | 
			
		||||
				]);
 | 
			
		||||
				renote.push([
 | 
			
		||||
					x,
 | 
			
		||||
					stats.diffs.renote[i]
 | 
			
		||||
				]);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const chart = new ApexCharts(this.$refs.chart, {
 | 
			
		||||
				chart: {
 | 
			
		||||
					type: 'bar',
 | 
			
		||||
					stacked: true,
 | 
			
		||||
					height: 100,
 | 
			
		||||
					sparkline: {
 | 
			
		||||
						enabled: true
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				plotOptions: {
 | 
			
		||||
					bar: {
 | 
			
		||||
						columnWidth: '40%'
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				dataLabels: {
 | 
			
		||||
					enabled: false
 | 
			
		||||
				},
 | 
			
		||||
				grid: {
 | 
			
		||||
					clipMarkers: false,
 | 
			
		||||
					padding: {
 | 
			
		||||
						top: 0,
 | 
			
		||||
						right: 8,
 | 
			
		||||
						bottom: 0,
 | 
			
		||||
						left: 8
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
				tooltip: {
 | 
			
		||||
					shared: true,
 | 
			
		||||
					intersect: false
 | 
			
		||||
				},
 | 
			
		||||
				series: [{
 | 
			
		||||
					name: 'Normal',
 | 
			
		||||
					data: normal
 | 
			
		||||
				}, {
 | 
			
		||||
					name: 'Reply',
 | 
			
		||||
					data: reply
 | 
			
		||||
				}, {
 | 
			
		||||
					name: 'Renote',
 | 
			
		||||
					data: renote
 | 
			
		||||
				}],
 | 
			
		||||
				xaxis: {
 | 
			
		||||
					type: 'datetime',
 | 
			
		||||
					crosshairs: {
 | 
			
		||||
						width: 1,
 | 
			
		||||
						opacity: 1
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			chart.render();
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										98
									
								
								src/client/pages/user/index.photos.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								src/client/pages/user/index.photos.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,98 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="ujigsodd">
 | 
			
		||||
	<mk-loading v-if="fetching"/>
 | 
			
		||||
	<div class="stream" v-if="!fetching && images.length > 0">
 | 
			
		||||
		<a v-for="(image, i) in images" :key="i"
 | 
			
		||||
			class="img"
 | 
			
		||||
			:style="`background-image: url(${thumbnail(image.file)})`"
 | 
			
		||||
			:href="image.note | notePage"
 | 
			
		||||
		></a>
 | 
			
		||||
	</div>
 | 
			
		||||
	<p class="empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import i18n from '../../i18n';
 | 
			
		||||
import { getStaticImageUrl } from '../../scripts/get-static-image-url';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	i18n,
 | 
			
		||||
	props: ['user'],
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			fetching: true,
 | 
			
		||||
			images: []
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		const image = [
 | 
			
		||||
			'image/jpeg',
 | 
			
		||||
			'image/png',
 | 
			
		||||
			'image/gif',
 | 
			
		||||
			'image/apng',
 | 
			
		||||
			'image/vnd.mozilla.apng',
 | 
			
		||||
		];
 | 
			
		||||
		this.$root.api('users/notes', {
 | 
			
		||||
			userId: this.user.id,
 | 
			
		||||
			fileType: image,
 | 
			
		||||
			excludeNsfw: !this.$store.state.device.alwaysShowNsfw,
 | 
			
		||||
			limit: 9,
 | 
			
		||||
		}).then(notes => {
 | 
			
		||||
			for (const note of notes) {
 | 
			
		||||
				for (const file of note.files) {
 | 
			
		||||
					if (this.images.length < 9) {
 | 
			
		||||
						this.images.push({
 | 
			
		||||
							note,
 | 
			
		||||
							file
 | 
			
		||||
						});
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			this.fetching = false;
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		thumbnail(image: any): string {
 | 
			
		||||
			return this.$store.state.device.disableShowingAnimatedImages
 | 
			
		||||
				? getStaticImageUrl(image.thumbnailUrl)
 | 
			
		||||
				: image.thumbnailUrl;
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.ujigsodd {
 | 
			
		||||
 | 
			
		||||
	> .stream {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		justify-content: center;
 | 
			
		||||
		flex-wrap: wrap;
 | 
			
		||||
		padding: 8px;
 | 
			
		||||
 | 
			
		||||
		> .img {
 | 
			
		||||
			flex: 1 1 33%;
 | 
			
		||||
			width: 33%;
 | 
			
		||||
			height: 90px;
 | 
			
		||||
			box-sizing: border-box;
 | 
			
		||||
			background-position: center center;
 | 
			
		||||
			background-size: cover;
 | 
			
		||||
			background-clip: content-box;
 | 
			
		||||
			border: solid 2px transparent;
 | 
			
		||||
			border-radius: 4px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .empty {
 | 
			
		||||
		margin: 0;
 | 
			
		||||
		padding: 16px;
 | 
			
		||||
		text-align: center;
 | 
			
		||||
 | 
			
		||||
		> i {
 | 
			
		||||
			margin-right: 4px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										79
									
								
								src/client/pages/user/index.timeline.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/client/pages/user/index.timeline.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="kjeftjfm">
 | 
			
		||||
	<div class="with">
 | 
			
		||||
		<button class="_button" @click="with_ = null" :class="{ active: with_ === null }">{{ $t('notes') }}</button>
 | 
			
		||||
		<button class="_button" @click="with_ = 'replies'" :class="{ active: with_ === 'replies' }">{{ $t('notesAndReplies') }}</button>
 | 
			
		||||
		<button class="_button" @click="with_ = 'files'" :class="{ active: with_ === 'files' }">{{ $t('withFiles') }}</button>
 | 
			
		||||
	</div>
 | 
			
		||||
	<x-notes ref="timeline" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import XNotes from '../../components/notes.vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		XNotes
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		user: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		user() {
 | 
			
		||||
			this.$refs.timeline.reload();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		with_() {
 | 
			
		||||
			this.$refs.timeline.reload();
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			date: null,
 | 
			
		||||
			with_: null,
 | 
			
		||||
			pagination: {
 | 
			
		||||
				endpoint: 'users/notes',
 | 
			
		||||
				limit: 10,
 | 
			
		||||
				params: init => ({
 | 
			
		||||
					userId: this.user.id,
 | 
			
		||||
					includeReplies: this.with_ === 'replies',
 | 
			
		||||
					withFiles: this.with_ === 'files',
 | 
			
		||||
					untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.kjeftjfm {
 | 
			
		||||
	> .with {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		margin-bottom: var(--margin);
 | 
			
		||||
 | 
			
		||||
		@media (max-width: 500px) {
 | 
			
		||||
			font-size: 80%;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> button {
 | 
			
		||||
			flex: 1;
 | 
			
		||||
			padding: 11px 8px 8px 8px;
 | 
			
		||||
			border-bottom: solid 3px transparent;
 | 
			
		||||
 | 
			
		||||
			&.active {
 | 
			
		||||
				color: var(--accent);
 | 
			
		||||
				border-bottom-color: var(--accent);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										476
									
								
								src/client/pages/user/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										476
									
								
								src/client/pages/user/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,476 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mk-user-page" v-if="user">
 | 
			
		||||
	<portal to="title" v-if="user"><mk-user-name :user="user" :nowrap="false" class="name"/></portal>
 | 
			
		||||
	<portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
 | 
			
		||||
	
 | 
			
		||||
	<div class="remote-caution _panel" v-if="user.host != null"><fa :icon="faExclamationTriangle" style="margin-right: 8px;"/>{{ $t('remoteUserCaution') }}<a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('showOnRemote') }}</a></div>
 | 
			
		||||
	<transition name="zoom" mode="out-in" appear>
 | 
			
		||||
		<div class="profile _panel" :key="user.id">
 | 
			
		||||
			<div class="banner-container" :style="style">
 | 
			
		||||
				<div class="banner" ref="banner" :style="style"></div>
 | 
			
		||||
				<div class="fade"></div>
 | 
			
		||||
				<div class="title">
 | 
			
		||||
					<mk-user-name class="name" :user="user" :nowrap="true"/>
 | 
			
		||||
					<div class="bottom">
 | 
			
		||||
						<span class="username"><mk-acct :user="user" :detail="true" /></span>
 | 
			
		||||
						<span v-if="user.isAdmin" :title="$t('isAdmin')"><fa :icon="faBookmark"/></span>
 | 
			
		||||
						<span v-if="user.isLocked" :title="$t('isLocked')"><fa :icon="faLock"/></span>
 | 
			
		||||
						<span v-if="user.isBot" :title="$t('isBot')"><fa :icon="faRobot"/></span>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span>
 | 
			
		||||
				<div class="actions" v-if="$store.getters.isSignedIn">
 | 
			
		||||
					<button @click="menu" class="menu _button" ref="menu"><fa :icon="faEllipsisH"/></button>
 | 
			
		||||
					<x-follow-button v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" class="koudoku"/>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
 | 
			
		||||
			<div class="title">
 | 
			
		||||
				<mk-user-name :user="user" :nowrap="false" class="name"/>
 | 
			
		||||
				<div class="bottom">
 | 
			
		||||
					<span class="username"><mk-acct :user="user" :detail="true" /></span>
 | 
			
		||||
					<span v-if="user.isAdmin" :title="$t('isAdmin')"><fa :icon="faBookmark"/></span>
 | 
			
		||||
					<span v-if="user.isLocked" :title="$t('isLocked')"><fa :icon="faLock"/></span>
 | 
			
		||||
					<span v-if="user.isBot" :title="$t('isBot')"><fa :icon="faRobot"/></span>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="description">
 | 
			
		||||
				<mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
 | 
			
		||||
				<p v-else class="empty">{{ $t('noAccountDescription') }}</p>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="fields system">
 | 
			
		||||
				<dl class="field" v-if="user.location">
 | 
			
		||||
					<dt class="name"><fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt>
 | 
			
		||||
					<dd class="value">{{ user.location }}</dd>
 | 
			
		||||
				</dl>
 | 
			
		||||
				<dl class="field" v-if="user.birthday">
 | 
			
		||||
					<dt class="name"><fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt>
 | 
			
		||||
					<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
 | 
			
		||||
				</dl>
 | 
			
		||||
				<dl class="field">
 | 
			
		||||
					<dt class="name"><fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt>
 | 
			
		||||
					<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<mk-time :time="user.createdAt"/>)</dd>
 | 
			
		||||
				</dl>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="fields" v-if="user.fields.length > 0">
 | 
			
		||||
				<dl class="field" v-for="(field, i) in user.fields" :key="i">
 | 
			
		||||
					<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="$store.state.i" :custom-emojis="user.emojis" :colored="false"/>
 | 
			
		||||
					</dd>
 | 
			
		||||
				</dl>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="status" v-if="user.host === null">
 | 
			
		||||
				<router-link :to="user | userPage()" :class="{ active: $route.name === 'user' }">
 | 
			
		||||
					<b>{{ user.notesCount | number }}</b>
 | 
			
		||||
					<span>{{ $t('notes') }}</span>
 | 
			
		||||
				</router-link>
 | 
			
		||||
				<router-link :to="user | userPage('following')" :class="{ active: $route.name === 'userFollowing' }">
 | 
			
		||||
					<b>{{ user.followingCount | number }}</b>
 | 
			
		||||
					<span>{{ $t('following') }}</span>
 | 
			
		||||
				</router-link>
 | 
			
		||||
				<router-link :to="user | userPage('followers')" :class="{ active: $route.name === 'userFollowers' }">
 | 
			
		||||
					<b>{{ user.followersCount | number }}</b>
 | 
			
		||||
					<span>{{ $t('followers') }}</span>
 | 
			
		||||
				</router-link>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</transition>
 | 
			
		||||
	<router-view :user="user"></router-view>
 | 
			
		||||
	<template v-if="$route.name == 'user'">
 | 
			
		||||
		<sequential-entrance class="pins">
 | 
			
		||||
			<x-note v-for="(note, i) in user.pinnedNotes" class="note" :note="note" :key="note.id" :data-index="i" :detail="true" :pinned="true"/>
 | 
			
		||||
		</sequential-entrance>
 | 
			
		||||
		<mk-container :body-togglable="true" class="content">
 | 
			
		||||
			<template #header><fa :icon="faImage"/>{{ $t('images') }}</template>
 | 
			
		||||
			<div>
 | 
			
		||||
				<x-photos :user="user" :key="user.id"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</mk-container>
 | 
			
		||||
		<mk-container :body-togglable="true" class="content">
 | 
			
		||||
			<template #header><fa :icon="faChartBar"/>{{ $t('activity') }}</template>
 | 
			
		||||
			<div style="padding:8px;">
 | 
			
		||||
				<x-activity :user="user" :key="user.id"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</mk-container>
 | 
			
		||||
		<x-user-timeline :user="user"/>
 | 
			
		||||
	</template>
 | 
			
		||||
</div>
 | 
			
		||||
<div v-else-if="error">
 | 
			
		||||
	<mk-error @retry="fetch()"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { faEllipsisH, faRobot, faLock, faBookmark, faExclamationTriangle, faChartBar, faImage, faBirthdayCake, faMapMarker } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons';
 | 
			
		||||
import * as age from 's-age';
 | 
			
		||||
import XUserTimeline from './index.timeline.vue';
 | 
			
		||||
import XUserMenu from '../../components/user-menu.vue';
 | 
			
		||||
import XNote from '../../components/note.vue';
 | 
			
		||||
import XFollowButton from '../../components/follow-button.vue';
 | 
			
		||||
import MkContainer from '../../components/ui/container.vue';
 | 
			
		||||
import Progress from '../../scripts/loading';
 | 
			
		||||
import parseAcct from '../../../misc/acct/parse';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		XUserTimeline,
 | 
			
		||||
		XNote,
 | 
			
		||||
		XFollowButton,
 | 
			
		||||
		MkContainer,
 | 
			
		||||
		XPhotos: () => import('./index.photos.vue').then(m => m.default),
 | 
			
		||||
		XActivity: () => import('./index.activity.vue').then(m => m.default),
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	metaInfo() {
 | 
			
		||||
		return {
 | 
			
		||||
			title: (this.user ? '@' + Vue.filter('acct')(this.user).replace('@', ' | ') : null) as string
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			user: null,
 | 
			
		||||
			error: null,
 | 
			
		||||
			parallaxAnimationId: null,
 | 
			
		||||
			faEllipsisH, faRobot, faLock, faBookmark, faExclamationTriangle, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		style(): any {
 | 
			
		||||
			if (this.user.bannerUrl == null) return {};
 | 
			
		||||
			return {
 | 
			
		||||
				backgroundImage: `url(${ this.user.bannerUrl })`
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		age(): number {
 | 
			
		||||
			return age(this.user.birthday);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		$route: 'fetch'
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.fetch();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		window.requestAnimationFrame(this.parallaxLoop);
 | 
			
		||||
		window.addEventListener('scroll', this.parallax, { passive: true });
 | 
			
		||||
		document.addEventListener('touchmove', this.parallax, { passive: true });
 | 
			
		||||
		this.$once('hook:beforeDestroy', () => {
 | 
			
		||||
			window.cancelAnimationFrame(this.parallaxAnimationId);
 | 
			
		||||
			window.removeEventListener('scroll', this.parallax);
 | 
			
		||||
			document.removeEventListener('touchmove', this.parallax);
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		fetch() {
 | 
			
		||||
			Progress.start();
 | 
			
		||||
			this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => {
 | 
			
		||||
				this.user = user;
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				this.error = e;
 | 
			
		||||
			}).finally(() => {
 | 
			
		||||
				Progress.done();
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		menu() {
 | 
			
		||||
			this.$root.new(XUserMenu, {
 | 
			
		||||
				source: this.$refs.menu,
 | 
			
		||||
				user: this.user
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		parallaxLoop() {
 | 
			
		||||
			this.parallaxAnimationId = window.requestAnimationFrame(this.parallaxLoop);
 | 
			
		||||
			this.parallax();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		parallax() {
 | 
			
		||||
			const banner = this.$refs.banner as any;
 | 
			
		||||
			if (banner == null) return;
 | 
			
		||||
 | 
			
		||||
			const top = window.scrollY;
 | 
			
		||||
 | 
			
		||||
			if (top < 0) return;
 | 
			
		||||
 | 
			
		||||
			const z = 1.75; // 奥行き(小さいほど奥)
 | 
			
		||||
			const pos = -(top / z);
 | 
			
		||||
			banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.mk-user-page {
 | 
			
		||||
	> .remote-caution {
 | 
			
		||||
		font-size: 0.8em;
 | 
			
		||||
		padding: 16px;
 | 
			
		||||
		margin-bottom: var(--margin);
 | 
			
		||||
 | 
			
		||||
		> a {
 | 
			
		||||
			margin-left: 4px;
 | 
			
		||||
			color: var(--accent);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .profile {
 | 
			
		||||
		position: relative;
 | 
			
		||||
		margin-bottom: var(--margin);
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
 | 
			
		||||
		> .banner-container {
 | 
			
		||||
			position: relative;
 | 
			
		||||
			height: 250px;
 | 
			
		||||
			overflow: hidden;
 | 
			
		||||
			background-size: cover;
 | 
			
		||||
			background-position: center;
 | 
			
		||||
 | 
			
		||||
			@media (max-width: 500px) {
 | 
			
		||||
				height: 140px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .banner {
 | 
			
		||||
				height: 100%;
 | 
			
		||||
				background-color: #4c5e6d;
 | 
			
		||||
				background-size: cover;
 | 
			
		||||
				background-position: center;
 | 
			
		||||
				box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .fade {
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				bottom: 0;
 | 
			
		||||
				left: 0;
 | 
			
		||||
				width: 100%;
 | 
			
		||||
				height: 78px;
 | 
			
		||||
				background: linear-gradient(transparent, rgba(#000, 0.7));
 | 
			
		||||
 | 
			
		||||
				@media (max-width: 500px) {
 | 
			
		||||
					display: none;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .followed {
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				top: 12px;
 | 
			
		||||
				left: 12px;
 | 
			
		||||
				padding: 4px 6px;
 | 
			
		||||
				color: #fff;
 | 
			
		||||
				background: rgba(0, 0, 0, 0.7);
 | 
			
		||||
				font-size: 12px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .actions {
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				top: 12px;
 | 
			
		||||
				right: 12px;
 | 
			
		||||
				-webkit-backdrop-filter: blur(8px);
 | 
			
		||||
				backdrop-filter: 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;
 | 
			
		||||
 | 
			
		||||
				@media (max-width: 500px) {
 | 
			
		||||
					display: none;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .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 1px var(--divider);
 | 
			
		||||
 | 
			
		||||
			@media (max-width: 500px) {
 | 
			
		||||
				display: block;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .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);
 | 
			
		||||
 | 
			
		||||
			@media (max-width: 500px) {
 | 
			
		||||
				top: 90px;
 | 
			
		||||
				left: 0;
 | 
			
		||||
				right: 0;
 | 
			
		||||
				width: 92px;
 | 
			
		||||
				height: 92px;
 | 
			
		||||
				margin: auto;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .description {
 | 
			
		||||
			padding: 24px 24px 24px 154px;
 | 
			
		||||
			font-size: 15px;
 | 
			
		||||
 | 
			
		||||
			@media (max-width: 500px) {
 | 
			
		||||
				padding: 16px;
 | 
			
		||||
				text-align: center;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .empty {
 | 
			
		||||
				margin: 0;
 | 
			
		||||
				opacity: 0.5;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .fields {
 | 
			
		||||
			padding: 24px;
 | 
			
		||||
			font-size: 14px;
 | 
			
		||||
			border-top: solid 1px var(--divider);
 | 
			
		||||
 | 
			
		||||
			@media (max-width: 500px) {
 | 
			
		||||
				padding: 16px;
 | 
			
		||||
			}
 | 
			
		||||
		
 | 
			
		||||
			> .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;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.system > .field > .name {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .status {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			padding: 24px;
 | 
			
		||||
			border-top: solid 1px var(--divider);
 | 
			
		||||
 | 
			
		||||
			@media (max-width: 500px) {
 | 
			
		||||
				padding: 16px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> a {
 | 
			
		||||
				flex: 1;
 | 
			
		||||
				text-align: center;
 | 
			
		||||
 | 
			
		||||
				&.active {
 | 
			
		||||
					color: var(--accent);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:hover {
 | 
			
		||||
					text-decoration: none;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> b {
 | 
			
		||||
					display: block;
 | 
			
		||||
					line-height: 16px;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> span {
 | 
			
		||||
					font-size: 70%;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .pins {
 | 
			
		||||
		> .note {
 | 
			
		||||
			margin-bottom: var(--margin);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .content {
 | 
			
		||||
		margin-bottom: var(--margin);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
		Reference in New Issue
	
	Block a user