feat: improve tl performance (#11946)

* wip

* wip

* wip

* wip

* wip

* wip

* Update NoteCreateService.ts

* wip

* wip

* wip

* wip

* Update NoteCreateService.ts

* wip

* Update NoteCreateService.ts

* wip

* Update user-notes.ts

* wip

* wip

* wip

* Update NoteCreateService.ts

* wip

* Update timeline.ts

* Update timeline.ts

* Update timeline.ts

* Update timeline.ts

* Update timeline.ts

* wip

* Update timelines.ts

* Update timelines.ts

* Update timelines.ts

* wip

* wip

* wip

* Update timelines.ts

* Update misskey-js.api.md

* Update timelines.ts

* Update timelines.ts

* wip

* wip

* wip

* Update timelines.ts

* wip

* Update timelines.ts

* wip

* test

* Update activitypub.ts

* refactor: UserListJoining -> UserListMembership

* Update NoteCreateService.ts

* wip
This commit is contained in:
syuilo
2023-10-03 20:26:11 +09:00
committed by GitHub
parent 5ee93dc4a2
commit 6277a5545c
84 changed files with 1898 additions and 947 deletions

View File

@@ -29,16 +29,22 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s">
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
<div v-for="user in users" :key="user.id" :class="$style.userItem">
<MkA :class="$style.userItemBody" :to="`${userPage(user)}`">
<MkUserCardMini :user="user"/>
</MkA>
<button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button>
</div>
<MkButton v-if="!fetching && queueUserIds.length !== 0" v-appear="enableInfiniteScroll ? fetchMoreUsers : null" :class="$style.more" :style="{ cursor: 'pointer' }" primary rounded @click="fetchMoreUsers">
{{ i18n.ts.loadMore }}
</MkButton>
<MkLoading v-if="fetching" class="loading"/>
<MkPagination ref="paginationEl" :pagination="membershipsPagination">
<template #default="{ items }">
<div class="_gaps_s">
<div v-for="item in items" :key="item.id">
<div :class="$style.userItem">
<MkA :class="$style.userItemBody" :to="`${userPage(item.user)}`">
<MkUserCardMini :user="item.user"/>
</MkA>
<button class="_button" :class="$style.menu" @click="showMembershipMenu(item, $event)"><i class="ti ti-dots"></i></button>
<button class="_button" :class="$style.remove" @click="removeUser(item, $event)"><i class="ti ti-x"></i></button>
</div>
</div>
</div>
</template>
</MkPagination>
</div>
</MkFolder>
</div>
@@ -59,9 +65,11 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkInput from '@/components/MkInput.vue';
import { userListsCache } from '@/cache';
import { userListsCache } from '@/cache.js';
import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
const {
enableInfiniteScroll,
} = defaultStore.reactiveState;
@@ -70,40 +78,25 @@ const props = defineProps<{
listId: string;
}>();
const FETCH_USERS_LIMIT = 20;
const paginationEl = ref<InstanceType<typeof MkPagination>>();
let list = $ref<Misskey.entities.UserList | null>(null);
let users = $ref<Misskey.entities.UserLite[]>([]);
let queueUserIds = $ref<string[]>([]);
let fetching = $ref(true);
const isPublic = ref(false);
const name = ref('');
const membershipsPagination = {
endpoint: 'users/lists/get-memberships' as const,
limit: 30,
params: computed(() => ({
listId: props.listId,
})),
};
function fetchList() {
fetching = true;
os.api('users/lists/show', {
listId: props.listId,
}).then(_list => {
list = _list;
name.value = list.name;
isPublic.value = list.isPublic;
queueUserIds = list.userIds;
return fetchMoreUsers();
});
}
function fetchMoreUsers() {
if (!list) return;
if (fetching && users.length !== 0) return; // fetchingがtrueならやめるが、usersが空なら続行
fetching = true;
os.api('users/show', {
userIds: queueUserIds.slice(0, FETCH_USERS_LIMIT),
}).then(_users => {
users = users.concat(_users);
queueUserIds = queueUserIds.slice(FETCH_USERS_LIMIT);
}).finally(() => {
fetching = false;
});
}
@@ -114,12 +107,12 @@ function addUser() {
listId: list.id,
userId: user.id,
}).then(() => {
users.push(user);
paginationEl.value.reload();
});
});
}
async function removeUser(user, ev) {
async function removeUser(item, ev) {
os.popupMenu([{
text: i18n.ts.remove,
icon: 'ti ti-x',
@@ -128,9 +121,28 @@ async function removeUser(user, ev) {
if (!list) return;
os.api('users/lists/pull', {
listId: list.id,
userId: user.id,
userId: item.userId,
}).then(() => {
users = users.filter(x => x.id !== user.id);
paginationEl.value.removeItem(item.id);
});
},
}], ev.currentTarget ?? ev.target);
}
async function showMembershipMenu(item, ev) {
os.popupMenu([{
text: item.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline,
icon: item.withReplies ? 'ti ti-messages-off' : 'ti ti-messages',
action: async () => {
os.api('users/lists/update-membership', {
listId: list.id,
userId: item.userId,
withReplies: !item.withReplies,
}).then(() => {
paginationEl.value.updateItem(item.id, (old) => ({
...old,
withReplies: !item.withReplies,
}));
});
},
}], ev.currentTarget ?? ev.target);
@@ -202,6 +214,12 @@ definePageMetadata(computed(() => list ? {
align-self: center;
}
.menu {
width: 32px;
height: 32px;
align-self: center;
}
.more {
margin-left: auto;
margin-right: auto;

View File

@@ -5,29 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
<MkTab v-model="tab">
<option value="soft">{{ i18n.ts._wordMute.soft }}</option>
<option value="hard">{{ i18n.ts._wordMute.hard }}</option>
</MkTab>
<div>
<div v-show="tab === 'soft'" class="_gaps_m">
<MkInfo>{{ i18n.ts._wordMute.softDescription }}</MkInfo>
<MkTextarea v-model="softMutedWords">
<span>{{ i18n.ts._wordMute.muteWords }}</span>
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
</MkTextarea>
</div>
<div v-show="tab === 'hard'" class="_gaps_m">
<MkInfo>{{ i18n.ts._wordMute.hardDescription }} {{ i18n.ts.reflectMayTakeTime }}</MkInfo>
<MkTextarea v-model="hardMutedWords">
<span>{{ i18n.ts._wordMute.muteWords }}</span>
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
</MkTextarea>
<MkKeyValue v-if="hardWordMutedNotesCount != null">
<template #key>{{ i18n.ts._wordMute.mutedNotes }}</template>
<template #value>{{ number(hardWordMutedNotesCount) }}</template>
</MkKeyValue>
</div>
<MkTextarea v-model="mutedWords">
<span>{{ i18n.ts._wordMute.muteWords }}</span>
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
</MkTextarea>
</div>
<MkButton primary inline :disabled="!changed" @click="save()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
@@ -56,25 +38,15 @@ const render = (mutedWords) => mutedWords.map(x => {
}).join('\n');
const tab = ref('soft');
const softMutedWords = ref(render(defaultStore.state.mutedWords));
const hardMutedWords = ref(render($i!.mutedWords));
const hardWordMutedNotesCount = ref(null);
const mutedWords = ref(render($i!.mutedWords));
const changed = ref(false);
os.api('i/get-word-muted-notes-count', {}).then(response => {
hardWordMutedNotesCount.value = response?.count;
});
watch(softMutedWords, () => {
changed.value = true;
});
watch(hardMutedWords, () => {
watch(mutedWords, () => {
changed.value = true;
});
async function save() {
const parseMutes = (mutes, tab) => {
const parseMutes = (mutes) => {
// split into lines, remove empty lines and unnecessary whitespace
let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '');
@@ -92,7 +64,7 @@ async function save() {
os.alert({
type: 'error',
title: i18n.ts.regexpError,
text: i18n.t('regexpErrorDescription', { tab, line: i + 1 }) + '\n' + err.toString(),
text: i18n.t('regexpErrorDescription', { tab: 'word mute', line: i + 1 }) + '\n' + err.toString(),
});
// re-throw error so these invalid settings are not saved
throw err;
@@ -105,18 +77,16 @@ async function save() {
return lines;
};
let softMutes, hardMutes;
let parsed;
try {
softMutes = parseMutes(softMutedWords.value, i18n.ts._wordMute.soft);
hardMutes = parseMutes(hardMutedWords.value, i18n.ts._wordMute.hard);
parsed = parseMutes(mutedWords.value);
} catch (err) {
// already displayed error message in parseMutes
return;
}
defaultStore.set('mutedWords', softMutes);
await os.api('i/update', {
mutedWords: hardMutes,
mutedWords: parsed,
});
changed.value = false;

View File

@@ -15,11 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.tl">
<MkTimeline
ref="tlComponent"
:key="src + withRenotes + withReplies + onlyFiles"
:key="src + withRenotes + onlyFiles"
:src="src.split(':')[0]"
:list="src.split(':')[1]"
:withRenotes="withRenotes"
:withReplies="withReplies"
:onlyFiles="onlyFiles"
:sound="true"
@queue="queueUpdated"
@@ -62,7 +61,6 @@ let queue = $ref(0);
let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global');
const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) });
const withRenotes = $ref(true);
const withReplies = $ref(false);
const onlyFiles = $ref(false);
watch($$(src), () => queue = 0);
@@ -144,11 +142,6 @@ const headerActions = $computed(() => [{
text: i18n.ts.showRenotes,
icon: 'ti ti-repeat',
ref: $$(withRenotes),
}, {
type: 'switch',
text: i18n.ts.withReplies,
icon: 'ti ti-arrow-back-up',
ref: $$(withReplies),
}, {
type: 'switch',
text: i18n.ts.fileAttachedOnly,

View File

@@ -128,14 +128,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo>
<template v-if="narrow">
<XPhotos :key="user.id" :user="user"/>
<XFiles :key="user.id" :user="user"/>
<XActivity :key="user.id" :user="user"/>
</template>
<MkNotes v-if="!disableNotes" :class="$style.tl" :noGap="true" :pagination="pagination"/>
</div>
</div>
<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
<XPhotos :key="user.id" :user="user"/>
<XFiles :key="user.id" :user="user"/>
<XActivity :key="user.id" :user="user"/>
</div>
</div>
@@ -182,7 +182,7 @@ function calcAge(birthdate: string): number {
return yearDiff;
}
const XPhotos = defineAsyncComponent(() => import('./index.photos.vue'));
const XFiles = defineAsyncComponent(() => import('./index.files.vue'));
const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
const props = withDefaults(defineProps<{

View File

@@ -6,20 +6,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkContainer :max-height="300" :foldable="true">
<template #icon><i class="ti ti-photo"></i></template>
<template #header>{{ i18n.ts.images }}</template>
<template #header>{{ i18n.ts.files }}</template>
<div :class="$style.root">
<MkLoading v-if="fetching"/>
<div v-if="!fetching && images.length > 0" :class="$style.stream">
<div v-if="!fetching && files.length > 0" :class="$style.stream">
<MkA
v-for="image in images"
:key="image.note.id + image.file.id"
v-for="file in files"
:key="file.note.id + file.file.id"
:class="$style.img"
:to="notePage(image.note)"
:to="notePage(file.note)"
>
<ImgWithBlurhash :hash="image.file.blurhash" :src="thumbnail(image.file)" :title="image.file.name"/>
<!-- TODO: 画像以外のファイルに対応 -->
<ImgWithBlurhash :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name"/>
</MkA>
</div>
<p v-if="!fetching && images.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p>
<p v-if="!fetching && files.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p>
</div>
</MkContainer>
</template>
@@ -40,7 +41,7 @@ const props = defineProps<{
}>();
let fetching = $ref(true);
let images = $ref<{
let files = $ref<{
note: Misskey.entities.Note;
file: Misskey.entities.DriveFile;
}[]>([]);
@@ -52,24 +53,15 @@ function thumbnail(image: Misskey.entities.DriveFile): string {
}
onMounted(() => {
const image = [
'image/jpeg',
'image/webp',
'image/avif',
'image/png',
'image/gif',
'image/apng',
'image/vnd.mozilla.apng',
];
os.api('users/notes', {
userId: props.user.id,
fileType: image,
withFiles: true,
excludeNsfw: defaultStore.state.nsfw !== 'ignore',
limit: 10,
limit: 15,
}).then(notes => {
for (const note of notes) {
for (const file of note.files) {
images.push({
files.push({
note,
file,
});