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:
かっこかり
2025-01-14 21:05:34 +09:00
committed by GitHub
parent 71cecdbcf2
commit 40f8b5e7f5
8 changed files with 217 additions and 54 deletions

View 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>

View File

@@ -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);

View File

@@ -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;
});
});

View File

@@ -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,