fixes from peer review

This commit is contained in:
Hazel K 2024-10-02 11:38:21 -04:00
parent 19204851a0
commit ef7cde6bc6
7 changed files with 30 additions and 24 deletions

View File

@ -1134,7 +1134,8 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
private async updateLatestNote(note: MiNote) { private async updateLatestNote(note: MiNote) {
// Ignore DMs // Ignore DMs.
// Followers-only posts are *included*, as this table is used to back the "following" feed.
if (note.visibility === 'specified') return; if (note.visibility === 'specified') return;
// Ignore pure renotes // Ignore pure renotes
@ -1143,7 +1144,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// Make sure that this isn't an *older* post. // Make sure that this isn't an *older* post.
// We can get older posts through replies, lookups, etc. // We can get older posts through replies, lookups, etc.
const currentLatest = await this.latestNotesRepository.findOneBy({ userId: note.userId }); const currentLatest = await this.latestNotesRepository.findOneBy({ userId: note.userId });
if (currentLatest != null && currentLatest.userId >= note.id) return; if (currentLatest != null && currentLatest.noteId >= note.id) return;
// Record this as the latest note for the given user // Record this as the latest note for the given user
const latestNote = new LatestNote({ const latestNote = new LatestNote({

View File

@ -240,6 +240,10 @@ export class NoteDeleteService {
// If it's a DM, then it can't possibly be the latest note so we can safely skip this. // If it's a DM, then it can't possibly be the latest note so we can safely skip this.
if (note.visibility === 'specified') return; if (note.visibility === 'specified') return;
// Check if the deleted note was possibly the latest for the user
const hasLatestNote = await this.latestNotesRepository.existsBy({ userId: note.userId });
if (hasLatestNote) return;
// Find the newest remaining note for the user. // Find the newest remaining note for the user.
// We exclude DMs and pure renotes. // We exclude DMs and pure renotes.
const nextLatest = await this.notesRepository const nextLatest = await this.notesRepository
@ -269,12 +273,14 @@ export class NoteDeleteService {
noteId: nextLatest.id, noteId: nextLatest.id,
}); });
// We use an upsert because this deleted note might not have been the newest. // When inserting the latest note, it's possible that another worker has "raced" the insert and already added a newer note.
// In that case, the latest note may already be populated for this user. // We must use orIgnore() to ensure that the query ignores conflicts, otherwise an exception may be thrown.
// We want postgres to do nothing instead of replacing the value or returning an error. await this.latestNotesRepository
await this.latestNotesRepository.upsert(latestNote, { .createQueryBuilder('latest')
conflictPaths: ['userId'], .insert()
skipUpdateIfNoValuesChanged: true, .into(LatestNote)
}); .values(latestNote)
.orIgnore()
.execute();
} }
} }

View File

