summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAcid Chicken (硫酸鶏) <root@acid-chicken.com>2023-04-03 13:44:32 +0900
committerAcid Chicken (硫酸鶏) <root@acid-chicken.com>2023-04-03 13:44:32 +0900
commit8d90e88e16c6b9159ac3273e3882ced5251e69d0 (patch)
treedd91ef53608285fe9ab07c70a585dcc188031581
parentci(#10336): use `pull_request` instead of `pull_request_target` for now (diff)
parentMerge pull request #10420 from misskey-dev/acid-chicken-patch-1 (diff)
downloadmisskey-8d90e88e16c6b9159ac3273e3882ced5251e69d0.tar.gz
misskey-8d90e88e16c6b9159ac3273e3882ced5251e69d0.tar.bz2
misskey-8d90e88e16c6b9159ac3273e3882ced5251e69d0.zip
Merge branch 'develop' of https://github.com/misskey-dev/misskey into storybook
-rw-r--r--CHANGELOG.md11
-rw-r--r--locales/ja-JP.yml1
-rw-r--r--packages/backend/migration/1680491187535-cleanup.js10
-rw-r--r--packages/backend/src/core/AntennaService.ts61
-rw-r--r--packages/backend/src/core/IdService.ts15
-rw-r--r--packages/backend/src/core/NoteCreateService.ts12
-rw-r--r--packages/backend/src/core/NoteReadService.ts43
-rw-r--r--packages/backend/src/core/entities/AntennaEntityService.ts9
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts8
-rw-r--r--packages/backend/src/di-symbols.ts1
-rw-r--r--packages/backend/src/misc/id/aid.ts5
-rw-r--r--packages/backend/src/models/RepositoryModule.ts10
-rw-r--r--packages/backend/src/models/entities/AntennaNote.ts43
-rw-r--r--packages/backend/src/models/index.ts3
-rw-r--r--packages/backend/src/postgres.ts2
-rw-r--r--packages/backend/src/queue/processors/CleanProcessorService.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/notes.ts40
-rw-r--r--packages/backend/src/server/api/endpoints/channels/timeline.ts29
-rw-r--r--packages/frontend/src/components/MkPagination.vue36
-rw-r--r--packages/frontend/src/components/global/MkAvatar.vue25
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.vue18
-rw-r--r--packages/frontend/src/pages/admin/object-storage.vue6
22 files changed, 182 insertions, 211 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 711d9db62a..8612220c86 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 a9c54810ac..a4f1d802cc 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 0000000000..1e609ca060
--- /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 aaa26a8321..4bd3f39af2 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 31c0819e50..94084ad84f 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 7d08053761..7af7099432 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 22d72815ec..1bf0eb918f 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 e02daefd64..328511f5df 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 b693883e06..61fd6f2f66 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 4f475a03ad..f2ab6cb864 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 19c8546f95..93a9929aa7 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 da7faf9ffb..b74ee3689c 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 5524a89367..0000000000
--- 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 79bd014cea..c4c9717ed5 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 cbe3814a24..024aa114fc 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 9534454fd7..3feb86f86f 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 039ba1115a..364f9d9c05 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 cdaa400137..eef343d139 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 867d432572..cd8af560e4 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 c45e9a172d..0cc30a887f 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 0d229a9370..710edd797a 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 cbe38b2d81..704b27c174 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>