diff --git a/CHANGELOG.md b/CHANGELOG.md index 711d9db62afd33aa4b38c2531ff10cdc38f0e072..8612220c869277e5878f9f020fe78ccd936f8dea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,13 @@ - ### Client -- +- 「ã«ã‚ƒã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚ï¼ï¼ï¼ï¼ï¼ï¼ï¼ï¼ï¼ï¼ï¼ï¼ã€ (`isCat`) 有効時ã«ã‚¢ãƒã‚¿ãƒ¼ã«è¡¨ç¤ºã•ã‚Œã‚‹çŒ«è€³ã«ã¤ã„ã¦æŒ™å‹•ã‚’変更 + - 「UIã«ã¼ã‹ã—効果を使用〠(`useBlurEffect`) ã§æ¬¡ã®æŒ™å‹•ãŒæœ‰åŠ¹ã«ãªã‚Šã¾ã™ + - 猫耳ã®ã‚¢ãƒã‚¿ãƒ¼å†…部部分をã¼ã‹ã—ã§ãƒžã‚¹ã‚¯è¡¨ç¤ºã—ã¦ã‚ˆã‚ŠçŒ«è€³ã£ã½ã見ãˆã‚‹ã‚ˆã†ã« + - 猫耳ã®è‰²ãŒã‚¢ãƒã‚¿ãƒ¼ä¸Šéƒ¨ã®ãƒ”クセルã‹ã‚‰æ±ºå®šã•ã‚Œã¾ã™ï¼ˆç„¡åŠ¹åŒ–時ã¯ã‚¢ãƒã‚¿ãƒ¼å…¨ä½“ã®å¹³å‡è‰²ï¼‰ + - 左耳ã¯ä¸Šã‹ã‚‰ãŠã‚ˆã 10%, å·¦ã‹ã‚‰ãŠã‚ˆã 20% ã®ä½ç½®ã§æ±ºå®šã—ã¾ã™ + - å³è€³ã¯ä¸Šã‹ã‚‰ãŠã‚ˆã 10%, å·¦ã‹ã‚‰ãŠã‚ˆã 80% ã®ä½ç½®ã§æ±ºå®šã—ã¾ã™ + - 「UIã®ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’減らã™ã€ (`reduceAnimation`) ã§çŒ«è€³ã‚’æ’«ã§ã‚‰ã‚Œãªããªã‚Šã¾ã™ ### Server - @@ -17,10 +23,13 @@ ### General - ãƒãƒ£ãƒ³ãƒãƒ«ã‚’ãŠæ°—ã«å…¥ã‚Šã«ç™»éŒ²ã§ãるよã†ã« - ãƒãƒ£ãƒ³ãƒãƒ«ã«ãƒŽãƒ¼ãƒˆã‚’ピン留ã‚ã§ãるよã†ã« +- アンテナã®ã‚¿ã‚¤ãƒ ラインå–得時ã®ãƒ‘フォーマンスをå‘上 +- ãƒãƒ£ãƒ³ãƒãƒ«ã®ã‚¿ã‚¤ãƒ ラインå–得時ã®ãƒ‘フォーマンスをå‘上 ### Client - 検索ページã§URLを入力ã—ãŸéš›ã«ç…§ä¼šã—ãŸã¨ãã¨åŒç‰ã®æŒ™å‹•ã‚’ã™ã‚‹ã‚ˆã†ã« - ノートã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’大ãã表示ã™ã‚‹ã‚ªãƒ—ã‚·ãƒ§ãƒ³ã‚’è¿½åŠ +- オブジェクトストレージã®è¨å®šç”»é¢ã‚’分ã‹ã‚Šã‚„ã™ã ### Server - diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a9c54810acf6f8cac98ef31669d0799580e36a2f..a4f1d802cc4ede58150a8bbb192a7b83560b4ac7 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "API接続ã«httpsを使用ã—ãªã„å ´åˆã¯ã‚ªãƒ•ã« objectStorageUseProxy: "Proxyを利用ã™ã‚‹" objectStorageUseProxyDesc: "API接続ã«proxyを利用ã—ãªã„å ´åˆã¯ã‚ªãƒ•ã«ã—ã¦ãã ã•ã„" objectStorageSetPublicRead: "アップãƒãƒ¼ãƒ‰æ™‚ã«'public-read'ã‚’è¨å®šã™ã‚‹" +s3ForcePathStyleDesc: "s3ForcePathStyleを有効ã«ã™ã‚‹ã¨ã€ãƒã‚±ãƒƒãƒˆåã‚’URLã®ãƒ›ã‚¹ãƒˆåã§ã¯ãªãパスã®ä¸€éƒ¨ã¨ã—ã¦æŒ‡å®šã™ã‚‹ã“ã¨ã‚’強制ã—ã¾ã™ã€‚セルフホストã•ã‚ŒãŸMinioãªã©ã®ä½¿ç”¨æ™‚ã«æœ‰åŠ¹ã«ã™ã‚‹å¿…è¦ãŒã‚ã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚" serverLogs: "サーãƒãƒ¼ãƒã‚°" deleteAll: "å…¨ã¦å‰Šé™¤" showFixedPostForm: "タイムライン上部ã«æŠ•ç¨¿ãƒ•ã‚©ãƒ¼ãƒ を表示ã™ã‚‹" diff --git a/packages/backend/migration/1680491187535-cleanup.js b/packages/backend/migration/1680491187535-cleanup.js new file mode 100644 index 0000000000000000000000000000000000000000..1e609ca060dc358975bd5253303fe304f75a68ff --- /dev/null +++ b/packages/backend/migration/1680491187535-cleanup.js @@ -0,0 +1,10 @@ +export class cleanup1680491187535 { + name = 'cleanup1680491187535' + + async up(queryRunner) { + await queryRunner.query(`DROP TABLE "antenna_note" `); + } + + async down(queryRunner) { + } +} diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index aaa26a832173ef497d829f533e2078e95b97e88b..4bd3f39af20d32dae966c7692461a80ea4094ca6 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -12,7 +12,7 @@ import { PushNotificationService } from '@/core/PushNotificationService.js'; import * as Acct from '@/misc/acct.js'; import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; -import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js'; +import type { MutingsRepository, NotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { StreamMessages } from '@/server/api/stream/types.js'; @@ -24,6 +24,9 @@ export class AntennaService implements OnApplicationShutdown { private antennas: Antenna[]; constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.redisSubscriber) private redisSubscriber: Redis.Redis, @@ -33,9 +36,6 @@ export class AntennaService implements OnApplicationShutdown { @Inject(DI.notesRepository) private notesRepository: NotesRepository, - @Inject(DI.antennaNotesRepository) - private antennaNotesRepository: AntennaNotesRepository, - @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, @@ -92,54 +92,13 @@ export class AntennaService implements OnApplicationShutdown { @bindThis public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> { - // 通知ã—ãªã„è¨å®šã«ãªã£ã¦ã„ã‚‹ã‹ã€è‡ªåˆ†è‡ªèº«ã®æŠ•ç¨¿ãªã‚‰æ—¢èªã«ã™ã‚‹ - const read = !antenna.notify || (antenna.userId === noteUser.id); - - this.antennaNotesRepository.insert({ - id: this.idService.genId(), - antennaId: antenna.id, - noteId: note.id, - read: read, - }); - + this.redisClient.xadd( + `antennaTimeline:${antenna.id}`, + 'MAXLEN', '~', '200', + `${this.idService.parse(note.id).date.getTime()}-*`, + 'note', note.id); + this.globalEventService.publishAntennaStream(antenna.id, 'note', note); - - if (!read) { - const mutings = await this.mutingsRepository.find({ - where: { - muterId: antenna.userId, - }, - select: ['muteeId'], - }); - - // Copy - const _note: Note = { - ...note, - }; - - if (note.replyId != null) { - _note.reply = await this.notesRepository.findOneByOrFail({ id: note.replyId }); - } - if (note.renoteId != null) { - _note.renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId }); - } - - if (isUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) { - return; - } - - // 2秒経ã£ã¦ã‚‚æ—¢èªã«ãªã‚‰ãªã‹ã£ãŸã‚‰é€šçŸ¥ - setTimeout(async () => { - const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false }); - if (unread) { - this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna); - this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', { - antenna: { id: antenna.id, name: antenna.name }, - note: await this.noteEntityService.pack(note), - }); - } - }, 2000); - } } // NOTE: フォãƒãƒ¼ã—ã¦ã„るユーザーã®ãƒŽãƒ¼ãƒˆã€ãƒªã‚¹ãƒˆã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒŽãƒ¼ãƒˆã€ã‚°ãƒ«ãƒ¼ãƒ—ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒŽãƒ¼ãƒˆæŒ‡å®šã¯ãƒ‘フォーマンス上ã®ç†ç”±ã§ç„¡åŠ¹ã«ãªã£ã¦ã„ã‚‹ diff --git a/packages/backend/src/core/IdService.ts b/packages/backend/src/core/IdService.ts index 31c0819e506ff790dee88d2486869e7e22836d65..94084ad84fe889613c146f01a1407649adb759a7 100644 --- a/packages/backend/src/core/IdService.ts +++ b/packages/backend/src/core/IdService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { ulid } from 'ulid'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import { genAid } from '@/misc/id/aid.js'; +import { genAid, parseAid } from '@/misc/id/aid.js'; import { genMeid } from '@/misc/id/meid.js'; import { genMeidg } from '@/misc/id/meidg.js'; import { genObjectId } from '@/misc/id/object-id.js'; @@ -32,4 +32,17 @@ export class IdService { default: throw new Error('unrecognized id generation method'); } } + + @bindThis + public parse(id: string): { date: Date; } { + switch (this.method) { + case 'aid': return parseAid(id); + // TODO + //case 'meid': + //case 'meidg': + //case 'ulid': + //case 'objectid': + default: throw new Error('unrecognized id generation method'); + } + } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 7d080537610c0bef6b88c0f8104e6bff9c6b0dfd..7af7099432a314989a7e009c9887707b3ab67ae6 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -1,6 +1,7 @@ import { setImmediate } from 'node:timers/promises'; import * as mfm from 'mfm-js'; import { In, DataSource } from 'typeorm'; +import Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { extractMentions } from '@/misc/extract-mentions.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; @@ -150,6 +151,9 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.db) private db: DataSource, + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -321,6 +325,14 @@ export class NoteCreateService implements OnApplicationShutdown { const note = await this.insertNote(user, data, tags, emojis, mentionedUsers); + if (data.channel) { + this.redisClient.xadd( + `channelTimeline:${data.channel.id}`, + 'MAXLEN', '~', '1000', + `${this.idService.parse(note.id).date.getTime()}-*`, + 'note', note.id); + } + setImmediate('post created', { signal: this.#shutdownController.signal }).then( () => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!), () => { /* aborted, ignore this */ }, diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts index 22d72815ece07eff111149034384f8c5dd07f246..1bf0eb918f10e3001b308835bf2ebd47303f2305 100644 --- a/packages/backend/src/core/NoteReadService.ts +++ b/packages/backend/src/core/NoteReadService.ts @@ -8,7 +8,7 @@ import type { Packed } from '@/misc/json-schema.js'; import type { Note } from '@/models/entities/Note.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js'; +import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { NotificationService } from './NotificationService.js'; @@ -38,9 +38,6 @@ export class NoteReadService implements OnApplicationShutdown { @Inject(DI.channelFollowingsRepository) private channelFollowingsRepository: ChannelFollowingsRepository, - @Inject(DI.antennaNotesRepository) - private antennaNotesRepository: AntennaNotesRepository, - private userEntityService: UserEntityService, private idService: IdService, private globalEventService: GlobalEventService, @@ -121,7 +118,6 @@ export class NoteReadService implements OnApplicationShutdown { const readMentions: (Note | Packed<'Note'>)[] = []; const readSpecifiedNotes: (Note | Packed<'Note'>)[] = []; const readChannelNotes: (Note | Packed<'Note'>)[] = []; - const readAntennaNotes: (Note | Packed<'Note'>)[] = []; for (const note of notes) { if (note.mentions && note.mentions.includes(userId)) { @@ -133,14 +129,6 @@ export class NoteReadService implements OnApplicationShutdown { if (note.channelId && followingChannels.has(note.channelId)) { readChannelNotes.push(note); } - - if (note.user != null) { // ãŸã¶ã‚“nullã«ãªã‚‹ã“ã¨ã¯ç„¡ã„ã¯ãšã ã‘ã©ä¸€å¿œ - for (const antenna of myAntennas) { - if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) { - readAntennaNotes.push(note); - } - } - } } if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) { @@ -186,35 +174,6 @@ export class NoteReadService implements OnApplicationShutdown { noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]), }); } - - if (readAntennaNotes.length > 0) { - await this.antennaNotesRepository.update({ - antennaId: In(myAntennas.map(a => a.id)), - noteId: In(readAntennaNotes.map(n => n.id)), - }, { - read: true, - }); - - // TODO: ã¾ã¨ã‚ã¦ã‚¯ã‚¨ãƒªã—ãŸã„ - for (const antenna of myAntennas) { - const count = await this.antennaNotesRepository.countBy({ - antennaId: antenna.id, - read: false, - }); - - if (count === 0) { - this.globalEventService.publishMainStream(userId, 'readAntenna', antenna); - this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id }); - } - } - - this.userEntityService.getHasUnreadAntenna(userId).then(unread => { - if (!unread) { - this.globalEventService.publishMainStream(userId, 'readAllAntennas'); - this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined); - } - }); - } } onApplicationShutdown(signal?: string | undefined): void { diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts index e02daefd6458f15367a5c70923efb18ac0a04698..328511f5df9e63038b6e781414d8a72b9656de00 100644 --- a/packages/backend/src/core/entities/AntennaEntityService.ts +++ b/packages/backend/src/core/entities/AntennaEntityService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { AntennaNotesRepository, AntennasRepository } from '@/models/index.js'; +import type { AntennasRepository } from '@/models/index.js'; import type { Packed } from '@/misc/json-schema.js'; import type { Antenna } from '@/models/entities/Antenna.js'; import { bindThis } from '@/decorators.js'; @@ -10,9 +10,6 @@ export class AntennaEntityService { constructor( @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, - - @Inject(DI.antennaNotesRepository) - private antennaNotesRepository: AntennaNotesRepository, ) { } @@ -22,8 +19,6 @@ export class AntennaEntityService { ): Promise<Packed<'Antenna'>> { const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src }); - const hasUnreadNote = (await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false })) != null; - return { id: antenna.id, createdAt: antenna.createdAt.toISOString(), @@ -38,7 +33,7 @@ export class AntennaEntityService { withReplies: antenna.withReplies, withFile: antenna.withFile, isActive: antenna.isActive, - hasUnreadNote, + hasUnreadNote: false, // TODO }; } } diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index b693883e06fe138b73452d1d1755fd6dc9e841b5..61fd6f2f667ee849e5e91d1888a96fe0bf88556f 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -12,7 +12,7 @@ import { KVCache } from '@/misc/cache.js'; import type { Instance } from '@/models/entities/Instance.js'; import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; -import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js'; +import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -108,9 +108,6 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.announcementsRepository) private announcementsRepository: AnnouncementsRepository, - @Inject(DI.antennaNotesRepository) - private antennaNotesRepository: AntennaNotesRepository, - @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, @@ -223,6 +220,7 @@ export class UserEntityService implements OnModuleInit { @bindThis public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> { + /* const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId); const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({ @@ -231,6 +229,8 @@ export class UserEntityService implements OnModuleInit { }) : null; return unread != null; + */ + return false; // TODO } @bindThis diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 4f475a03adf66a65efc2d48db5616ba9e4c0b024..f2ab6cb864cd3de9409e083edf5b02519f17ee0b 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -54,7 +54,6 @@ export const DI = { clipNotesRepository: Symbol('clipNotesRepository'), clipFavoritesRepository: Symbol('clipFavoritesRepository'), antennasRepository: Symbol('antennasRepository'), - antennaNotesRepository: Symbol('antennaNotesRepository'), promoNotesRepository: Symbol('promoNotesRepository'), promoReadsRepository: Symbol('promoReadsRepository'), relaysRepository: Symbol('relaysRepository'), diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts index 19c8546f95c645aced87a04f16485c085d8b519c..93a9929aa74cf04bc585c296366bf26c76f6536f 100644 --- a/packages/backend/src/misc/id/aid.ts +++ b/packages/backend/src/misc/id/aid.ts @@ -23,3 +23,8 @@ export function genAid(date: Date): string { counter++; return getTime(t) + getNoise(); } + +export function parseAid(id: string): { date: Date; } { + const time = parseInt(id.slice(0, 8), 36) + TIME2000; + return { date: new Date(time) }; +} diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index da7faf9ffbb0e7ba1df79d1d791da0a67fd88e58..b74ee3689cd272167439a283afbff10f29c9a5ee 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js'; +import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -298,12 +298,6 @@ const $antennasRepository: Provider = { inject: [DI.db], }; -const $antennaNotesRepository: Provider = { - provide: DI.antennaNotesRepository, - useFactory: (db: DataSource) => db.getRepository(AntennaNote), - inject: [DI.db], -}; - const $promoNotesRepository: Provider = { provide: DI.promoNotesRepository, useFactory: (db: DataSource) => db.getRepository(PromoNote), @@ -453,7 +447,6 @@ const $roleAssignmentsRepository: Provider = { $clipNotesRepository, $clipFavoritesRepository, $antennasRepository, - $antennaNotesRepository, $promoNotesRepository, $promoReadsRepository, $relaysRepository, @@ -521,7 +514,6 @@ const $roleAssignmentsRepository: Provider = { $clipNotesRepository, $clipFavoritesRepository, $antennasRepository, - $antennaNotesRepository, $promoNotesRepository, $promoReadsRepository, $relaysRepository, diff --git a/packages/backend/src/models/entities/AntennaNote.ts b/packages/backend/src/models/entities/AntennaNote.ts deleted file mode 100644 index 5524a89367d3c6f3294bf4c691e67bc234ceff19..0000000000000000000000000000000000000000 --- a/packages/backend/src/models/entities/AntennaNote.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './Note.js'; -import { Antenna } from './Antenna.js'; - -@Entity() -@Index(['noteId', 'antennaId'], { unique: true }) -export class AntennaNote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: 'The note ID.', - }) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Index() - @Column({ - ...id(), - comment: 'The antenna ID.', - }) - public antennaId: Antenna['id']; - - @ManyToOne(type => Antenna, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public antenna: Antenna | null; - - @Index() - @Column('boolean', { - default: false, - }) - public read: boolean; -} diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index 79bd014cea88f9773124a5fb32e333ecb836b482..c4c9717ed54f349e461b889a076d3b070562fea2 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -4,7 +4,6 @@ import { Ad } from '@/models/entities/Ad.js'; import { Announcement } from '@/models/entities/Announcement.js'; import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js'; import { Antenna } from '@/models/entities/Antenna.js'; -import { AntennaNote } from '@/models/entities/AntennaNote.js'; import { App } from '@/models/entities/App.js'; import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js'; import { AuthSession } from '@/models/entities/AuthSession.js'; @@ -73,7 +72,6 @@ export { Announcement, AnnouncementRead, Antenna, - AntennaNote, App, AttestationChallenge, AuthSession, @@ -141,7 +139,6 @@ export type AdsRepository = Repository<Ad>; export type AnnouncementsRepository = Repository<Announcement>; export type AnnouncementReadsRepository = Repository<AnnouncementRead>; export type AntennasRepository = Repository<Antenna>; -export type AntennaNotesRepository = Repository<AntennaNote>; export type AppsRepository = Repository<App>; export type AttestationChallengesRepository = Repository<AttestationChallenge>; export type AuthSessionsRepository = Repository<AuthSession>; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index cbe3814a24f6c86289265b2e475a18ab55ae72fd..024aa114fc5c10806fe6ae7e7a0aa88cd11fde95 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -12,7 +12,6 @@ import { Ad } from '@/models/entities/Ad.js'; import { Announcement } from '@/models/entities/Announcement.js'; import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js'; import { Antenna } from '@/models/entities/Antenna.js'; -import { AntennaNote } from '@/models/entities/AntennaNote.js'; import { App } from '@/models/entities/App.js'; import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js'; import { AuthSession } from '@/models/entities/AuthSession.js'; @@ -168,7 +167,6 @@ export const entities = [ ClipNote, ClipFavorite, Antenna, - AntennaNote, PromoNote, PromoRead, Relay, diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index 9534454fd75de93c63f2c5fa5024133de7aaa3cf..3feb86f86f930cf4913701961ac5741261c411a0 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, LessThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AntennaNotesRepository, AntennasRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js'; +import type { AntennasRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; @@ -29,9 +29,6 @@ export class CleanProcessorService { @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, - @Inject(DI.antennaNotesRepository) - private antennaNotesRepository: AntennaNotesRepository, - @Inject(DI.roleAssignmentsRepository) private roleAssignmentsRepository: RoleAssignmentsRepository, diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 039ba1115aeb7c5284d16cd9a66621019c8b9e9a..364f9d9c05b298ea55c3cb44677c970aa2cb3146 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -1,10 +1,12 @@ import { Inject, Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NotesRepository, AntennaNotesRepository, AntennasRepository } from '@/models/index.js'; +import type { NotesRepository, AntennasRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { IdService } from '@/core/IdService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -50,15 +52,16 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, - @Inject(DI.antennaNotesRepository) - private antennaNotesRepository: AntennaNotesRepository, - + private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, private noteReadService: NoteReadService, @@ -73,9 +76,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { throw new ApiError(meta.errors.noSuchAntenna); } - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), - ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .innerJoin(this.antennaNotesRepository.metadata.targetName, 'antennaNote', 'antennaNote.noteId = note.id') + const noteIdsRes = await this.redisClient.xrevrange( + `antennaTimeline:${antenna.id}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', + '-', + 'COUNT', ps.limit + 1); // untilIdã«æŒ‡å®šã—ãŸã‚‚ã®ã‚‚å«ã¾ã‚Œã‚‹ãŸã‚+1 + + if (noteIdsRes.length === 0) { + return []; + } + + const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); + + if (noteIds.length === 0) { + return []; + } + + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') @@ -86,16 +104,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') - .andWhere('antennaNote.antennaId = :antennaId', { antennaId: antenna.id }); + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); this.queryService.generateVisibilityQuery(query, me); this.queryService.generateMutedUserQuery(query, me); this.queryService.generateBlockedUserQuery(query, me); - const notes = await query - .take(ps.limit) - .getMany(); + const notes = await query.getMany(); + notes.sort((a, b) => a.id > b.id ? -1 : 1); if (notes.length > 0) { this.noteReadService.read(me.id, notes); diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index cdaa400137245590148d409c69905be301daf49d..eef343d139c6155cb7b572fe16cdc11e3b6a75d0 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -1,10 +1,12 @@ import { Inject, Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { ChannelsRepository, NotesRepository } from '@/models/index.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; +import { IdService } from '@/core/IdService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -48,12 +50,16 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, + private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, private activeUsersChart: ActiveUsersChart, @@ -67,9 +73,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { throw new ApiError(meta.errors.noSuchChannel); } + const noteIdsRes = await this.redisClient.xrevrange( + `channelTimeline:${channel.id}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', + '-', + 'COUNT', ps.limit + 1); // untilIdã«æŒ‡å®šã—ãŸã‚‚ã®ã‚‚å«ã¾ã‚Œã‚‹ãŸã‚+1 + + if (noteIdsRes.length === 0) { + return []; + } + + const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); + + if (noteIds.length === 0) { + return []; + } + //#region Construct query - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.channelId = :channelId', { channelId: channel.id }) + const query = this.notesRepository.createQueryBuilder('note') + .where('note.id IN (:...noteIds)', { noteIds: noteIds }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') @@ -90,7 +112,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } //#endregion - const timeline = await query.take(ps.limit).getMany(); + const timeline = await query.getMany(); + timeline.sort((a, b) => a.id > b.id ? -1 : 1); if (me) this.activeUsersChart.read(me); diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 867d432572e4aa1e713814659f7a399d809c66e6..cd8af560e43cead1e4a1e6df14ab24efdd64d1e9 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -163,21 +163,22 @@ async function init(): Promise<void> { const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; await os.api(props.pagination.endpoint, { ...params, - limit: props.pagination.noPaging ? (props.pagination.limit || 10) : (props.pagination.limit || 10) + 1, + limit: props.pagination.limit ?? 10, }).then(res => { for (let i = 0; i < res.length; i++) { const item = res[i]; if (i === 3) item._shouldInsertAd_ = true; } - if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) { - res.pop(); - if (props.pagination.reversed) moreFetching.value = true; + + if (res.length === 0 || props.pagination.noPaging) { items.value = res; - more.value = true; + more.value = false; } else { + if (props.pagination.reversed) moreFetching.value = true; items.value = res; - more.value = false; + more.value = true; } + offset.value = res.length; error.value = false; fetching.value = false; @@ -198,7 +199,7 @@ const fetchMore = async (): Promise<void> => { const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; await os.api(props.pagination.endpoint, { ...params, - limit: SECOND_FETCH_LIMIT + 1, + limit: SECOND_FETCH_LIMIT, ...(props.pagination.offsetMode ? { offset: offset.value, } : { @@ -227,28 +228,26 @@ const fetchMore = async (): Promise<void> => { }); }; - if (res.length > SECOND_FETCH_LIMIT) { - res.pop(); - + if (res.length === 0) { if (props.pagination.reversed) { reverseConcat(res).then(() => { - more.value = true; + more.value = false; moreFetching.value = false; }); } else { items.value = items.value.concat(res); - more.value = true; + more.value = false; moreFetching.value = false; } } else { if (props.pagination.reversed) { reverseConcat(res).then(() => { - more.value = false; + more.value = true; moreFetching.value = false; }); } else { items.value = items.value.concat(res); - more.value = false; + more.value = true; moreFetching.value = false; } } @@ -264,20 +263,19 @@ const fetchMoreAhead = async (): Promise<void> => { const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; await os.api(props.pagination.endpoint, { ...params, - limit: SECOND_FETCH_LIMIT + 1, + limit: SECOND_FETCH_LIMIT, ...(props.pagination.offsetMode ? { offset: offset.value, } : { sinceId: items.value[items.value.length - 1].id, }), }).then(res => { - if (res.length > SECOND_FETCH_LIMIT) { - res.pop(); + if (res.length === 0) { items.value = items.value.concat(res); - more.value = true; + more.value = false; } else { items.value = items.value.concat(res); - more.value = false; + more.value = true; } offset.value += res.length; moreFetching.value = false; diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index c45e9a172d425b671123ed01351ed5c9c5d82f01..0cc30a887f007ae4c664e227598e1822a12f850d 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -1,5 +1,5 @@ <template> -<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick"> +<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick"> <img :class="$style.inner" :src="url" decoding="async"/> <MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/> <div v-if="user.isCat" :class="[$style.ears, { [$style.mask]: useBlurEffect }]"> @@ -27,6 +27,7 @@ import { acct, userPage } from '@/filters/user'; import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue'; import { defaultStore } from '@/store'; +const animation = $ref(defaultStore.state.animation); const squareAvatars = $ref(defaultStore.state.squareAvatars); const useBlurEffect = $ref(defaultStore.state.useBlurEffect); @@ -86,6 +87,18 @@ watch(() => props.user.avatarBlurhash, () => { to { transform: rotate(-37.6deg) skew(-30deg); } } +@keyframes eartightleft { + from { transform: rotate(37.6deg) skew(30deg); } + 50% { transform: rotate(37.4deg) skew(30deg); } + to { transform: rotate(37.6deg) skew(30deg); } +} + +@keyframes eartightright { + from { transform: rotate(-37.6deg) skew(-30deg); } + 50% { transform: rotate(-37.4deg) skew(-30deg); } + to { transform: rotate(-37.6deg) skew(-30deg); } +} + .root { position: relative; display: inline-block; @@ -145,6 +158,14 @@ watch(() => props.user.avatarBlurhash, () => { mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') exclude center / 50% 50%, linear-gradient(#fff, #fff); // polyfill of `image(#fff)` + + > .earLeft { + animation: eartightleft 6s infinite; + } + + > .earRight { + animation: eartightright 6s infinite; + } } > .earLeft, @@ -226,7 +247,7 @@ watch(() => props.user.avatarBlurhash, () => { } } - &:hover { + &.animation:hover { > .ears { > .earLeft { animation: earwiggleleft 1s infinite; diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index 0d229a937072df1d872e0c00ae4dcb6cfdc7b150..710edd797ae5e8615338e4af78df85ccc83ee393 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -8,7 +8,9 @@ <template v-if="metadata"> <div v-if="!hideTitle" :class="$style.titleContainer" @click="top"> - <MkAvatar v-if="metadata.avatar" :class="$style.titleAvatar" :user="metadata.avatar" indicator/> + <div v-if="metadata.avatar" :class="$style.titleAvatarContainer"> + <MkAvatar :class="$style.titleAvatar" :user="metadata.avatar" indicator/> + </div> <i v-else-if="metadata.icon" :class="[$style.titleIcon, metadata.icon]"></i> <div :class="$style.title"> @@ -249,13 +251,19 @@ onUnmounted(() => { margin-left: 24px; } -.titleAvatar { +.titleAvatarContainer { $size: 32px; - display: inline-block; + contain: strict; + overflow: clip; width: $size; height: $size; - vertical-align: bottom; - margin: 0 8px; + padding: 8px; + flex-shrink: 0; +} + +.titleAvatar { + width: 100%; + height: 100%; pointer-events: none; } diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue index cbe38b2d8141d9722ff7166d3b5adc4582d1bdb1..704b27c1745304500a401e2d35471f07c93c1708 100644 --- a/packages/frontend/src/pages/admin/object-storage.vue +++ b/packages/frontend/src/pages/admin/object-storage.vue @@ -7,7 +7,7 @@ <MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch> <template v-if="useObjectStorage"> - <MkInput v-model="objectStorageBaseUrl"> + <MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'"> <template #label>{{ i18n.ts.objectStorageBaseUrl }}</template> <template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template> </MkInput> @@ -22,8 +22,9 @@ <template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template> </MkInput> - <MkInput v-model="objectStorageEndpoint"> + <MkInput v-model="objectStorageEndpoint" :placeholder="'example.com'"> <template #label>{{ i18n.ts.objectStorageEndpoint }}</template> + <template #prefix>https://</template> <template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template> </MkInput> @@ -60,6 +61,7 @@ <MkSwitch v-model="objectStorageS3ForcePathStyle"> <template #label>s3ForcePathStyle</template> + <template #caption>{{ i18n.ts.s3ForcePathStyleDesc }}</template> </MkSwitch> </template> </div>