@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA> </MkA>
</header> </header>
<div> <div>
<Mfm :class="$style.text" :text="getNoteSummary(note)" :isBlock="false" :plain="true" :nowrap="false" :isNote="true" :author="note.user"/> <Mfm :class="$style.text" :text="getNoteSummary(note)" :isBlock="true" :plain="true" :nowrap="false" :isNote="true" nyaize="respect" :author="note.user"/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -70,7 +70,7 @@ export const navbarItemDef = reactive({
}, },
following: { following: {
title: i18n.ts.following, title: i18n.ts.following,
icon: 'ti ti-user-check', icon: 'ph-user-check ph-bold ph-lg',
to: '/following-feed', to: '/following-feed',
}, },
lists: { lists: {

View File

@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkPagination> </MkPagination>
</MkPullToRefresh> </MkPullToRefresh>
<MkPullToRefresh v-if="isDesktop" :refresher="() => reloadUserNotes()"> <MkPullToRefresh v-if="isWideViewport" :refresher="() => reloadUserNotes()">
<div v-if="selectedUser" :class="$style.userInfo"> <div v-if="selectedUser" :class="$style.userInfo">
<MkUserInfo class="user" :user="selectedUser"/> <MkUserInfo class="user" :user="selectedUser"/>
<MkNotes :noGap="true" :pagination="userNotesPagination"/> <MkNotes :noGap="true" :pagination="userNotesPagination"/>
@ -62,7 +62,7 @@ import FollowingFeedEntry from '@/components/FollowingFeedEntry.vue';
import MkNotes from '@/components/MkNotes.vue'; import MkNotes from '@/components/MkNotes.vue';
import MkUserInfo from '@/components/MkUserInfo.vue'; import MkUserInfo from '@/components/MkUserInfo.vue';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import {useRouter} from "@/router/supplier.js"; import { useRouter } from '@/router/supplier.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
initialTab?: FollowingFeedTab, initialTab?: FollowingFeedTab,
@ -79,17 +79,17 @@ const currentTab: Ref<FollowingFeedTab> = ref(props.initialTab);
const mutualsOnly: Ref<boolean> = computed(() => currentTab.value === mutualsTab); const mutualsOnly: Ref<boolean> = computed(() => currentTab.value === mutualsTab);
// We have to disable the per-user feed on small displays, and it must be done through JS instead of CSS. // We have to disable the per-user feed on small displays, and it must be done through JS instead of CSS.
// Otherwise, the second column will resources in the background. // Otherwise, the second column will waste resources in the background.
const desktopMediaQuery = window.matchMedia('(min-width: 750px)'); const wideViewportQuery = window.matchMedia('(min-width: 750px)');
const isDesktop: Ref<boolean> = ref(desktopMediaQuery.matches); const isWideViewport: Ref<boolean> = ref(wideViewportQuery.matches);
desktopMediaQuery.addEventListener('change', () => isDesktop.value = desktopMediaQuery.matches); wideViewportQuery.addEventListener('change', () => isWideViewport.value = wideViewportQuery.matches);
const selectedUserError: Ref<string> = ref(''); const selectedUserError: Ref<string> = ref('');
const selectedUserId: Ref<string> = ref(''); const selectedUserId: Ref<string> = ref('');
const selectedUser: Ref<Misskey.entities.UserDetailed | null> = ref(null); const selectedUser: Ref<Misskey.entities.UserDetailed | null> = ref(null);
async function userSelected(user: Misskey.entities.UserLite): Promise<void> { async function userSelected(user: Misskey.entities.UserLite): Promise<void> {
if (isDesktop.value) { if (isWideViewport.value) {
await showUserNotes(user.id); await showUserNotes(user.id);
} else { } else {
if (user.host) { if (user.host) {
@ -139,7 +139,7 @@ async function onListReady(): Promise<void> {
// This just gets the first user ID // This just gets the first user ID
const selectedNote: Misskey.entities.Note = latestNotesPaging.value.items.values().next().value; const selectedNote: Misskey.entities.Note = latestNotesPaging.value.items.values().next().value;
// Wait for 1 second to match the animation effects. // Wait for 1 second to match the animation effects in MkHorizontalSwipe, MkPullToRefresh, and MkPagination.
// Otherwise, the page appears to load "backwards". // Otherwise, the page appears to load "backwards".
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
await showUserNotes(selectedNote.userId); await showUserNotes(selectedNote.userId);
@ -179,19 +179,19 @@ const headerActions: PageHeaderItem[] = [
const headerTabs = computed(() => [ const headerTabs = computed(() => [
{ {
key: followingTab, key: followingTab,
icon: 'ti ti-user-check', icon: 'ph-user-check ph-bold ph-lg',
title: i18n.ts.following, title: i18n.ts.following,
} satisfies Tab, } satisfies Tab,
{ {
key: mutualsTab, key: mutualsTab,
icon: 'ti ti-user-heart', icon: 'ph-user-switch ph-bold ph-lg',
title: i18n.ts.mutuals, title: i18n.ts.mutuals,
} satisfies Tab, } satisfies Tab,
]); ]);
definePageMetadata(() => ({ definePageMetadata(() => ({
title: i18n.ts.following, title: i18n.ts.following,
icon: 'ti ti-user-check', icon: 'ph-user-check ph-bold ph-lg',
})); }));
</script> </script>

View File

@ -312,7 +312,7 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList
icon: basicTimelineIconClass(tl), icon: basicTimelineIconClass(tl),
iconOnly: true, iconOnly: true,
})), { })), {
icon: 'ti ti-user-check', icon: 'ph-user-check ph-bold ph-lg',
title: i18n.ts.following, title: i18n.ts.following,
iconOnly: true, iconOnly: true,
onClick: () => router.push('/following-feed'), onClick: () => router.push('/following-feed'),

View File

@ -348,7 +348,6 @@ export function pluginReplaceIcons() {
'ti ti-user-circle': 'ph-user-circle ph-bold ph-lg', 'ti ti-user-circle': 'ph-user-circle ph-bold ph-lg',
'ti ti-user-edit': 'ph-user-list ph-bold ph-lg', 'ti ti-user-edit': 'ph-user-list ph-bold ph-lg',
'ti ti-user-exclamation': 'ph-warning-circle ph-bold ph-lg', 'ti ti-user-exclamation': 'ph-warning-circle ph-bold ph-lg',
'ti ti-user-heart': 'ph-user-switch ph-bold ph-lg',
'ti ti-user-off': 'ph-user-minus ph-bold ph-lg', 'ti ti-user-off': 'ph-user-minus ph-bold ph-lg',
'ti ti-user-plus': 'ph-user-plus ph-bold ph-lg', 'ti ti-user-plus': 'ph-user-plus ph-bold ph-lg',
'ti ti-user-search': 'ph-user-circle ph-bold ph-lg', 'ti ti-user-search': 'ph-user-circle ph-bold ph-lg',