diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-05-09 17:40:08 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-05-09 17:40:08 +0900 |
| commit | 8c2ab25e5f2040fcbc81bc2a02a279fed40e1c11 (patch) | |
| tree | ae0d3573bd5a3175bc6174d33129dc64205a1436 /packages/frontend/src/pages | |
| parent | refactor (diff) | |
| download | misskey-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')
34 files changed, 396 insertions, 395 deletions
diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index 97743995bf..dc3c906217 100644 --- a/packages/frontend/src/pages/about.federation.vue +++ b/packages/frontend/src/pages/about.federation.vue @@ -55,7 +55,7 @@ import { computed, ref } from 'vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkPagination from '@/components/MkPagination.vue'; -import type { Paging } from '@/components/MkPagination.vue'; +import type { PagingCtx } from '@/use/use-pagination.js'; import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue'; import FormSplit from '@/components/form/split.vue'; import { i18n } from '@/i18n.js'; @@ -81,7 +81,7 @@ const pagination = { state.value === 'notResponding' ? { notResponding: true } : {}), })), -} as Paging; +} as PagingCtx; function getStatus(instance) { if (instance.isSuspended) return 'Suspended'; diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue index 3dc5c2ef7e..14e8e600b0 100644 --- a/packages/frontend/src/pages/admin/abuses.vue +++ b/packages/frontend/src/pages/admin/abuses.vue @@ -87,7 +87,7 @@ const pagination = { }; function resolved(reportId) { - reports.value?.removeItem(reportId); + reports.value?.paginator.removeItem(reportId); } function closeTutorial() { diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue index 072175f3af..754bf74c2a 100644 --- a/packages/frontend/src/pages/admin/invites.vue +++ b/packages/frontend/src/pages/admin/invites.vue @@ -55,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref, useTemplateRef } from 'vue'; -import type { Paging } from '@/components/MkPagination.vue'; +import type { PagingCtx } from '@/use/use-pagination.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -73,7 +73,7 @@ const pagingComponent = useTemplateRef('pagingComponent'); const type = ref('all'); const sort = ref('+createdAt'); -const pagination: Paging = { +const pagination: PagingCtx = { endpoint: 'admin/invite/list' as const, limit: 10, params: computed(() => ({ @@ -100,12 +100,12 @@ async function createWithOptions() { text: tickets.map(x => x.code).join('\n'), }); - tickets.forEach(ticket => pagingComponent.value?.prepend(ticket)); + tickets.forEach(ticket => pagingComponent.value?.paginator.prepend(ticket)); } function deleted(id: string) { if (pagingComponent.value) { - pagingComponent.value.items.delete(id); + pagingComponent.value.paginator.removeItem(id); } } diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue index 6eb3c04dde..56cf8876f0 100644 --- a/packages/frontend/src/pages/admin/users.vue +++ b/packages/frontend/src/pages/admin/users.vue @@ -121,7 +121,7 @@ async function addUser() { username: username, password: password, }).then(res => { - paginationComponent.value?.reload(); + paginationComponent.value?.paginator.reload(); }); } diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index bb4730c606..2c671c6b34 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, useTemplateRef } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -71,7 +71,7 @@ const paginationPast = { }, }; -const paginationEl = ref<InstanceType<typeof MkPagination>>(); +const paginationEl = useTemplateRef('paginationEl'); const tab = ref('current'); @@ -86,10 +86,10 @@ async function read(target) { } if (!paginationEl.value) return; - paginationEl.value.updateItem(target.id, a => { - a.isRead = true; - return a; - }); + paginationEl.value.paginator.updateItem(target.id, a => ({ + ...a, + isRead: true, + })); misskeyApi('i/read-announcement', { announcementId: target.id }); updateCurrentAccountPartial({ unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id), diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index 89ab1bf99a..7d2393dba5 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -6,17 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <PageWithHeader :actions="headerActions" :tabs="headerTabs"> <div class="_spacer" style="--MI_SPACER-w: 800px;"> - <div ref="rootEl"> - <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> - <div :class="$style.tl"> - <MkTimeline - ref="tlEl" :key="antennaId" - src="antenna" - :antenna="antennaId" - :sound="true" - @queue="queueUpdated" - /> - </div> + <div :class="$style.tl"> + <MkStreamingNotesTimeline + ref="tlEl" :key="antennaId" + src="antenna" + :antenna="antennaId" + :sound="true" + /> </div> </div> </PageWithHeader> @@ -25,8 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, ref, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; -import { scrollInContainer } from '@@/js/scroll.js'; -import MkTimeline from '@/components/MkTimeline.vue'; +import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { definePage } from '@/page.js'; @@ -40,18 +35,8 @@ const props = defineProps<{ }>(); const antenna = ref<Misskey.entities.Antenna | null>(null); -const queue = ref(0); -const rootEl = useTemplateRef('rootEl'); const tlEl = useTemplateRef('tlEl'); -function queueUpdated(q) { - queue.value = q; -} - -function top() { - scrollInContainer(rootEl.value, { top: 0 }); -} - async function timetravel() { const { canceled, result: date } = await os.inputDate({ title: i18n.ts.date, @@ -94,25 +79,6 @@ definePage(() => ({ </script> <style lang="scss" module> -.new { - position: sticky; - top: calc(var(--MI-stickyTop, 0px) + 16px); - z-index: 1000; - width: 100%; - margin: calc(-0.675em - 8px) 0; - - &:first-child { - margin-top: calc(-0.675em - 8px - var(--MI-margin)); - } -} - -.newButton { - display: block; - margin: var(--MI-margin) auto 0 auto; - padding: 8px 16px; - border-radius: 32px; -} - .tl { background: var(--MI_THEME-bg); border-radius: var(--MI-radius); diff --git a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue index cb0e1666f8..ddc4e89ef1 100644 --- a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue +++ b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue @@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, ref } from 'vue'; +import { computed, watch, ref, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkWindow from '@/components/MkWindow.vue'; import MkButton from '@/components/MkButton.vue'; @@ -86,7 +86,7 @@ const emit = defineEmits<{ (ev: 'closed'): void }>(); -const windowEl = ref<InstanceType<typeof MkWindow> | null>(null); +const windowEl = useTemplateRef('windowEl'); const url = ref<string>(props.avatarDecoration ? props.avatarDecoration.url : ''); const name = ref<string>(props.avatarDecoration ? props.avatarDecoration.name : ''); const description = ref<string>(props.avatarDecoration ? props.avatarDecoration.description : ''); diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 1c411d2a2e..6eb390f743 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -37,10 +37,10 @@ SPDX-License-Identifier: AGPL-3.0-only <!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる --> <MkPostForm v-if="$i && prefer.r.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/> - <MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/> + <MkStreamingNotesTimeline :key="channelId" src="channel" :channel="channelId"/> </div> <div v-else-if="tab === 'featured'"> - <MkNotes :pagination="featuredPagination"/> + <MkNotesTimeline :pagination="featuredPagination"/> </div> <div v-else-if="tab === 'search'"> <div v-if="notesSearchAvailable" class="_gaps"> @@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> <MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton> </div> - <MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/> + <MkNotesTimeline v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/> </div> <div v-else> <MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo> @@ -73,9 +73,10 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { url } from '@@/js/config.js'; +import { useInterval } from '@@/js/use-interval.js'; import type { PageHeaderItem } from '@/types/page-header.js'; import MkPostForm from '@/components/MkPostForm.vue'; -import MkTimeline from '@/components/MkTimeline.vue'; +import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue'; import XChannelFollowButton from '@/components/MkChannelFollowButton.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -83,7 +84,7 @@ import { $i, iAmModerator } from '@/i.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; import { deviceKind } from '@/utility/device-kind.js'; -import MkNotes from '@/components/MkNotes.vue'; +import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import { favoritedChannelsCache } from '@/cache.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -118,6 +119,14 @@ const featuredPagination = computed(() => ({ }, })); +useInterval(() => { + if (channel.value == null) return; + miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.value.id}`, Date.now()); +}, 3000, { + immediate: true, + afterMounted: true, +}); + watch(() => props.channelId, async () => { channel.value = await misskeyApi('channels/show', { channelId: props.channelId, diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 68c5d6c270..dc043e2ce1 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> - <MkNotes :pagination="pagination" :detail="true"/> + <MkNotesTimeline :pagination="pagination" :detail="true"/> </div> </div> </PageWithHeader> @@ -34,7 +34,7 @@ import { computed, watch, provide, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { url } from '@@/js/config.js'; import type { MenuItem } from '@/types/menu.js'; -import MkNotes from '@/components/MkNotes.vue'; +import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import { $i } from '@/i.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 16a95c6753..46e494e6f6 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -115,7 +115,7 @@ const selectAll = () => { if (selectedEmojis.value.length > 0) { selectedEmojis.value = []; } else { - selectedEmojis.value = Array.from(emojisPaginationComponent.value?.items.values(), item => item.id); + selectedEmojis.value = emojisPaginationComponent.value?.paginator.items.value.map(item => item.id); } }; @@ -132,7 +132,7 @@ const add = async (ev: MouseEvent) => { }, { done: result => { if (result.created) { - emojisPaginationComponent.value?.prepend(result.created); + emojisPaginationComponent.value?.paginator.prepend(result.created); } }, closed: () => dispose(), @@ -145,12 +145,12 @@ const edit = (emoji) => { }, { done: result => { if (result.updated) { - emojisPaginationComponent.value?.updateItem(result.updated.id, (oldEmoji) => ({ + emojisPaginationComponent.value?.paginator.updateItem(result.updated.id, (oldEmoji) => ({ ...oldEmoji, ...result.updated, })); } else if (result.deleted) { - emojisPaginationComponent.value?.removeItem(emoji.id); + emojisPaginationComponent.value?.paginator.removeItem(emoji.id); } }, closed: () => dispose(), @@ -242,7 +242,7 @@ const setCategoryBulk = async () => { ids: selectedEmojis.value, category: result, }); - emojisPaginationComponent.value?.reload(); + emojisPaginationComponent.value?.paginator.reload(); }; const setLicenseBulk = async () => { @@ -254,7 +254,7 @@ const setLicenseBulk = async () => { ids: selectedEmojis.value, license: result, }); - emojisPaginationComponent.value?.reload(); + emojisPaginationComponent.value?.paginator.reload(); }; const addTagBulk = async () => { @@ -266,7 +266,7 @@ const addTagBulk = async () => { ids: selectedEmojis.value, aliases: result.split(' '), }); - emojisPaginationComponent.value?.reload(); + emojisPaginationComponent.value?.paginator.reload(); }; const removeTagBulk = async () => { @@ -278,7 +278,7 @@ const removeTagBulk = async () => { ids: selectedEmojis.value, aliases: result.split(' '), }); - emojisPaginationComponent.value?.reload(); + emojisPaginationComponent.value?.paginator.reload(); }; const setTagBulk = async () => { @@ -290,7 +290,7 @@ const setTagBulk = async () => { ids: selectedEmojis.value, aliases: result.split(' '), }); - emojisPaginationComponent.value?.reload(); + emojisPaginationComponent.value?.paginator.reload(); }; const delBulk = async () => { @@ -302,7 +302,7 @@ const delBulk = async () => { await os.apiWithDialog('admin/emoji/delete-bulk', { ids: selectedEmojis.value, }); - emojisPaginationComponent.value?.reload(); + emojisPaginationComponent.value?.paginator.reload(); }; const headerActions = computed(() => [{ diff --git a/packages/frontend/src/pages/drive.file.notes.vue b/packages/frontend/src/pages/drive.file.notes.vue index d7519896cc..95ac5082bb 100644 --- a/packages/frontend/src/pages/drive.file.notes.vue +++ b/packages/frontend/src/pages/drive.file.notes.vue @@ -6,16 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps"> <MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo> - <MkNotes ref="tlComponent" :pagination="pagination"/> + <MkNotesTimeline ref="tlComponent" :pagination="pagination"/> </div> </template> <script lang="ts" setup> import { ref, computed } from 'vue'; import { i18n } from '@/i18n.js'; -import type { Paging } from '@/components/MkPagination.vue'; +import type { PagingCtx } from '@/use/use-pagination.js'; import MkInfo from '@/components/MkInfo.vue'; -import MkNotes from '@/components/MkNotes.vue'; +import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; const props = defineProps<{ fileId: string; @@ -23,7 +23,7 @@ const props = defineProps<{ const realFileId = computed(() => props.fileId); -const pagination = ref<Paging>({ +const pagination = ref<PagingCtx>({ endpoint: 'drive/files/attached-notes', limit: 10, params: { diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index 9eb24aa70e..0479ed6f6c 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, ref } from 'vue'; +import { computed, watch, ref, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkWindow from '@/components/MkWindow.vue'; import MkButton from '@/components/MkButton.vue'; @@ -103,7 +103,7 @@ const emit = defineEmits<{ (ev: 'closed'): void }>(); -const windowEl = ref<InstanceType<typeof MkWindow> | null>(null); +const windowEl = useTemplateRef('windowEl'); const name = ref<string>(props.emoji ? props.emoji.name : ''); const category = ref<string>(props.emoji?.category ? props.emoji.category : ''); const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : ''); diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue index a47e3efbc8..b8eb7eb8d5 100644 --- a/packages/frontend/src/pages/explore.featured.vue +++ b/packages/frontend/src/pages/explore.featured.vue @@ -9,14 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="notes">{{ i18n.ts.notes }}</option> <option value="polls">{{ i18n.ts.poll }}</option> </MkTab> - <MkNotes v-if="tab === 'notes'" :pagination="paginationForNotes"/> - <MkNotes v-else-if="tab === 'polls'" :pagination="paginationForPolls"/> + <MkNotesTimeline v-if="tab === 'notes'" :pagination="paginationForNotes"/> + <MkNotesTimeline v-else-if="tab === 'polls'" :pagination="paginationForPolls"/> </div> </template> <script lang="ts" setup> import { ref } from 'vue'; -import MkNotes from '@/components/MkNotes.vue'; +import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import MkTab from '@/components/MkTab.vue'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue index 9b4e3faaef..e98ae99a10 100644 --- a/packages/frontend/src/pages/follow-requests.vue +++ b/packages/frontend/src/pages/follow-requests.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; import { useTemplateRef, computed, ref } from 'vue'; -import type { Paging } from '@/components/MkPagination.vue'; +import type { PagingCtx } from '@/use/use-pagination.js'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import { userPage, acct } from '@/filters/user.js'; @@ -47,7 +47,7 @@ import { $i } from '@/i.js'; const paginationComponent = useTemplateRef('paginationComponent'); -const pagination = computed<Paging>(() => tab.value === 'list' ? { +const pagination = computed<PagingCtx>(() => tab.value === 'list' ? { endpoint: 'following/requests/list', limit: 10, } : { @@ -57,19 +57,19 @@ const pagination = computed<Paging>(() => tab.value === 'list' ? { function accept(user: Misskey.entities.UserLite) { os.apiWithDialog('following/requests/accept', { userId: user.id }).then(() => { - paginationComponent.value?.reload(); + paginationComponent.value?.paginator.reload(); }); } function reject(user: Misskey.entities.UserLite) { os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => { - paginationComponent.value?.reload(); + paginationComponent.value?.paginator.reload(); }); } function cancel(user: Misskey.entities.UserLite) { os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => { - paginationComponent.value?.reload(); + paginationComponent.value?.paginator.reload(); }); } diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 96a43f67e8..c25a5b36d8 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, computed, watch } from 'vue'; import * as Misskey from 'misskey-js'; import type { ChartSrc } from '@/components/MkChart.vue'; -import type { Paging } from '@/components/MkPagination.vue'; +import type { PagingCtx } from '@/use/use-pagination.js'; import MkChart from '@/components/MkChart.vue'; import MkObjectView from '@/components/MkObjectView.vue'; import FormLink from '@/components/form/link.vue'; @@ -180,7 +180,7 @@ const usersPagination = { hostname: props.host, }, offsetMode: true, -} satisfies Paging; +} satisfies PagingCtx; if (iAmModerator) { watch(moderationNote, async () => { diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue index 406c08bcf2..4cc9021424 100644 --- a/packages/frontend/src/pages/invite.vue +++ b/packages/frontend/src/pages/invite.vue @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; -import type { Paging } from '@/components/MkPagination.vue'; +import type { PagingCtx } from '@/use/use-pagination.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -45,7 +45,7 @@ const currentInviteLimit = ref<null | number>(null); const inviteLimit = (($i != null && $i.policies.inviteLimit) || (($i == null && instance.policies.inviteLimit))) as number; const inviteLimitCycle = (($i != null && $i.policies.inviteLimitCycle) || ($i == null && instance.policies.inviteLimitCycle)) as number; -const pagination: Paging = { +const pagination: PagingCtx = { endpoint: 'invite/list' as const, limit: 10, }; @@ -68,13 +68,13 @@ async function create() { text: ticket.code, }); - pagingComponent.value?.prepend(ticket); + pagingComponent.value?.paginator.prepend(ticket); update(); } function deleted(id: string) { if (pagingComponent.value) { - pagingComponent.value.items.delete(id); + pagingComponent.value.paginator.removeItem(id); } update(); } diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index 9e427ecf35..4dafd87b80 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -73,15 +73,15 @@ async function create() { clipsCache.delete(); - pagingComponent.value?.reload(); + pagingComponent.value?.paginator.reload(); } function onClipCreated() { - pagingComponent.value?.reload(); + pagingComponent.value?.paginator.reload(); } function onClipDeleted() { - pagingComponent.value?.reload(); + pagingComponent.value?.paginator.reload(); } const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index 0b76fb4725..06abe3d7fd 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref, watch } from 'vue'; +import { computed, ref, useTemplateRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; @@ -80,7 +80,7 @@ const props = defineProps<{ listId: string; }>(); -const paginationEl = ref<InstanceType<typeof MkPagination>>(); +const paginationEl = useTemplateRef('paginationEl'); const list = ref<Misskey.entities.UserList | null>(null); const isPublic = ref(false); const name = ref(''); @@ -109,7 +109,7 @@ function addUser() { listId: list.value.id, userId: user.id, }).then(() => { - paginationEl.value?.reload(); + paginationEl.value?.paginator.reload(); }); }); } @@ -125,7 +125,7 @@ async function removeUser(item, ev) { listId: list.value.id, userId: item.userId, }).then(() => { - paginationEl.value?.removeItem(item.id); + paginationEl.value?.paginator.removeItem(item.id); }); }, }], ev.currentTarget ?? ev.target); @@ -147,7 +147,7 @@ async function showMembershipMenu(item, ev) { userId: item.userId, withReplies, }).then(() => { - paginationEl.value!.updateItem(item.id, (old) => ({ + paginationEl.value!.paginator.updateItem(item.id, (old) => ({ ...old, withReplies, })); diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index 0f1dbc4432..68a009f648 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -6,42 +6,40 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <PageWithHeader :actions="headerActions" :tabs="headerTabs"> <div class="_spacer" style="--MI_SPACER-w: 800px;"> - <div> - <Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in"> - <div v-if="note"> - <div v-if="showNext" class="_margin"> - <MkNotes class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="true"/> - </div> + <Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in"> + <div v-if="note"> + <div v-if="showNext" class="_margin"> + <MkNotesTimeline :pullToRefresh="false" class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="true"/> + </div> - <div class="_margin"> - <div v-if="!showNext" class="_buttons" :class="$style.loadNext"> - <MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showNext = 'channel'"><i class="ti ti-chevron-up"></i> <i class="ti ti-device-tv"></i></MkButton> - <MkButton rounded :class="$style.loadButton" @click="showNext = 'user'"><i class="ti ti-chevron-up"></i> <i class="ti ti-user"></i></MkButton> - </div> - <div class="_margin _gaps_s"> - <MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/> - <MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note"/> - </div> - <div v-if="clips && clips.length > 0" class="_margin"> - <div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div> - <div class="_gaps"> - <MkClipPreview v-for="item in clips" :key="item.id" :clip="item"/> - </div> - </div> - <div v-if="!showPrev" class="_buttons" :class="$style.loadPrev"> - <MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showPrev = 'channel'"><i class="ti ti-chevron-down"></i> <i class="ti ti-device-tv"></i></MkButton> - <MkButton rounded :class="$style.loadButton" @click="showPrev = 'user'"><i class="ti ti-chevron-down"></i> <i class="ti ti-user"></i></MkButton> + <div class="_margin"> + <div v-if="!showNext" class="_buttons" :class="$style.loadNext"> + <MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showNext = 'channel'"><i class="ti ti-chevron-up"></i> <i class="ti ti-device-tv"></i></MkButton> + <MkButton rounded :class="$style.loadButton" @click="showNext = 'user'"><i class="ti ti-chevron-up"></i> <i class="ti ti-user"></i></MkButton> + </div> + <div class="_margin _gaps_s"> + <MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/> + <MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note"/> + </div> + <div v-if="clips && clips.length > 0" class="_margin"> + <div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div> + <div class="_gaps"> + <MkClipPreview v-for="item in clips" :key="item.id" :clip="item"/> </div> </div> - - <div v-if="showPrev" class="_margin"> - <MkNotes class="" :pagination="showPrev === 'channel' ? prevChannelPagination : prevUserPagination" :noGap="true"/> + <div v-if="!showPrev" class="_buttons" :class="$style.loadPrev"> + <MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showPrev = 'channel'"><i class="ti ti-chevron-down"></i> <i class="ti ti-device-tv"></i></MkButton> + <MkButton rounded :class="$style.loadButton" @click="showPrev = 'user'"><i class="ti ti-chevron-down"></i> <i class="ti ti-user"></i></MkButton> </div> </div> - <MkError v-else-if="error" @retry="fetchNote()"/> - <MkLoading v-else/> - </Transition> - </div> + + <div v-if="showPrev" class="_margin"> + <MkNotesTimeline :pullToRefresh="false" class="" :pagination="showPrev === 'channel' ? prevChannelPagination : prevUserPagination" :noGap="true"/> + </div> + </div> + <MkError v-else-if="error" @retry="fetchNote()"/> + <MkLoading v-else/> + </Transition> </div> </PageWithHeader> </template> @@ -50,9 +48,9 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import { host } from '@@/js/config.js'; -import type { Paging } from '@/components/MkPagination.vue'; +import type { PagingCtx } from '@/use/use-pagination.js'; import MkNoteDetailed from '@/components/MkNoteDetailed.vue'; -import MkNotes from '@/components/MkNotes.vue'; +import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; import MkButton from '@/components/MkButton.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -80,26 +78,27 @@ const showPrev = ref<'user' | 'channel' | false>(false); const showNext = ref<'user' | 'channel' | false>(false); const error = ref(); -const prevUserPagination: Paging = { +const prevUserPagination: PagingCtx = { endpoint: 'users/notes', limit: 10, + baseId: props.noteId, + direction: 'older', params: computed(() => note.value ? ({ userId: note.value.userId, - untilId: note.value.id, }) : undefined), }; -const nextUserPagination: Paging = { - reversed: true, +const nextUserPagination: PagingCtx = { endpoint: 'users/notes', limit: 10, + baseId: props.noteId, + direction: 'newer', params: computed(() => note.value ? ({ userId: note.value.userId, - sinceId: note.value.id, }) : undefined), }; -const prevChannelPagination: Paging = { +const prevChannelPagination: PagingCtx = { endpoint: 'channels/timeline', limit: 10, params: computed(() => note.value ? ({ @@ -108,7 +107,7 @@ const prevChannelPagination: Paging = { }) : undefined), }; -const nextChannelPagination: Paging = { +const nextChannelPagination: PagingCtx = { reversed: true, endpoint: 'channels/timeline', limit: 10, diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index 5cb71945dd..db911c1202 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -7,13 +7,13 @@ SPDX-License-Identifier: AGPL-3.0-only <PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true"> <div class="_spacer" style="--MI_SPACER-w: 800px;"> <div v-if="tab === 'all'"> - <XNotifications :class="$style.notifications" :excludeTypes="excludeTypes"/> + <MkStreamingNotificationsTimeline :class="$style.notifications" :excludeTypes="excludeTypes"/> </div> <div v-else-if="tab === 'mentions'"> - <MkNotes :pagination="mentionsPagination"/> + <MkNotesTimeline :pagination="mentionsPagination"/> </div> <div v-else-if="tab === 'directNotes'"> - <MkNotes :pagination="directNotesPagination"/> + <MkNotesTimeline :pagination="directNotesPagination"/> </div> </div> </PageWithHeader> @@ -22,8 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref } from 'vue'; import { notificationTypes } from '@@/js/const.js'; -import XNotifications from '@/components/MkNotifications.vue'; -import MkNotes from '@/components/MkNotes.vue'; +import MkStreamingNotificationsTimeline from '@/components/MkStreamingNotificationsTimeline.vue'; +import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue index 9d01edb255..42639cde9e 100644 --- a/packages/frontend/src/pages/role.vue +++ b/packages/frontend/src/pages/role.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div v-else-if="tab === 'timeline'" class="_spacer" style="--MI_SPACER-w: 700px;"> - <MkTimeline v-if="visible" ref="timeline" src="role" :role="props.roleId"/> + <MkStreamingNotesTimeline v-if="visible" ref="timeline" src="role" :role="props.roleId"/> <MkResult v-else-if="!visible" type="empty" :text="i18n.ts.nothing"/> </div> </PageWithHeader> @@ -29,7 +29,7 @@ import { misskeyApi } from '@/utility/misskey-api.js'; import MkUserList from '@/components/MkUserList.vue'; import { definePage } from '@/page.js'; import { i18n } from '@/i18n.js'; -import MkTimeline from '@/components/MkTimeline.vue'; +import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue'; const props = withDefaults(defineProps<{ roleId: string; diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue index 17cf272a36..35609912eb 100644 --- a/packages/frontend/src/pages/search.note.vue +++ b/packages/frontend/src/pages/search.note.vue @@ -105,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFoldableSection v-if="notePagination"> <template #header>{{ i18n.ts.searchResult }}</template> - <MkNotes :key="`searchNotes:${key}`" :pagination="notePagination"/> + <MkNotesTimeline :key="`searchNotes:${key}`" :pagination="notePagination"/> </MkFoldableSection> </div> </template> @@ -113,7 +113,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref, shallowRef, toRef } from 'vue'; import type * as Misskey from 'misskey-js'; -import type { Paging } from '@/components/MkPagination.vue'; +import type { PagingCtx } from '@/use/use-pagination.js'; import { $i } from '@/i.js'; import { host as localHost } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; @@ -125,7 +125,7 @@ import { useRouter } from '@/router.js'; import MkButton from '@/components/MkButton.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkInput from '@/components/MkInput.vue'; -import MkNotes from '@/components/MkNotes.vue'; +import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; @@ -144,7 +144,7 @@ const props = withDefaults(defineProps<{ const router = useRouter(); const key = ref(0); -const notePagination = ref<Paging<'notes/search'>>(); +const notePagination = ref<PagingCtx<'notes/search'>>(); const searchQuery = ref(toRef(props, 'query').value); const hostInput = ref(toRef(props, 'host').value); diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index 101de6a64f..fb4166fe36 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, toRef } from 'vue'; import type { Endpoints } from 'misskey-js'; -import type { Paging } from '@/components/MkPagination.vue'; +import type { PagingCtx } from '@/use/use-pagination.js'; import MkUserList from '@/components/MkUserList.vue'; import MkInput from '@/components/MkInput.vue'; import MkRadios from '@/components/MkRadios.vue'; @@ -50,7 +50,7 @@ const props = withDefaults(defineProps<{ const router = useRouter(); const key = ref(0); -const userPagination = ref<Paging<'users/search'>>(); +const userPagination = ref<PagingCtx<'users/search'>>(); const searchQuery = ref(toRef(props, 'query').value); const searchOrigin = ref(toRef(props, 'origin').value); diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue index 33c17e5d7f..ec45eb3487 100644 --- a/packages/frontend/src/pages/settings/apps.vue +++ b/packages/frontend/src/pages/settings/apps.vue @@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; import FormPagination from '@/components/MkPagination.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -59,7 +59,7 @@ import MkKeyValue from '@/components/MkKeyValue.vue'; import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; -const list = ref<InstanceType<typeof FormPagination>>(); +const list = useTemplateRef('list'); const pagination = { endpoint: 'i/apps' as const, @@ -72,7 +72,7 @@ const pagination = { function revoke(token) { misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => { - list.value?.reload(); + list.value?.paginator.reload(); }); } diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue index 4d718d21b4..c6732e7787 100644 --- a/packages/frontend/src/pages/settings/preferences.vue +++ b/packages/frontend/src/pages/settings/preferences.vue @@ -41,6 +41,26 @@ SPDX-License-Identifier: AGPL-3.0-only </MkRadios> </SearchMarker> + <SearchMarker :keywords="['realtimemode']"> + <MkSwitch v-model="realtimeMode"> + <template #label><i class="ti ti-bolt"></i> <SearchLabel>{{ i18n.ts.realtimeMode }}</SearchLabel></template> + <template #caption><SearchKeyword>{{ i18n.ts._settings.realtimeMode_description }}</SearchKeyword></template> + </MkSwitch> + </SearchMarker> + + <MkDisableSection :disabled="realtimeMode"> + <SearchMarker :keywords="['polling', 'interval']"> + <MkPreferenceContainer k="pollingInterval"> + <MkRange v-model="pollingInterval" :min="1" :max="3" :step="1" easing :showTicks="true" :textConverter="(v) => v === 1 ? i18n.ts.low : v === 2 ? i18n.ts.middle : v === 3 ? i18n.ts.high : ''"> + <template #label><SearchLabel>{{ i18n.ts._settings.contentsUpdateFrequency }}</SearchLabel></template> + <template #caption><SearchKeyword>{{ i18n.ts._settings.contentsUpdateFrequency_description }}</SearchKeyword><br><SearchKeyword>{{ i18n.ts._settings.contentsUpdateFrequency_description2 }}</SearchKeyword></template> + <template #prefix><i class="ti ti-player-play"></i></template> + <template #suffix><i class="ti ti-player-track-next"></i></template> + </MkRange> + </MkPreferenceContainer> + </SearchMarker> + </MkDisableSection> + <div class="_gaps_s"> <SearchMarker :keywords="['titlebar', 'show']"> <MkPreferenceContainer k="showTitlebar"> @@ -148,22 +168,6 @@ SPDX-License-Identifier: AGPL-3.0-only </MkPreferenceContainer> </SearchMarker> - <SearchMarker :keywords="['note', 'timeline', 'gap']"> - <MkPreferenceContainer k="showGapBetweenNotesInTimeline"> - <MkSwitch v-model="showGapBetweenNotesInTimeline"> - <template #label><SearchLabel>{{ i18n.ts.showGapBetweenNotesInTimeline }}</SearchLabel></template> - </MkSwitch> - </MkPreferenceContainer> - </SearchMarker> - - <SearchMarker :keywords="['disable', 'streaming', 'timeline']"> - <MkPreferenceContainer k="disableStreamingTimeline"> - <MkSwitch v-model="disableStreamingTimeline"> - <template #label><SearchLabel>{{ i18n.ts.disableStreamingTimeline }}</SearchLabel></template> - </MkSwitch> - </MkPreferenceContainer> - </SearchMarker> - <SearchMarker :keywords="['pinned', 'list']"> <MkFolder> <template #label><SearchLabel>{{ i18n.ts.pinnedList }}</SearchLabel></template> @@ -734,7 +738,7 @@ import MkRadios from '@/components/MkRadios.vue'; import MkRange from '@/components/MkRange.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkButton from '@/components/MkButton.vue'; -import FormSection from '@/components/form/section.vue'; +import MkDisableSection from '@/components/MkDisableSection.vue'; import FormLink from '@/components/form/link.vue'; import MkLink from '@/components/MkLink.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -757,8 +761,10 @@ const $i = ensureSignin(); const lang = ref(miLocalStorage.getItem('lang')); const dataSaver = ref(prefer.s.dataSaver); +const realtimeMode = computed(store.makeGetterSetter('realtimeMode')); const overridedDeviceKind = prefer.model('overridedDeviceKind'); +const pollingInterval = prefer.model('pollingInterval'); const showTitlebar = prefer.model('showTitlebar'); const keepCw = prefer.model('keepCw'); const serverDisconnectedBehavior = prefer.model('serverDisconnectedBehavior'); @@ -777,7 +783,6 @@ const showFixedPostFormInChannel = prefer.model('showFixedPostFormInChannel'); const numberOfPageCache = prefer.model('numberOfPageCache'); const enableInfiniteScroll = prefer.model('enableInfiniteScroll'); const useReactionPickerForContextMenu = prefer.model('useReactionPickerForContextMenu'); -const disableStreamingTimeline = prefer.model('disableStreamingTimeline'); const useGroupedNotifications = prefer.model('useGroupedNotifications'); const alwaysConfirmFollow = prefer.model('alwaysConfirmFollow'); const confirmWhenRevealingSensitiveMedia = prefer.model('confirmWhenRevealingSensitiveMedia'); @@ -785,7 +790,6 @@ const confirmOnReact = prefer.model('confirmOnReact'); const defaultNoteVisibility = prefer.model('defaultNoteVisibility'); const defaultNoteLocalOnly = prefer.model('defaultNoteLocalOnly'); const rememberNoteVisibility = prefer.model('rememberNoteVisibility'); -const showGapBetweenNotesInTimeline = prefer.model('showGapBetweenNotesInTimeline'); const notificationPosition = prefer.model('notificationPosition'); const notificationStackAxis = prefer.model('notificationStackAxis'); const instanceTicker = prefer.model('instanceTicker'); @@ -843,13 +847,13 @@ watch(useSystemFont, () => { watch([ hemisphere, lang, + realtimeMode, + pollingInterval, enableInfiniteScroll, showNoteActionsOnlyHover, overridedDeviceKind, - disableStreamingTimeline, alwaysConfirmFollow, confirmWhenRevealingSensitiveMedia, - showGapBetweenNotesInTimeline, mediaListWithOneImageAppearance, reactionsDisplaySize, limitWidthOfReaction, diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue index e1dffd4f2d..d1e5db5a5b 100644 --- a/packages/frontend/src/pages/tag.vue +++ b/packages/frontend/src/pages/tag.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <PageWithHeader :actions="headerActions" :tabs="headerTabs"> <div class="_spacer" style="--MI_SPACER-w: 800px;"> - <MkNotes ref="notes" class="" :pagination="pagination"/> + <MkNotesTimeline ref="tlComponent" class="" :pagination="pagination"/> </div> <template v-if="$i" #footer> <div :class="$style.footer"> @@ -19,8 +19,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref } from 'vue'; -import MkNotes from '@/components/MkNotes.vue'; +import { computed, ref, useTemplateRef } from 'vue'; +import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import MkButton from '@/components/MkButton.vue'; import { definePage } from '@/page.js'; import { i18n } from '@/i18n.js'; @@ -40,7 +40,8 @@ const pagination = { tag: props.tag, })), }; -const notes = ref<InstanceType<typeof MkNotes>>(); + +const tlComponent = useTemplateRef('tlComponent'); async function post() { store.set('postFormHashtags', props.tag); @@ -48,7 +49,7 @@ async function post() { await os.post(); store.set('postFormHashtags', ''); store.set('postFormWithHashtags', false); - notes.value?.pagingComponent?.reload(); + tlComponent.value?.reload(); } const headerActions = computed(() => [{ diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index efe2689579..453a48d1bc 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -4,14 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<PageWithHeader ref="pageComponent" v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :swipable="true" :displayMyAvatar="true"> +<PageWithHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :swipable="true" :displayMyAvatar="true"> <div class="_spacer" style="--MI_SPACER-w: 800px;"> <MkInfo v-if="isBasicTimeline(src) && !store.r.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()"> {{ i18n.ts._timelineDescription[src] }} </MkInfo> <MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="_panel" fixed style="margin-bottom: var(--MI-margin);"/> - <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> - <MkTimeline + <MkStreamingNotesTimeline ref="tlComponent" :key="src + withRenotes + withReplies + onlyFiles + withSensitive" :class="$style.tl" @@ -22,7 +21,6 @@ SPDX-License-Identifier: AGPL-3.0-only :withSensitive="withSensitive" :onlyFiles="onlyFiles" :sound="true" - @queue="queueUpdated" /> </div> </PageWithHeader> @@ -33,7 +31,7 @@ import { computed, watch, provide, useTemplateRef, ref, onMounted, onActivated } import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; import type { MenuItem } from '@/types/menu.js'; import type { BasicTimelineType } from '@/timelines.js'; -import MkTimeline from '@/components/MkTimeline.vue'; +import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkPostForm from '@/components/MkPostForm.vue'; import * as os from '@/os.js'; @@ -51,11 +49,9 @@ import { prefer } from '@/preferences.js'; provide('shouldOmitHeaderTitle', true); const tlComponent = useTemplateRef('tlComponent'); -const pageComponent = useTemplateRef('pageComponent'); type TimelinePageSrc = BasicTimelineType | `list:${string}`; -const queue = ref(0); const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global'); const src = computed<TimelinePageSrc>({ get: () => ($i ? store.r.tl.value.src : srcWhenNotSignin.value), @@ -110,18 +106,6 @@ const withSensitive = computed<boolean>({ set: (x) => saveTlFilter('withSensitive', x), }); -watch(src, () => { - queue.value = 0; -}); - -function queueUpdated(q: number): void { - queue.value = q; -} - -function top(): void { - if (pageComponent.value) pageComponent.value.scrollToTop(); -} - async function chooseList(ev: MouseEvent): Promise<void> { const lists = await userListsCache.fetch(); const items: MenuItem[] = [ diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index e05e35d533..f166495258 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -6,17 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <PageWithHeader :actions="headerActions" :tabs="headerTabs"> <div class="_spacer" style="--MI_SPACER-w: 800px;"> - <div ref="rootEl"> - <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> - <div :class="$style.tl"> - <MkTimeline - ref="tlEl" :key="listId" - src="list" - :list="listId" - :sound="true" - @queue="queueUpdated" - /> - </div> + <div :class="$style.tl"> + <MkStreamingNotesTimeline + ref="tlEl" :key="listId" + src="list" + :list="listId" + :sound="true" + /> </div> </div> </PageWithHeader> @@ -25,8 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, ref, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; -import { scrollInContainer } from '@@/js/scroll.js'; -import MkTimeline from '@/components/MkTimeline.vue'; +import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; import { definePage } from '@/page.js'; import { i18n } from '@/i18n.js'; @@ -39,9 +34,6 @@ const props = defineProps<{ }>(); const list = ref<Misskey.entities.UserList | null>(null); -const queue = ref(0); -const tlEl = useTemplateRef('tlEl'); -const rootEl = useTemplateRef('rootEl'); watch(() => props.listId, async () => { list.value = await misskeyApi('users/lists/show', { @@ -49,14 +41,6 @@ watch(() => props.listId, async () => { }); }, { immediate: true }); -function queueUpdated(q) { - queue.value = q; -} - -function top() { - scrollInContainer(rootEl.value, { top: 0 }); -} - function settings() { router.push(`/my/lists/${props.listId}`); } @@ -76,25 +60,6 @@ definePage(() => ({ </script> <style lang="scss" module> -.new { - position: sticky; - top: calc(var(--MI-stickyTop, 0px) + 16px); - z-index: 1000; - width: 100%; - margin: calc(-0.675em - 8px) 0; - - &:first-child { - margin-top: calc(-0.675em - 8px - var(--MI-margin)); - } -} - -.newButton { - display: block; - margin: var(--MI-margin) auto 0 auto; - padding: 8px 16px; - border-radius: 32px; -} - .tl { background: var(--MI_THEME-bg); border-radius: var(--MI-radius); 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> diff --git a/packages/frontend/src/pages/welcome.timeline.note.vue b/packages/frontend/src/pages/welcome.timeline.note.vue index 680fe08c14..62a220d2f1 100644 --- a/packages/frontend/src/pages/welcome.timeline.note.vue +++ b/packages/frontend/src/pages/welcome.timeline.note.vue @@ -27,7 +27,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/> </div> <div v-if="note.reactionCount > 0" :class="$style.reactions"> - <MkReactionsViewer :note="note" :maxNumber="16"/> + <!-- TODO --> + <!--<MkReactionsViewer :note="note" :maxNumber="16"/>--> </div> </div> </div> |