From f8b55d357d0f4eaa33d3da000e15bfc4df8bf5de Mon Sep 17 00:00:00 2001 From: Hazel K Date: Sun, 29 Sep 2024 21:27:38 -0400 Subject: [PATCH 01/69] add `latest_note` table --- .../backend/1727659258948-add_latest_note.js | 15 ++++++++ packages/backend/src/di-symbols.ts | 1 + packages/backend/src/models/LatestNote.ts | 37 +++++++++++++++++++ .../backend/src/models/RepositoryModule.ts | 9 +++++ packages/backend/src/models/_.ts | 3 ++ packages/backend/src/postgres.ts | 2 + 6 files changed, 67 insertions(+) create mode 100644 packages/backend/1727659258948-add_latest_note.js create mode 100644 packages/backend/src/models/LatestNote.ts diff --git a/packages/backend/1727659258948-add_latest_note.js b/packages/backend/1727659258948-add_latest_note.js new file mode 100644 index 0000000000..b63133cb38 --- /dev/null +++ b/packages/backend/1727659258948-add_latest_note.js @@ -0,0 +1,15 @@ +export class AddLatestNote1727659258948 { + name = 'AddLatestNote1727659258948'; + + async up(queryRunner) { + await queryRunner.query('CREATE TABLE "latest_note" ("user_id" character varying(32) NOT NULL, "note_id" character varying(32) NOT NULL, "userId" character varying(32), "noteId" character varying(32), CONSTRAINT "PK_f619b62bfaafabe68f52fb50c9a" PRIMARY KEY ("user_id"))'); + await queryRunner.query('ALTER TABLE "latest_note" ADD CONSTRAINT "FK_20e346fffe4a2174585005d6d80" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION'); + await queryRunner.query('ALTER TABLE "latest_note" ADD CONSTRAINT "FK_47a38b1c13de6ce4e5090fb1acd" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION'); + } + + async down(queryRunner) { + await queryRunner.query('ALTER TABLE "latest_note" DROP CONSTRAINT "FK_47a38b1c13de6ce4e5090fb1acd"'); + await queryRunner.query('ALTER TABLE "latest_note" DROP CONSTRAINT "FK_20e346fffe4a2174585005d6d80"'); + await queryRunner.query('DROP TABLE "latest_note"'); + } +} diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index d4a21ab625..72a9aed4f3 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -19,6 +19,7 @@ export const DI = { announcementReadsRepository: Symbol('announcementReadsRepository'), appsRepository: Symbol('appsRepository'), avatarDecorationsRepository: Symbol('avatarDecorationsRepository'), + latestNotesRepository: Symbol('latestNotesRepository'), noteFavoritesRepository: Symbol('noteFavoritesRepository'), noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'), noteReactionsRepository: Symbol('noteReactionsRepository'), diff --git a/packages/backend/src/models/LatestNote.ts b/packages/backend/src/models/LatestNote.ts new file mode 100644 index 0000000000..9d56b82620 --- /dev/null +++ b/packages/backend/src/models/LatestNote.ts @@ -0,0 +1,37 @@ +import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { MiUser } from '@/models/User.js'; +import { MiNote } from '@/models/Note.js'; + +/** + * Maps a user to the most recent post by that user. + * Public, home-only, and followers-only posts are included. + * DMs are not counted. + */ +@Entity('latest_note') +export class LatestNote { + @PrimaryColumn({ + name: 'user_id', + type: 'varchar' as const, + length: 32, + }) + public userId: string; + + @ManyToOne(() => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column({ + name: 'note_id', + type: 'varchar' as const, + length: 32, + }) + public noteId: string; + + @ManyToOne(() => MiNote, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: MiNote | null; +} diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 1eaeb86df6..f44334d84e 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -7,6 +7,7 @@ import type { Provider } from '@nestjs/common'; import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import { + LatestNote, MiAbuseReportNotificationRecipient, MiAbuseUserReport, MiAccessToken, @@ -118,6 +119,12 @@ const $avatarDecorationsRepository: Provider = { inject: [DI.db], }; +const $latestNotesRepository: Provider = { + provide: DI.latestNotesRepository, + useFactory: (db: DataSource) => db.getRepository(LatestNote).extend(miRepository as MiRepository), + inject: [DI.db], +}; + const $noteFavoritesRepository: Provider = { provide: DI.noteFavoritesRepository, useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository), @@ -511,6 +518,7 @@ const $reversiGamesRepository: Provider = { $announcementReadsRepository, $appsRepository, $avatarDecorationsRepository, + $latestNotesRepository, $noteFavoritesRepository, $noteThreadMutingsRepository, $noteReactionsRepository, @@ -583,6 +591,7 @@ const $reversiGamesRepository: Provider = { $announcementReadsRepository, $appsRepository, $avatarDecorationsRepository, + $latestNotesRepository, $noteFavoritesRepository, $noteThreadMutingsRepository, $noteReactionsRepository, diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index f7646dce2a..1a79aeb80d 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -10,6 +10,7 @@ import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLo import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js'; import { ObjectUtils } from 'typeorm/util/ObjectUtils.js'; import { OrmUtils } from 'typeorm/util/OrmUtils.js'; +import { LatestNote } from '@/models/LatestNote.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; import { MiAccessToken } from '@/models/AccessToken.js'; @@ -126,6 +127,7 @@ export const miRepository = { } satisfies MiRepository; export { + LatestNote, MiAbuseUserReport, MiAbuseReportNotificationRecipient, MiAccessToken, @@ -224,6 +226,7 @@ export type GalleryPostsRepository = Repository & MiRepository & MiRepository; export type InstancesRepository = Repository & MiRepository; export type MetasRepository = Repository & MiRepository; +export type LatestNoteRepository = Repository & MiRepository; export type ModerationLogsRepository = Repository & MiRepository; export type MutingsRepository = Repository & MiRepository; export type RenoteMutingsRepository = Repository & MiRepository; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 047b7f8ae9..de7353dc8c 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -83,6 +83,7 @@ import { MiReversiGame } from '@/models/ReversiGame.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import {LatestNote} from "@/models/LatestNote.js"; pg.types.setTypeParser(20, Number); @@ -130,6 +131,7 @@ class MyCustomLogger implements Logger { } export const entities = [ + LatestNote, MiAnnouncement, MiAnnouncementRead, MiMeta, From 80b3da531ed6c164308fcb01b922bfdcd7df1488 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Sun, 29 Sep 2024 21:52:43 -0400 Subject: [PATCH 02/69] fix name of LatestNotesRepository --- packages/backend/src/models/_.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index 1a79aeb80d..9e01f4b6d7 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -226,7 +226,7 @@ export type GalleryPostsRepository = Repository & MiRepository & MiRepository; export type InstancesRepository = Repository & MiRepository; export type MetasRepository = Repository & MiRepository; -export type LatestNoteRepository = Repository & MiRepository; +export type LatestNotesRepository = Repository & MiRepository; export type ModerationLogsRepository = Repository & MiRepository; export type MutingsRepository = Repository & MiRepository; export type RenoteMutingsRepository = Repository & MiRepository; From a3d67b58ed79184abd0381ed73e4a434ee63163e Mon Sep 17 00:00:00 2001 From: Hazel K Date: Sun, 29 Sep 2024 21:52:57 -0400 Subject: [PATCH 03/69] add copy constructor to LatestNote --- packages/backend/src/models/LatestNote.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/backend/src/models/LatestNote.ts b/packages/backend/src/models/LatestNote.ts index 9d56b82620..07aee73e03 100644 --- a/packages/backend/src/models/LatestNote.ts +++ b/packages/backend/src/models/LatestNote.ts @@ -34,4 +34,13 @@ export class LatestNote { }) @JoinColumn() public note: MiNote | null; + + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } } From 06932710f005ec92479ee3c5778900ef61f404de Mon Sep 17 00:00:00 2001 From: Hazel K Date: Sun, 29 Sep 2024 22:39:06 -0400 Subject: [PATCH 04/69] move migration to correct folder --- packages/backend/{ => migration}/1727659258948-add_latest_note.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/backend/{ => migration}/1727659258948-add_latest_note.js (100%) diff --git a/packages/backend/1727659258948-add_latest_note.js b/packages/backend/migration/1727659258948-add_latest_note.js similarity index 100% rename from packages/backend/1727659258948-add_latest_note.js rename to packages/backend/migration/1727659258948-add_latest_note.js From d1b5b54bad4efba9b406557bb9d084b3c0f6d2a8 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Sun, 29 Sep 2024 22:50:29 -0400 Subject: [PATCH 05/69] remove extra space --- packages/backend/src/models/LatestNote.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/models/LatestNote.ts b/packages/backend/src/models/LatestNote.ts index 07aee73e03..5cce09e556 100644 --- a/packages/backend/src/models/LatestNote.ts +++ b/packages/backend/src/models/LatestNote.ts @@ -35,7 +35,6 @@ export class LatestNote { @JoinColumn() public note: MiNote | null; - constructor(data: Partial) { if (data == null) return; From 0a09e6d86a2feb03f59e00210fe36b509333f1d7 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Sun, 29 Sep 2024 22:50:39 -0400 Subject: [PATCH 06/69] clean up copy constructor --- packages/backend/src/models/LatestNote.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/backend/src/models/LatestNote.ts b/packages/backend/src/models/LatestNote.ts index 5cce09e556..24cb9607b0 100644 --- a/packages/backend/src/models/LatestNote.ts +++ b/packages/backend/src/models/LatestNote.ts @@ -36,10 +36,8 @@ export class LatestNote { public note: MiNote | null; constructor(data: Partial) { - if (data == null) return; - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; + (this as Record)[k] = v; } } } From 01ec9635c1636af9b9755e3b1a3638e85591f7c3 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Sun, 29 Sep 2024 22:51:24 -0400 Subject: [PATCH 07/69] remove duplicate generated columns from LatestNote --- .../backend/migration/1727659258948-add_latest_note.js | 6 +++--- packages/backend/src/models/LatestNote.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/backend/migration/1727659258948-add_latest_note.js b/packages/backend/migration/1727659258948-add_latest_note.js index b63133cb38..739aaf0775 100644 --- a/packages/backend/migration/1727659258948-add_latest_note.js +++ b/packages/backend/migration/1727659258948-add_latest_note.js @@ -2,9 +2,9 @@ export class AddLatestNote1727659258948 { name = 'AddLatestNote1727659258948'; async up(queryRunner) { - await queryRunner.query('CREATE TABLE "latest_note" ("user_id" character varying(32) NOT NULL, "note_id" character varying(32) NOT NULL, "userId" character varying(32), "noteId" character varying(32), CONSTRAINT "PK_f619b62bfaafabe68f52fb50c9a" PRIMARY KEY ("user_id"))'); - await queryRunner.query('ALTER TABLE "latest_note" ADD CONSTRAINT "FK_20e346fffe4a2174585005d6d80" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION'); - await queryRunner.query('ALTER TABLE "latest_note" ADD CONSTRAINT "FK_47a38b1c13de6ce4e5090fb1acd" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION'); + await queryRunner.query('CREATE TABLE "latest_note" ("user_id" character varying(32) NOT NULL, "note_id" character varying(32) NOT NULL, CONSTRAINT "PK_f619b62bfaafabe68f52fb50c9a" PRIMARY KEY ("user_id"))'); + await queryRunner.query('ALTER TABLE "latest_note" ADD CONSTRAINT "FK_20e346fffe4a2174585005d6d80" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION'); + await queryRunner.query('ALTER TABLE "latest_note" ADD CONSTRAINT "FK_47a38b1c13de6ce4e5090fb1acd" FOREIGN KEY ("note_id") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION'); } async down(queryRunner) { diff --git a/packages/backend/src/models/LatestNote.ts b/packages/backend/src/models/LatestNote.ts index 24cb9607b0..4b81c46d43 100644 --- a/packages/backend/src/models/LatestNote.ts +++ b/packages/backend/src/models/LatestNote.ts @@ -19,7 +19,9 @@ export class LatestNote { @ManyToOne(() => MiUser, { onDelete: 'CASCADE', }) - @JoinColumn() + @JoinColumn({ + name: 'user_id', + }) public user: MiUser | null; @Column({ @@ -32,7 +34,9 @@ export class LatestNote { @ManyToOne(() => MiNote, { onDelete: 'CASCADE', }) - @JoinColumn() + @JoinColumn({ + name: 'note_id', + }) public note: MiNote | null; constructor(data: Partial) { From 7b104ffe940b9919f4d986c3c5ee9349c60f0847 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Sun, 29 Sep 2024 23:22:58 -0400 Subject: [PATCH 08/69] fix crash during startup --- packages/backend/src/models/LatestNote.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/models/LatestNote.ts b/packages/backend/src/models/LatestNote.ts index 4b81c46d43..4a712c8e20 100644 --- a/packages/backend/src/models/LatestNote.ts +++ b/packages/backend/src/models/LatestNote.ts @@ -39,7 +39,9 @@ export class LatestNote { }) public note: MiNote | null; - constructor(data: Partial) { + constructor(data?: Partial) { + if (!data) return; + for (const [k, v] of Object.entries(data)) { (this as Record)[k] = v; } From ebff2eec87d2c55896dbd3625848101c16dbbdbb Mon Sep 17 00:00:00 2001 From: Hazel K Date: Sun, 29 Sep 2024 23:24:22 -0400 Subject: [PATCH 09/69] track latest note for each user --- .../backend/src/core/NoteCreateService.ts | 25 ++++++++++- .../backend/src/core/NoteDeleteService.ts | 41 ++++++++++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index c252336f99..17631eea89 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -14,7 +14,8 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; -import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import { LatestNote } from '@/models/LatestNote.js'; +import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, LatestNotesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; @@ -170,6 +171,9 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.notesRepository) private notesRepository: NotesRepository, + @Inject(DI.latestNotesRepository) + private latestNotesRepository: LatestNotesRepository, + @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, @@ -514,6 +518,8 @@ export class NoteCreateService implements OnApplicationShutdown { await this.notesRepository.insert(insert); } + await this.updateLatestNote(insert); + return insert; } catch (e) { // duplicate key error @@ -1125,4 +1131,21 @@ export class NoteCreateService implements OnApplicationShutdown { public onApplicationShutdown(signal?: string | undefined): void { this.dispose(); } + + private async updateLatestNote(note: MiNote) { + // Ignore DMs + if (note.visibility === 'specified') return; + + // Make sure that this isn't an *older* post. + // We can get older posts through replies, lookups, etc. + const currentLatest = await this.latestNotesRepository.findOneBy({ userId: note.userId }); + if (currentLatest != null && currentLatest.userId >= note.id) return; + + // Record this as the latest note for the given user + const latestNote = new LatestNote({ + userId: note.userId, + noteId: note.id, + }); + await this.latestNotesRepository.upsert(latestNote, ['userId']); + } } diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 7ce6d7c605..898e164966 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -3,11 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Brackets, In } from 'typeorm'; +import { Brackets, In, Not } from 'typeorm'; import { Injectable, Inject } from '@nestjs/common'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; -import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/_.js'; +import { LatestNote } from '@/models/LatestNote.js'; +import type { InstancesRepository, LatestNotesRepository, NotesRepository, UsersRepository } from '@/models/_.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -38,6 +39,9 @@ export class NoteDeleteService { @Inject(DI.notesRepository) private notesRepository: NotesRepository, + @Inject(DI.latestNotesRepository) + private latestNotesRepository: LatestNotesRepository, + @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, @@ -148,6 +152,8 @@ export class NoteDeleteService { userId: user.id, }); + await this.updateLatestNote(note); + if (deleter && (note.userId !== deleter.id)) { const user = await this.usersRepository.findOneByOrFail({ id: note.userId }); this.moderationLogService.log(deleter, 'deleteNote', { @@ -229,4 +235,35 @@ export class NoteDeleteService { this.apDeliverManagerService.deliverToUser(user, content, remoteUser); } } + + private async updateLatestNote(note: MiNote) { + // 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; + + // Find the newest remaining note for the user + const nextLatest = await this.notesRepository + .createQueryBuilder() + .select() + .where({ + userId: note.userId, + visibility: Not('specified'), + }) + .orderBy({ id: 'DESC' }) + .getOne(); + if (!nextLatest) return; + + // Record it as the latest + const latestNote = new LatestNote({ + userId: note.userId, + noteId: nextLatest.id, + }); + + // We use an upsert because this deleted note might not have been the newest. + // In that case, the latest note may already be populated for this user. + // We want postgres to do nothing instead of replacing the value or returning an error. + await this.latestNotesRepository.upsert(latestNote, { + conflictPaths: ['userId'], + skipUpdateIfNoValuesChanged: true, + }); + } } From 072f4b460865320ae437c0c838f588a632086db7 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 30 Sep 2024 00:49:45 -0400 Subject: [PATCH 10/69] add /notes/following endpoint --- .../backend/src/server/api/EndpointsModule.ts | 4 + packages/backend/src/server/api/endpoints.ts | 2 + .../server/api/endpoints/notes/following.ts | 88 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 packages/backend/src/server/api/endpoints/notes/following.ts diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 4a08410ceb..c90a23d7b5 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -290,6 +290,7 @@ import * as ep___notes_delete from './endpoints/notes/delete.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; +import * as ep___notes_following from './endpoints/notes/following.js'; import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js'; import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; @@ -686,6 +687,7 @@ const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___not const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default }; const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default }; const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; +const $notes_following: Provider = { provide: 'ep:notes/following', useClass: ep___notes_following.default }; const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default }; const $notes_bubbleTimeline: Provider = { provide: 'ep:notes/bubble-timeline', useClass: ep___notes_bubbleTimeline.default }; const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default }; @@ -1086,6 +1088,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_favorites_create, $notes_favorites_delete, $notes_featured, + $notes_following, $notes_globalTimeline, $notes_bubbleTimeline, $notes_hybridTimeline, @@ -1480,6 +1483,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_favorites_create, $notes_favorites_delete, $notes_featured, + $notes_following, $notes_globalTimeline, $notes_bubbleTimeline, $notes_hybridTimeline, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index e2fcd1a9d0..e93e57f907 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -296,6 +296,7 @@ import * as ep___notes_delete from './endpoints/notes/delete.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; +import * as ep___notes_following from './endpoints/notes/following.js'; import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js'; import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; @@ -690,6 +691,7 @@ const eps = [ ['notes/favorites/create', ep___notes_favorites_create], ['notes/favorites/delete', ep___notes_favorites_delete], ['notes/featured', ep___notes_featured], + ['notes/following', ep___notes_following], ['notes/global-timeline', ep___notes_globalTimeline], ['notes/bubble-timeline', ep___notes_bubbleTimeline], ['notes/hybrid-timeline', ep___notes_hybridTimeline], diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts new file mode 100644 index 0000000000..57ab5a6aeb --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { LatestNote, MiFollowing, MiBlocking, MiMuting } from '@/models/_.js'; +import type { NotesRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { QueryService } from '@/core/QueryService.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + kind: 'read:account', + allowGet: true, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Note', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.notesRepository + .createQueryBuilder('note') + + // Limit to latest notes + .innerJoin(LatestNote, 'latest', 'note.id = latest.note_id') + + // Avoid N+1 queries from the "pack" method + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel') + + // Respect blocks and mutes + .leftJoin(MiBlocking, 'b', 'note."userId" = b."blockerId"') + .leftJoin(MiMuting, 'm', 'note."userId" = m."muteeId"') + .where('b.id IS NULL AND m.id IS NULL') + + // Limit to followers + .innerJoin(MiFollowing, 'following', 'latest.user_id = following."followeeId"') + .andWhere('following."followerId" = :me', { me: me.id }) + + // Support pagination + .orderBy('note.id', 'DESC') + .take(ps.limit); + + // Query and return the next page + const notes = await this.queryService + .makePaginationQuery(query, ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .getMany(); + return await this.noteEntityService.packMany(notes, me); + }); + } +} From 3479c2c13a64e71900644b65acce33226d3f1213 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 30 Sep 2024 01:12:29 -0400 Subject: [PATCH 11/69] add mutuals-only option --- .../server/api/endpoints/notes/following.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts index 57ab5a6aeb..a317e8e8b1 100644 --- a/packages/backend/src/server/api/endpoints/notes/following.ts +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -32,6 +32,7 @@ export const meta = { export const paramDef = { type: 'object', properties: { + mutualsOnly: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -51,8 +52,9 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.notesRepository + let query = this.notesRepository .createQueryBuilder('note') + .setParameter('me', me.id) // Limit to latest notes .innerJoin(LatestNote, 'latest', 'note.id = latest.note_id') @@ -72,16 +74,22 @@ export default class extends Endpoint { // eslint- // Limit to followers .innerJoin(MiFollowing, 'following', 'latest.user_id = following."followeeId"') - .andWhere('following."followerId" = :me', { me: me.id }) + .andWhere('following."followerId" = :me'); - // Support pagination + // Limit to mutuals, if requested + if (ps.mutualsOnly) { + query = query + .innerJoin(MiFollowing, 'mutuals', 'latest.user_id = mutuals."followerId" AND mutuals."followeeId" = :me'); + } + + // Support pagination + query = this.queryService + .makePaginationQuery(query, ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .orderBy('note.id', 'DESC') .take(ps.limit); // Query and return the next page - const notes = await this.queryService - .makePaginationQuery(query, ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .getMany(); + const notes = await query.getMany(); return await this.noteEntityService.packMany(notes, me); }); } From 04baad2f9c6d06c855b17302b57a14a59d7c7d49 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 30 Sep 2024 12:03:21 -0400 Subject: [PATCH 12/69] fix icon mappings for ti-user-check and ti-user-heart --- packages/frontend/vite.replaceIcons.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/frontend/vite.replaceIcons.ts b/packages/frontend/vite.replaceIcons.ts index 92ac568ef3..1be76f07c7 100644 --- a/packages/frontend/vite.replaceIcons.ts +++ b/packages/frontend/vite.replaceIcons.ts @@ -344,10 +344,11 @@ export function pluginReplaceIcons() { 'ti ti-universe': 'ph-rocket-launch ph-bold ph-lg', 'ti ti-upload': 'ph-upload ph-bold ph-lg', 'ti ti-user': 'ph-user ph-bold ph-lg', - 'ti ti-user-check': 'ph-check ph-bold ph-lg', + 'ti ti-user-check': 'ph-user-check 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-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-plus': 'ph-user-plus ph-bold ph-lg', 'ti ti-user-search': 'ph-user-circle ph-bold ph-lg', From 502e642b4a6e52e84f13e04254d7934873a37fe3 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 30 Sep 2024 12:03:48 -0400 Subject: [PATCH 13/69] update misskey-js for notes/following endpoint --- packages/misskey-js/etc/misskey-js.api.md | 8 ++ .../misskey-js/src/autogen/apiClientJSDoc.ts | 13 ++- packages/misskey-js/src/autogen/endpoint.ts | 4 + packages/misskey-js/src/autogen/entities.ts | 2 + packages/misskey-js/src/autogen/types.ts | 82 ++++++++++++++++++- 5 files changed, 106 insertions(+), 3 deletions(-) diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 87fda5568e..d2a82f4901 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1642,6 +1642,8 @@ declare namespace entities { NotesFavoritesDeleteRequest, NotesFeaturedRequest, NotesFeaturedResponse, + NotesFollowingRequest, + NotesFollowingResponse, NotesGlobalTimelineRequest, NotesGlobalTimelineResponse, NotesBubbleTimelineRequest, @@ -2648,6 +2650,12 @@ type NotesFeaturedRequest = operations['notes___featured']['requestBody']['conte // @public (undocumented) type NotesFeaturedResponse = operations['notes___featured']['responses']['200']['content']['application/json']; +// @public (undocumented) +type NotesFollowingRequest = operations['notes___following']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesFollowingResponse = operations['notes___following']['responses']['200']['content']['application/json']; + // @public (undocumented) type NotesGlobalTimelineRequest = operations['notes___global-timeline']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index c13485621b..c2134a49e1 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -3174,6 +3174,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * @@ -4258,7 +4269,7 @@ declare module '../api.js' { ): Promise>; /** - * Get Sharkey GH Sponsors + * Get Sharkey Sponsors * * **Credential required**: *No* */ diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 628825d504..2652f40cf9 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -420,6 +420,8 @@ import type { NotesFavoritesDeleteRequest, NotesFeaturedRequest, NotesFeaturedResponse, + NotesFollowingRequest, + NotesFollowingResponse, NotesGlobalTimelineRequest, NotesGlobalTimelineResponse, NotesBubbleTimelineRequest, @@ -873,6 +875,7 @@ export type Endpoints = { 'notes/favorites/create': { req: NotesFavoritesCreateRequest; res: EmptyResponse }; 'notes/favorites/delete': { req: NotesFavoritesDeleteRequest; res: EmptyResponse }; 'notes/featured': { req: NotesFeaturedRequest; res: NotesFeaturedResponse }; + 'notes/following': { req: NotesFollowingRequest; res: NotesFollowingResponse }; 'notes/global-timeline': { req: NotesGlobalTimelineRequest; res: NotesGlobalTimelineResponse }; 'notes/bubble-timeline': { req: NotesBubbleTimelineRequest; res: NotesBubbleTimelineResponse }; 'notes/hybrid-timeline': { req: NotesHybridTimelineRequest; res: NotesHybridTimelineResponse }; @@ -1268,6 +1271,7 @@ export const endpointReqTypes: Record Date: Mon, 30 Sep 2024 12:04:09 -0400 Subject: [PATCH 14/69] add locale string "mutuals" --- locales/en-US.yml | 1 + locales/index.d.ts | 4 ++++ locales/ja-JP.yml | 1 + 3 files changed, 6 insertions(+) diff --git a/locales/en-US.yml b/locales/en-US.yml index 6d24bb5b41..f6861dcfb5 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -88,6 +88,7 @@ note: "Note" notes: "Notes" following: "Following" followers: "Followers" +mutuals: "Mutuals" followsYou: "Follows you" createList: "Create list" manageLists: "Manage lists" diff --git a/locales/index.d.ts b/locales/index.d.ts index 177a3c8160..0456a947d3 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -368,6 +368,10 @@ export interface Locale extends ILocale { * フォロワー */ "followers": string; + /** + * Mutuals + */ + "mutuals": string; /** * フォローされています */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b4d47a449c..fd1a89b076 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -88,6 +88,7 @@ note: "ノート" notes: "ノート" following: "フォロー" followers: "フォロワー" +mutuals: "Mutuals" followsYou: "フォローされています" createList: "リスト作成" manageLists: "リストの管理" From c5117552ca8919788f3aaa13a91661ede728d674 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 30 Sep 2024 12:04:44 -0400 Subject: [PATCH 15/69] expose prop to force-collapse MkNote / SkNote --- packages/frontend/src/components/MkNote.vue | 8 ++++++-- packages/frontend/src/components/SkNote.vue | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index e2f0a4e492..a6dff06fd5 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -241,8 +241,12 @@ const props = withDefaults(defineProps<{ pinned?: boolean; mock?: boolean; withHardMute?: boolean; + collapseRenote?: boolean; + collapseReplies?: boolean; }>(), { mock: false, + collapseRenote: undefined, + collapseReplies: undefined, }); provide('mock', props.mock); @@ -314,12 +318,12 @@ const translating = ref(false); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id)); const renoteCollapsed = ref( - defaultStore.state.collapseRenotes && isRenote && ( + (props.collapseRenote ?? defaultStore.state.collapseRenotes) && isRenote && ( ($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131 (appearNote.value.myReaction != null) ), ); -const inReplyToCollapsed = ref(defaultStore.state.collapseNotesRepliedTo); +const inReplyToCollapsed = ref(props.collapseReplies ?? defaultStore.state.collapseNotesRepliedTo); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null); const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index b02d902482..60e593b7b0 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -241,8 +241,12 @@ const props = withDefaults(defineProps<{ pinned?: boolean; mock?: boolean; withHardMute?: boolean; + collapseRenote?: boolean; + collapseReplies?: boolean; }>(), { mock: false, + collapseRenote: undefined, + collapseReplies: undefined, }); provide('mock', props.mock); @@ -314,12 +318,12 @@ const translating = ref(false); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id)); const renoteCollapsed = ref( - defaultStore.state.collapseRenotes && isRenote && ( + (props.collapseRenote ?? defaultStore.state.collapseRenotes) && isRenote && ( ($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131 (appearNote.value.myReaction != null) ) ); -const inReplyToCollapsed = ref(defaultStore.state.collapseNotesRepliedTo); +const inReplyToCollapsed = ref(props.collapseReplies ?? defaultStore.state.collapseNotesRepliedTo); const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null); const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); From 6a42874eaceb26c2f53ea5419f5839294cce3764 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 30 Sep 2024 12:06:00 -0400 Subject: [PATCH 16/69] add following-feed page --- packages/frontend/src/navbar.ts | 5 + .../frontend/src/pages/following-feed.vue | 108 ++++++++++++++++++ packages/frontend/src/pages/timeline.vue | 8 ++ packages/frontend/src/router/definition.ts | 4 + 4 files changed, 125 insertions(+) create mode 100644 packages/frontend/src/pages/following-feed.vue diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index ccd7034968..2d67a29a24 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -68,6 +68,11 @@ export const navbarItemDef = reactive({ lookup(); }, }, + following: { + title: i18n.ts.following, + icon: 'ti ti-user-check', + to: '/following-feed', + }, lists: { title: i18n.ts.lists, icon: 'ti ti-list', diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue new file mode 100644 index 0000000000..45c1f67d23 --- /dev/null +++ b/packages/frontend/src/pages/following-feed.vue @@ -0,0 +1,108 @@ + + + + + + + + + diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 20d8abccf6..7dc63a887a 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -55,9 +55,12 @@ import { MenuItem } from '@/types/menu.js'; import { miLocalStorage } from '@/local-storage.js'; import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; import type { BasicTimelineType } from '@/timelines.js'; +import { useRouter } from '@/router/supplier.js'; provide('shouldOmitHeaderTitle', true); +const router = useRouter(); + const tlComponent = shallowRef>(); const rootEl = shallowRef(); @@ -309,6 +312,11 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList icon: basicTimelineIconClass(tl), iconOnly: true, })), { + icon: 'ti ti-user-check', + title: i18n.ts.following, + iconOnly: true, + onClick: () => router.push('/following-feed'), +}, { icon: 'ti ti-list', title: i18n.ts.lists, iconOnly: true, diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index 14110d1f9b..6ebaa95cdc 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -227,6 +227,10 @@ const routes: RouteDef[] = [{ path: '/explore', component: page(() => import('@/pages/explore.vue')), hash: 'initialTab', +}, { + path: '/following-feed', + component: page(() => import('@/pages/following-feed.vue')), + hash: 'initialTab', }, { path: '/search', component: page(() => import('@/pages/search.vue')), From 168ff64b03ebaf048542956b2d0b0dd19166aa71 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 30 Sep 2024 12:06:44 -0400 Subject: [PATCH 17/69] fix copyright header --- packages/backend/src/server/api/endpoints/notes/following.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts index a317e8e8b1..189c4b7ce6 100644 --- a/packages/backend/src/server/api/endpoints/notes/following.ts +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors * SPDX-License-Identifier: AGPL-3.0-only */ From 8d3367dee39015addfb2d616afa998a57cdabcc5 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 30 Sep 2024 12:14:00 -0400 Subject: [PATCH 18/69] fix more copyright headers --- packages/backend/migration/1727659258948-add_latest_note.js | 5 +++++ packages/backend/src/models/LatestNote.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/packages/backend/migration/1727659258948-add_latest_note.js b/packages/backend/migration/1727659258948-add_latest_note.js index 739aaf0775..0ab2b3f0d1 100644 --- a/packages/backend/migration/1727659258948-add_latest_note.js +++ b/packages/backend/migration/1727659258948-add_latest_note.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class AddLatestNote1727659258948 { name = 'AddLatestNote1727659258948'; diff --git a/packages/backend/src/models/LatestNote.ts b/packages/backend/src/models/LatestNote.ts index 4a712c8e20..1163ff3bc0 100644 --- a/packages/backend/src/models/LatestNote.ts +++ b/packages/backend/src/models/LatestNote.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne } from 'typeorm'; import { MiUser } from '@/models/User.js'; import { MiNote } from '@/models/Note.js'; From acc0c7867fcc44650f9ada06286f35a103ceec1e Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 30 Sep 2024 13:29:15 -0400 Subject: [PATCH 19/69] exclude boosts from featured timeline --- packages/backend/src/core/NoteCreateService.ts | 4 ++++ packages/backend/src/core/NoteDeleteService.ts | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 17631eea89..0af65b81b1 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -63,6 +63,7 @@ import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { isQuote, isRenote } from '@/misc/is-renote.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -1136,6 +1137,9 @@ export class NoteCreateService implements OnApplicationShutdown { // Ignore DMs if (note.visibility === 'specified') return; + // Ignore pure renotes + if (isRenote(note) && !isQuote(note)) return; + // Make sure that this isn't an *older* post. // We can get older posts through replies, lookups, etc. const currentLatest = await this.latestNotesRepository.findOneBy({ userId: note.userId }); diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 898e164966..de753a3aa2 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -240,14 +240,25 @@ export class NoteDeleteService { // 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; - // Find the newest remaining note for the user + // Find the newest remaining note for the user. + // We exclude DMs and pure renotes. const nextLatest = await this.notesRepository - .createQueryBuilder() + .createQueryBuilder('note') .select() .where({ userId: note.userId, visibility: Not('specified'), }) + .andWhere(` + ( + note."renoteId" IS NULL + OR note.text IS NOT NULL + OR note.cw IS NOT NULL + OR note."replyId" IS NOT NULL + OR note."hasPoll" + OR note."fileIds" != '{}' + ) + `) .orderBy({ id: 'DESC' }) .getOne(); if (!nextLatest) return; From e0221488755c93bc2c29873fab7e85c0c26178c5 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 30 Sep 2024 13:56:10 -0400 Subject: [PATCH 20/69] add refresh button --- packages/frontend/src/pages/following-feed.vue | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index 45c1f67d23..8ac474bafc 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -46,6 +46,7 @@ import { infoImageUrl } from '@/instance.js'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import { defaultStore } from '@/store.js'; import { Tab } from '@/components/global/MkPageHeader.tabs.vue'; +import { PageHeaderItem } from '@/types/page-header.js'; // Load the correct note component const MkNote = defineAsyncComponent( @@ -80,7 +81,14 @@ const pagination: Paging<'notes/following'> = { })), }; -const headerActions = computed(() => []); +const headerActions: PageHeaderItem[] = [ + { + icon: 'ti ti-refresh', + text: i18n.ts.reload, + handler: () => reload(), + }, +]; + const headerTabs = computed(() => [ { key: followingTab, From 6cb4ee78791dc525386a689420d1769c1826fdfe Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 30 Sep 2024 14:47:35 -0400 Subject: [PATCH 21/69] fix page icon --- packages/frontend/src/pages/following-feed.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index 8ac474bafc..766d059939 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -104,7 +104,7 @@ const headerTabs = computed(() => [ definePageMetadata(() => ({ title: i18n.ts.following, - icon: 'ti ti-hash', + icon: 'ti ti-user-check', })); From ae3863bed001d92eebc9bb0a44a18f9dddf1aadd Mon Sep 17 00:00:00 2001 From: Hazel K Date: Tue, 1 Oct 2024 14:37:09 -0400 Subject: [PATCH 22/69] use compact rendering for following feed --- .../src/components/FollowingFeedEntry.vue | 111 ++++++++++++++++++ .../frontend/src/pages/following-feed.vue | 13 +- 2 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 packages/frontend/src/components/FollowingFeedEntry.vue diff --git a/packages/frontend/src/components/FollowingFeedEntry.vue b/packages/frontend/src/components/FollowingFeedEntry.vue new file mode 100644 index 0000000000..056b950653 --- /dev/null +++ b/packages/frontend/src/components/FollowingFeedEntry.vue @@ -0,0 +1,111 @@ + + + + + + + diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index 766d059939..dbfdcc3eab 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -36,7 +36,7 @@ export const mutualsTab = 'mutuals' as const; From d2cf675569dd17626c8bbe02f4a18ee2baf95814 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Tue, 1 Oct 2024 22:21:08 -0400 Subject: [PATCH 27/69] show user info over user feed --- .../frontend/src/pages/following-feed.vue | 61 ++++++++++++++++--- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index 67983451c5..e6a4ecc22b 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -27,7 +27,12 @@ SPDX-License-Identifier: AGPL-3.0-only - +
+ + +
+
{{ selectedUserError }}
+
@@ -42,7 +47,8 @@ export const mutualsTab = 'mutuals' as const; From 444b02ecdd24970c2350dd53254f57af7b72a68a Mon Sep 17 00:00:00 2001 From: Hazel K Date: Wed, 2 Oct 2024 20:26:22 -0400 Subject: [PATCH 49/69] reload user info when switching tabs --- packages/frontend/src/pages/following-feed.vue | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index 087f5e4f7e..9bc44242ca 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only