summaryrefslogtreecommitdiff
path: root/packages/frontend/src/pages/user
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-05-09 17:40:08 +0900
committerGitHub <noreply@github.com>2025-05-09 17:40:08 +0900
commit8c2ab25e5f2040fcbc81bc2a02a279fed40e1c11 (patch)
treeae0d3573bd5a3175bc6174d33129dc64205a1436 /packages/frontend/src/pages/user
parentrefactor (diff)
downloadmisskey-8c2ab25e5f2040fcbc81bc2a02a279fed40e1c11.tar.gz
misskey-8c2ab25e5f2040fcbc81bc2a02a279fed40e1c11.tar.bz2
misskey-8c2ab25e5f2040fcbc81bc2a02a279fed40e1c11.zip
Feat: No websocket mode (#15851)
* wip * wip * wip * wip * Update MkTimeline.vue * wip * wip * wip * Update MkTimeline.vue * Update use-pagination.ts * wip * wip * Update MkTimeline.vue * Update MkTimeline.vue * wip * wip * Update MkTimeline.vue * Update MkTimeline.vue * Update MkTimeline.vue * wip * Update use-pagination.ts * wip * Update use-pagination.ts * Update MkNotifications.vue * Update MkNotifications.vue * wip * wip * wip * Update use-note-capture.ts * Update use-note-capture.ts * Update use-note-capture.ts * wip * wip * wip * wip * Update MkNoteDetailed.vue * wip * wip * Update MkTimeline.vue * wip * fix * Update MkTimeline.vue * wip * test * Revert "test" This reverts commit 3375619396c54dcda5e564eb1da444c2391208c9. * Update use-pagination.ts * test * Revert "test" This reverts commit 42c53c830e28485d2fb49061fa7cdeee31bc6a22. * test * Revert "test" This reverts commit c4f8cda4aa1cec9d1eb97557145f3ad3d2d0e469. * Update style.scss * Update MkTimeline.vue * Update MkTimeline.vue * Update MkTimeline.vue * ✌️ * Update MkTimeline.vue * wip * wip * test * Update MkPullToRefresh.vue * Update MkPullToRefresh.vue * Update MkPullToRefresh.vue * Update MkPullToRefresh.vue * Update MkTimeline.vue * wip * tweak navbar * wip * wip * wip * wip * wip * wip * wip * Update home.vue * wip * refactor * wip * wip * Update note.vue * Update navbar.vue * Update MkPullToRefresh.vue * Update MkPullToRefresh.vue * Update MkPullToRefresh.vue * wip * Update MkStreamingNotificationsTimeline.vue * Update use-pagination.ts * wip * improve perf * wip * Update MkNotesTimeline.vue * wip * megre * Update use-pagination.ts * Update use-pagination.ts * Update MkStreamingNotesTimeline.vue * Update use-pagination.ts * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md
Diffstat (limited to 'packages/frontend/src/pages/user')
-rw-r--r--packages/frontend/src/pages/user/files.vue16
-rw-r--r--packages/frontend/src/pages/user/home.vue269
-rw-r--r--packages/frontend/src/pages/user/index.timeline.vue8
-rw-r--r--packages/frontend/src/pages/user/index.vue6
-rw-r--r--packages/frontend/src/pages/user/notes.vue67
5 files changed, 219 insertions, 147 deletions
diff --git a/packages/frontend/src/pages/user/files.vue b/packages/frontend/src/pages/user/files.vue
index 91ebcad0b2..51ae809aac 100644
--- a/packages/frontend/src/pages/user/files.vue
+++ b/packages/frontend/src/pages/user/files.vue
@@ -4,15 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
- <div class="_spacer" style="--MI_SPACER-w: 1100px;">
- <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>
+<div class="_spacer" style="--MI_SPACER-w: 1100px;">
+ <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>
+</div>
</template>
<script lang="ts" setup>
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 50bb1de24f..23f740ddd0 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -4,158 +4,160 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div class="_spacer" :style="{ '--MI_SPACER-w': narrow ? '800px' : '1100px' }">
- <div ref="rootEl" class="ftskorzw" :class="{ wide: !narrow }" style="container-type: inline-size;">
- <div class="main _gaps">
- <!-- TODO -->
- <!-- <div class="punished" v-if="user.isSuspended"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSuspended }}</div> -->
- <!-- <div class="punished" v-if="user.isSilenced"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> -->
+<component :is="prefer.s.enablePullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => reload()">
+ <div class="_spacer" :style="{ '--MI_SPACER-w': narrow ? '800px' : '1100px' }">
+ <div ref="rootEl" class="ftskorzw" :class="{ wide: !narrow }" style="container-type: inline-size;">
+ <div class="main _gaps">
+ <!-- TODO -->
+ <!-- <div class="punished" v-if="user.isSuspended"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSuspended }}</div> -->
+ <!-- <div class="punished" v-if="user.isSilenced"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> -->
- <div class="profile _gaps">
- <MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/>
- <MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!"/>
- <MkInfo v-if="user.host == null && user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo>
+ <div class="profile _gaps">
+ <MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/>
+ <MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!"/>
+ <MkInfo v-if="user.host == null && user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo>
- <div :key="user.id" class="main _panel">
- <div class="banner-container" :style="style">
- <div ref="bannerEl" class="banner" :style="style"></div>
- <div class="fade"></div>
+ <div :key="user.id" class="main _panel">
+ <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="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
+ <span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
+ <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
+ <button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
+ <i class="ti ti-edit"/> {{ i18n.ts.addMemo }}
+ </button>
+ </div>
+ </div>
+ <span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
+ <div class="actions">
+ <button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
+ <MkFollowButton v-if="$i?.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+ </div>
+ </div>
+ <MkAvatar class="avatar" :user="user" indicator/>
<div class="title">
- <MkUserName class="name" :user="user" :nowrap="true"/>
+ <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="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
- <button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
- <i class="ti ti-edit"/> {{ i18n.ts.addMemo }}
- </button>
</div>
</div>
- <span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
- <div class="actions">
- <button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
- <MkFollowButton v-if="$i?.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+ <div v-if="user.followedMessage != null" class="followedMessage">
+ <MkFukidashi class="fukidashi" :tail="narrow ? 'none' : 'left'" negativeMargin>
+ <div class="messageHeader">{{ i18n.ts.messageToFollower }}</div>
+ <div><MkSparkle><Mfm :plain="true" :text="user.followedMessage" :author="user" class="_selectable"/></MkSparkle></div>
+ </MkFukidashi>
</div>
- </div>
- <MkAvatar class="avatar" :user="user" indicator/>
- <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="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
- <span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
- <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
+ <div v-if="user.roles.length > 0" class="roles">
+ <span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">
+ <MkA v-adaptive-bg :to="`/roles/${role.id}`">
+ <img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/>
+ {{ role.name }}
+ </MkA>
+ </span>
</div>
- </div>
- <div v-if="user.followedMessage != null" class="followedMessage">
- <MkFukidashi class="fukidashi" :tail="narrow ? 'none' : 'left'" negativeMargin>
- <div class="messageHeader">{{ i18n.ts.messageToFollower }}</div>
- <div><MkSparkle><Mfm :plain="true" :text="user.followedMessage" :author="user" class="_selectable"/></MkSparkle></div>
- </MkFukidashi>
- </div>
- <div v-if="user.roles.length > 0" class="roles">
- <span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">
- <MkA v-adaptive-bg :to="`/roles/${role.id}`">
- <img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/>
- {{ role.name }}
+ <div v-if="iAmModerator" class="moderationNote">
+ <MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" v-model="moderationNote" manualSave>
+ <template #label>{{ i18n.ts.moderationNote }}</template>
+ <template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
+ </MkTextarea>
+ <div v-else>
+ <MkButton small @click="editModerationNote = true">{{ i18n.ts.addModerationNote }}</MkButton>
+ </div>
+ </div>
+ <div v-if="isEditingMemo || memoDraft" class="memo" :class="{'no-memo': !memoDraft}">
+ <div class="heading" v-text="i18n.ts.memo"/>
+ <textarea
+ ref="memoTextareaEl"
+ v-model="memoDraft"
+ rows="1"
+ @focus="isEditingMemo = true"
+ @blur="updateMemo"
+ @input="adjustMemoTextarea"
+ />
+ </div>
+ <div class="description">
+ <MkOmit>
+ <Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user" class="_selectable"/>
+ <p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p>
+ </MkOmit>
+ </div>
+ <div class="fields system">
+ <dl v-if="user.location" class="field">
+ <dt class="name"><i class="ti ti-map-pin ti-fw"></i> {{ i18n.ts.location }}</dt>
+ <dd class="value">{{ user.location }}</dd>
+ </dl>
+ <dl v-if="user.birthday" class="field">
+ <dt class="name"><i class="ti ti-cake ti-fw"></i> {{ i18n.ts.birthday }}</dt>
+ <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.tsx.yearsOld({ age }) }})</dd>
+ </dl>
+ <dl class="field">
+ <dt class="name"><i class="ti ti-calendar ti-fw"></i> {{ i18n.ts.registeredDate }}</dt>
+ <dd class="value">{{ dateString(user.createdAt) }} (<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" :author="user" :plain="true" :colored="false" class="_selectable"/>
+ </dt>
+ <dd class="value">
+ <Mfm :text="field.value" :author="user" :colored="false" class="_selectable"/>
+ <i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ti ti-circle-check" :class="$style.verifiedLink"></i>
+ </dd>
+ </dl>
+ </div>
+ <div class="status">
+ <MkA :to="userPage(user)">
+ <b>{{ number(user.notesCount) }}</b>
+ <span>{{ i18n.ts.notes }}</span>
+ </MkA>
+ <MkA v-if="isFollowingVisibleForMe(user)" :to="userPage(user, 'following')">
+ <b>{{ number(user.followingCount) }}</b>
+ <span>{{ i18n.ts.following }}</span>
+ </MkA>
+ <MkA v-if="isFollowersVisibleForMe(user)" :to="userPage(user, 'followers')">
+ <b>{{ number(user.followersCount) }}</b>
+ <span>{{ i18n.ts.followers }}</span>
</MkA>
- </span>
- </div>
- <div v-if="iAmModerator" class="moderationNote">
- <MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" v-model="moderationNote" manualSave>
- <template #label>{{ i18n.ts.moderationNote }}</template>
- <template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
- </MkTextarea>
- <div v-else>
- <MkButton small @click="editModerationNote = true">{{ i18n.ts.addModerationNote }}</MkButton>
</div>
</div>
- <div v-if="isEditingMemo || memoDraft" class="memo" :class="{'no-memo': !memoDraft}">
- <div class="heading" v-text="i18n.ts.memo"/>
- <textarea
- ref="memoTextareaEl"
- v-model="memoDraft"
- rows="1"
- @focus="isEditingMemo = true"
- @blur="updateMemo"
- @input="adjustMemoTextarea"
- />
- </div>
- <div class="description">
- <MkOmit>
- <Mfm v-if="user.description" :text="user.description" :isNote="false" :author="user" class="_selectable"/>
- <p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p>
- </MkOmit>
- </div>
- <div class="fields system">
- <dl v-if="user.location" class="field">
- <dt class="name"><i class="ti ti-map-pin ti-fw"></i> {{ i18n.ts.location }}</dt>
- <dd class="value">{{ user.location }}</dd>
- </dl>
- <dl v-if="user.birthday" class="field">
- <dt class="name"><i class="ti ti-cake ti-fw"></i> {{ i18n.ts.birthday }}</dt>
- <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.tsx.yearsOld({ age }) }})</dd>
- </dl>
- <dl class="field">
- <dt class="name"><i class="ti ti-calendar ti-fw"></i> {{ i18n.ts.registeredDate }}</dt>
- <dd class="value">{{ dateString(user.createdAt) }} (<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" :author="user" :plain="true" :colored="false" class="_selectable"/>
- </dt>
- <dd class="value">
- <Mfm :text="field.value" :author="user" :colored="false" class="_selectable"/>
- <i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ti ti-circle-check" :class="$style.verifiedLink"></i>
- </dd>
- </dl>
+ </div>
+
+ <div class="contents _gaps">
+ <div v-if="user.pinnedNotes.length > 0" class="_gaps">
+ <MkNote v-for="note in user.pinnedNotes" :key="note.id" class="note _panel" :note="note" :pinned="true"/>
</div>
- <div class="status">
- <MkA :to="userPage(user)">
- <b>{{ number(user.notesCount) }}</b>
- <span>{{ i18n.ts.notes }}</span>
- </MkA>
- <MkA v-if="isFollowingVisibleForMe(user)" :to="userPage(user, 'following')">
- <b>{{ number(user.followingCount) }}</b>
- <span>{{ i18n.ts.following }}</span>
- </MkA>
- <MkA v-if="isFollowersVisibleForMe(user)" :to="userPage(user, 'followers')">
- <b>{{ number(user.followersCount) }}</b>
- <span>{{ i18n.ts.followers }}</span>
- </MkA>
+ <MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo>
+ <template v-if="narrow">
+ <MkLazy>
+ <XFiles :key="user.id" :user="user" @unfold="emit('unfoldFiles')"/>
+ </MkLazy>
+ <MkLazy>
+ <XActivity :key="user.id" :user="user"/>
+ </MkLazy>
+ </template>
+ <div v-if="!disableNotes">
+ <MkLazy>
+ <XTimeline :user="user"/>
+ </MkLazy>
</div>
</div>
</div>
-
- <div class="contents _gaps">
- <div v-if="user.pinnedNotes.length > 0" class="_gaps">
- <MkNote v-for="note in user.pinnedNotes" :key="note.id" class="note _panel" :note="note" :pinned="true"/>
- </div>
- <MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo>
- <template v-if="narrow">
- <MkLazy>
- <XFiles :key="user.id" :user="user" @unfold="emit('unfoldFiles')"/>
- </MkLazy>
- <MkLazy>
- <XActivity :key="user.id" :user="user"/>
- </MkLazy>
- </template>
- <div v-if="!disableNotes">
- <MkLazy>
- <XTimeline :user="user"/>
- </MkLazy>
- </div>
+ <div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
+ <XFiles :key="user.id" :user="user" @unfold="emit('unfoldFiles')"/>
+ <XActivity :key="user.id" :user="user"/>
</div>
</div>
- <div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
- <XFiles :key="user.id" :user="user" @unfold="emit('unfoldFiles')"/>
- <XActivity :key="user.id" :user="user"/>
- </div>
</div>
-</div>
+</component>
</template>
<script lang="ts" setup>
@@ -185,6 +187,7 @@ import { useRouter } from '@/router.js';
import { getStaticImageUrl } from '@/utility/media-proxy.js';
import MkSparkle from '@/components/MkSparkle.vue';
import { prefer } from '@/preferences.js';
+import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
function calcAge(birthdate: string): number {
const date = new Date(birthdate);
@@ -207,7 +210,7 @@ const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed;
- /** Test only; MkNotes currently causes problems in vitest */
+ /** Test only; MkNotesTimeline currently causes problems in vitest */
disableNotes: boolean;
}>(), {
disableNotes: false,
@@ -299,6 +302,10 @@ watch([props.user], () => {
memoDraft.value = props.user.memo;
});
+async function reload() {
+ // TODO
+}
+
onMounted(() => {
window.requestAnimationFrame(parallaxLoop);
narrow.value = rootEl.value!.clientWidth < 1000;
diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue
index 49d015a530..d8eca07a42 100644
--- a/packages/frontend/src/pages/user/index.timeline.vue
+++ b/packages/frontend/src/pages/user/index.timeline.vue
@@ -8,19 +8,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header>
<MkTab v-model="tab" :class="$style.tab">
<option value="featured">{{ i18n.ts.featured }}</option>
- <option :value="null">{{ i18n.ts.notes }}</option>
+ <option value="notes">{{ i18n.ts.notes }}</option>
<option value="all">{{ i18n.ts.all }}</option>
<option value="files">{{ i18n.ts.withFiles }}</option>
</MkTab>
</template>
- <MkNotes :noGap="true" :pagination="pagination" :class="$style.tl"/>
+ <MkNotesTimeline :key="tab" :noGap="true" :pagination="pagination" :pullToRefresh="false" :class="$style.tl"/>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
-import MkNotes from '@/components/MkNotes.vue';
+import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import MkTab from '@/components/MkTab.vue';
import { i18n } from '@/i18n.js';
@@ -28,7 +28,7 @@ const props = defineProps<{
user: Misskey.entities.UserDetailed;
}>();
-const tab = ref<string | null>('all');
+const tab = ref<string>('all');
const pagination = computed(() => tab.value === 'featured' ? {
endpoint: 'users/featured-notes' as const,
diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue
index d6e477d0ae..d4f36271ad 100644
--- a/packages/frontend/src/pages/user/index.vue
+++ b/packages/frontend/src/pages/user/index.vue
@@ -7,9 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<PageWithHeader v-model:tab="tab" :tabs="headerTabs" :actions="headerActions" :swipable="true">
<div v-if="user">
<XHome v-if="tab === 'home'" :user="user" @unfoldFiles="() => { tab = 'files'; }"/>
- <div v-else-if="tab === 'notes'" class="_spacer" style="--MI_SPACER-w: 800px;">
- <XTimeline :user="user"/>
- </div>
+ <XNotes v-else-if="tab === 'notes'" :user="user"/>
<XFiles v-else-if="tab === 'files'" :user="user"/>
<XActivity v-else-if="tab === 'activity'" :user="user"/>
<XAchievements v-else-if="tab === 'achievements'" :user="user"/>
@@ -37,7 +35,7 @@ import { $i } from '@/i.js';
import { serverContext, assertServerContext } from '@/server-context.js';
const XHome = defineAsyncComponent(() => import('./home.vue'));
-const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
+const XNotes = defineAsyncComponent(() => import('./notes.vue'));
const XFiles = defineAsyncComponent(() => import('./files.vue'));
const XActivity = defineAsyncComponent(() => import('./activity.vue'));
const XAchievements = defineAsyncComponent(() => import('./achievements.vue'));
diff --git a/packages/frontend/src/pages/user/notes.vue b/packages/frontend/src/pages/user/notes.vue
new file mode 100644
index 0000000000..c97177b6a5
--- /dev/null
+++ b/packages/frontend/src/pages/user/notes.vue
@@ -0,0 +1,67 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div class="_spacer" style="--MI_SPACER-w: 800px;">
+ <div :class="$style.root">
+ <MkStickyContainer>
+ <template #header>
+ <MkTab v-model="tab" :class="$style.tab">
+ <option value="featured">{{ i18n.ts.featured }}</option>
+ <option value="notes">{{ i18n.ts.notes }}</option>
+ <option value="all">{{ i18n.ts.all }}</option>
+ <option value="files">{{ i18n.ts.withFiles }}</option>
+ </MkTab>
+ </template>
+ <MkNotesTimeline :key="tab" :noGap="true" :pagination="pagination" :class="$style.tl"/>
+ </MkStickyContainer>
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue';
+import * as Misskey from 'misskey-js';
+import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
+import MkTab from '@/components/MkTab.vue';
+import { i18n } from '@/i18n.js';
+
+const props = defineProps<{
+ user: Misskey.entities.UserDetailed;
+}>();
+
+const tab = ref<string>('all');
+
+const pagination = computed(() => tab.value === 'featured' ? {
+ endpoint: 'users/featured-notes' as const,
+ limit: 10,
+ params: {
+ userId: props.user.id,
+ },
+} : {
+ endpoint: 'users/notes' as const,
+ limit: 10,
+ params: {
+ userId: props.user.id,
+ withRenotes: tab.value === 'all',
+ withReplies: tab.value === 'all',
+ withChannelNotes: tab.value === 'all',
+ withFiles: tab.value === 'files',
+ },
+});
+</script>
+
+<style lang="scss" module>
+.tab {
+ padding: calc(var(--MI-margin) / 2) 0;
+ background: var(--MI_THEME-bg);
+}
+
+.tl {
+ background: var(--MI_THEME-bg);
+ border-radius: var(--MI-radius);
+ overflow: clip;
+}
+</style>