enhance(frontend): ユーザーページに「ファイル」タブを新設 (#15130)
* 投稿したファイルの一覧をプロフィールページ内のタブで見れるようにしてみた (Otaku-Social#14) * ギャラリー(ノート)の取得方法を変更、ページネーションに対応 * ギャラリー(ノート)が動作しない問題を修正 * ギャラリー(ノート)の名称変更 * styles * GalleryFromPosts -> Files * fix * enhance: 既存のファイルコンテナの「もっと見る」をクリックしたらファイルタブに飛べるように * Update Changelog * 共通化 * spdx * その他のメディアがちゃんとプレビューされるように * fix(frontend): リストがセンシティブ設定を考慮するように * arrayをsetに変更 * remove unused imports * 🎨 * 🎨 * 画像以外のファイルのプレビューに対応したのでコメントを削除 * サムネイルをMkDriveFileThumbnailに統一 * v-panelに置き換え * lint --------- Co-authored-by: tmorio <morikapusan@morikapu-denki.com> Co-authored-by: tmorio <20278135+tmorio@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										56
									
								
								packages/frontend/src/pages/user/files.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								packages/frontend/src/pages/user/files.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| <!-- | ||||
| SPDX-FileCopyrightText: syuilo and misskey-project | ||||
| SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| 	<MkSpacer :contentMax="1100"> | ||||
| 		<div :class="$style.root"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="pagination"> | ||||
| 				<div :class="$style.stream"> | ||||
| 					<MkNoteMediaGrid v-for="note in items" :note="note" square/> | ||||
| 				</div> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { computed } from 'vue'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
|  | ||||
| import MkNoteMediaGrid from '@/components/MkNoteMediaGrid.vue'; | ||||
| import MkPagination from '@/components/MkPagination.vue'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	user: Misskey.entities.UserDetailed; | ||||
| }>(); | ||||
|  | ||||
| const pagination = { | ||||
| 	endpoint: 'users/notes' as const, | ||||
| 	limit: 15, | ||||
| 	params: computed(() => ({ | ||||
| 		userId: props.user.id, | ||||
| 		withFiles: true, | ||||
| 	})), | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	padding: 8px; | ||||
| } | ||||
|  | ||||
| .stream { | ||||
| 	display: grid; | ||||
| 	grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); | ||||
| 	gap: var(--MI-marginHalf); | ||||
| } | ||||
|  | ||||
| @media screen and (min-width: 600px) { | ||||
| 	.stream { | ||||
| 		grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); | ||||
| 	} | ||||
|  | ||||
| } | ||||
| </style> | ||||
| @@ -136,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 				<MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo> | ||||
| 				<template v-if="narrow"> | ||||
| 					<MkLazy> | ||||
| 						<XFiles :key="user.id" :user="user"/> | ||||
| 						<XFiles :key="user.id" :user="user" @unfold="emit('unfoldFiles')"/> | ||||
| 					</MkLazy> | ||||
| 					<MkLazy> | ||||
| 						<XActivity :key="user.id" :user="user"/> | ||||
| @@ -150,7 +150,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;"> | ||||
| 			<XFiles :key="user.id" :user="user"/> | ||||
| 			<XFiles :key="user.id" :user="user" @unfold="emit('unfoldFiles')"/> | ||||
| 			<XActivity :key="user.id" :user="user"/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| @@ -212,6 +212,10 @@ const props = withDefaults(defineProps<{ | ||||
| 	disableNotes: false, | ||||
| }); | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'unfoldFiles'): void; | ||||
| }>(); | ||||
|  | ||||
| const router = useRouter(); | ||||
|  | ||||
| const user = ref(props.user); | ||||
|   | ||||
| @@ -4,30 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| <MkContainer :max-height="300" :foldable="true"> | ||||
| <MkContainer :max-height="300" :foldable="true" :onUnfold="unfoldContainer"> | ||||
| 	<template #icon><i class="ti ti-photo"></i></template> | ||||
| 	<template #header>{{ i18n.ts.files }}</template> | ||||
| 	<div :class="$style.root"> | ||||
| 		<MkLoading v-if="fetching"/> | ||||
| 		<div v-if="!fetching && files.length > 0" :class="$style.stream"> | ||||
| 			<template v-for="file in files" :key="file.note.id + file.file.id"> | ||||
| 				<div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.img" @click="showingFiles.push(file.file.id)"> | ||||
| 					<!-- TODO: 画像以外のファイルに対応 --> | ||||
| 					<ImgWithBlurhash :class="$style.sensitiveImg" :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name" :forceBlurhash="true"/> | ||||
| 					<div :class="$style.sensitive"> | ||||
| 						<div> | ||||
| 							<div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div> | ||||
| 							<div>{{ i18n.ts.clickToShow }}</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<MkA v-else :class="$style.img" :to="notePage(file.note)"> | ||||
| 					<!-- TODO: 画像以外のファイルに対応 --> | ||||
| 					<ImgWithBlurhash :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name"/> | ||||
| 				</MkA> | ||||
| 			</template> | ||||
| 		<div v-if="!fetching && notes.length > 0" :class="$style.stream"> | ||||
| 			<MkNoteMediaGrid v-for="note in notes" :note="note"/> | ||||
| 		</div> | ||||
| 		<p v-if="!fetching && files.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p> | ||||
| 		<p v-if="!fetching && notes.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p> | ||||
| 	</div> | ||||
| </MkContainer> | ||||
| </template> | ||||
| @@ -35,45 +20,34 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, ref } from 'vue'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { getStaticImageUrl } from '@/scripts/media-proxy.js'; | ||||
| import { notePage } from '@/filters/note.js'; | ||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | ||||
| import MkContainer from '@/components/MkContainer.vue'; | ||||
| import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import MkNoteMediaGrid from '@/components/MkNoteMediaGrid.vue'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	user: Misskey.entities.UserDetailed; | ||||
| }>(); | ||||
|  | ||||
| const fetching = ref(true); | ||||
| const files = ref<{ | ||||
| 	note: Misskey.entities.Note; | ||||
| 	file: Misskey.entities.DriveFile; | ||||
| }[]>([]); | ||||
| const showingFiles = ref<string[]>([]); | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'unfold'): void; | ||||
| }>(); | ||||
|  | ||||
| function thumbnail(image: Misskey.entities.DriveFile): string { | ||||
| 	return defaultStore.state.disableShowingAnimatedImages | ||||
| 		? getStaticImageUrl(image.url) | ||||
| 		: image.thumbnailUrl; | ||||
| const fetching = ref(true); | ||||
| const notes = ref<Misskey.entities.Note[]>([]); | ||||
|  | ||||
| function unfoldContainer(): boolean { | ||||
| 	emit('unfold'); | ||||
| 	return false; | ||||
| } | ||||
|  | ||||
| onMounted(() => { | ||||
| 	misskeyApi('users/notes', { | ||||
| 		userId: props.user.id, | ||||
| 		withFiles: true, | ||||
| 		limit: 15, | ||||
| 	}).then(notes => { | ||||
| 		for (const note of notes) { | ||||
| 			for (const file of note.files) { | ||||
| 				files.value.push({ | ||||
| 					note, | ||||
| 					file, | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 		limit: 10, | ||||
| 	}).then(_notes => { | ||||
| 		notes.value = _notes; | ||||
| 		fetching.value = false; | ||||
| 	}); | ||||
| }); | ||||
|   | ||||
| @@ -9,10 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	<div> | ||||
| 		<div v-if="user"> | ||||
| 			<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> | ||||
| 				<XHome v-if="tab === 'home'" key="home" :user="user"/> | ||||
| 				<XHome v-if="tab === 'home'" key="home" :user="user" @unfoldFiles="() => { tab = 'files'; }"/> | ||||
| 				<MkSpacer v-else-if="tab === 'notes'" key="notes" :contentMax="800" style="padding-top: 0"> | ||||
| 					<XTimeline :user="user"/> | ||||
| 				</MkSpacer> | ||||
| 				<XFiles v-else-if="tab === 'files'" :user="user"/> | ||||
| 				<XActivity v-else-if="tab === 'activity'" key="activity" :user="user"/> | ||||
| 				<XAchievements v-else-if="tab === 'achievements'" key="achievements" :user="user"/> | ||||
| 				<XReactions v-else-if="tab === 'reactions'" key="reactions" :user="user"/> | ||||
| @@ -43,6 +44,7 @@ import { serverContext, assertServerContext } from '@/server-context.js'; | ||||
|  | ||||
| const XHome = defineAsyncComponent(() => import('./home.vue')); | ||||
| const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue')); | ||||
| const XFiles = defineAsyncComponent(() => import('./files.vue')); | ||||
| const XActivity = defineAsyncComponent(() => import('./activity.vue')); | ||||
| const XAchievements = defineAsyncComponent(() => import('./achievements.vue')); | ||||
| const XReactions = defineAsyncComponent(() => import('./reactions.vue')); | ||||
| @@ -103,6 +105,10 @@ const headerTabs = computed(() => user.value ? [{ | ||||
| 	key: 'notes', | ||||
| 	title: i18n.ts.notes, | ||||
| 	icon: 'ti ti-pencil', | ||||
| }, { | ||||
| 	key: 'files', | ||||
| 	title: i18n.ts.files, | ||||
| 	icon: 'ti ti-photo', | ||||
| }, { | ||||
| 	key: 'activity', | ||||
| 	title: i18n.ts.activity, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 かっこかり
					かっこかり