summaryrefslogtreecommitdiff
path: root/packages/frontend/src/pages
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
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')
-rw-r--r--packages/frontend/src/pages/about.federation.vue4
-rw-r--r--packages/frontend/src/pages/admin/abuses.vue2
-rw-r--r--packages/frontend/src/pages/admin/invites.vue8
-rw-r--r--packages/frontend/src/pages/admin/users.vue2
-rw-r--r--packages/frontend/src/pages/announcements.vue12
-rw-r--r--packages/frontend/src/pages/antenna-timeline.vue50
-rw-r--r--packages/frontend/src/pages/avatar-decoration-edit-dialog.vue4
-rw-r--r--packages/frontend/src/pages/channel.vue19
-rw-r--r--packages/frontend/src/pages/clip.vue4
-rw-r--r--packages/frontend/src/pages/custom-emojis-manager.vue20
-rw-r--r--packages/frontend/src/pages/drive.file.notes.vue8
-rw-r--r--packages/frontend/src/pages/emoji-edit-dialog.vue4
-rw-r--r--packages/frontend/src/pages/explore.featured.vue6
-rw-r--r--packages/frontend/src/pages/follow-requests.vue10
-rw-r--r--packages/frontend/src/pages/instance-info.vue4
-rw-r--r--packages/frontend/src/pages/invite.vue8
-rw-r--r--packages/frontend/src/pages/my-clips/index.vue6
-rw-r--r--packages/frontend/src/pages/my-lists/list.vue10
-rw-r--r--packages/frontend/src/pages/note.vue79
-rw-r--r--packages/frontend/src/pages/notifications.vue10
-rw-r--r--packages/frontend/src/pages/role.vue4
-rw-r--r--packages/frontend/src/pages/search.note.vue8
-rw-r--r--packages/frontend/src/pages/search.user.vue4
-rw-r--r--packages/frontend/src/pages/settings/apps.vue6
-rw-r--r--packages/frontend/src/pages/settings/preferences.vue46
-rw-r--r--packages/frontend/src/pages/tag.vue11
-rw-r--r--packages/frontend/src/pages/timeline.vue22
-rw-r--r--packages/frontend/src/pages/user-list-timeline.vue51
-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
-rw-r--r--packages/frontend/src/pages/welcome.timeline.note.vue3
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>