{ // eslint-
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId);
noteIds = noteIds.slice(0, ps.limit);
- if (noteIds.length === 0) {
- return [];
- }
+ let redisTimeline: MiNote[] = [];
- const query = this.notesRepository.createQueryBuilder('note')
- .where('note.id IN (:...noteIds)', { noteIds: noteIds })
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('note.channel', 'channel');
+ if (noteIds.length > 0) {
+ const query = this.notesRepository.createQueryBuilder('note')
+ .where('note.id IN (:...noteIds)', { noteIds: noteIds })
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('note.channel', 'channel');
- if (ps.withCats) {
- query.andWhere('(select "isCat" from "user" where id = note."userId")');
- }
+ redisTimeline = await query.getMany();
- let timeline = await query.getMany();
-
- timeline = timeline.filter(note => {
- if (note.userId === me.id) {
- return true;
- }
- if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
- if (isUserRelated(note, userIdsWhoMeMuting)) return false;
- if (note.renoteId) {
- if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
- if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
- if (ps.withRenotes === false) return false;
+ redisTimeline = redisTimeline.filter(note => {
+ if (note.userId === me.id) {
+ return true;
}
+ if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
+ if (isUserRelated(note, userIdsWhoMeMuting)) return false;
+ if (note.renoteId) {
+ if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
+ if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
+ if (ps.withRenotes === false) return false;
+ }
+ }
+
+ return true;
+ });
+
+ redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
+ }
+
+ if (redisTimeline.length > 0) {
+ this.activeUsersChart.read(me);
+ return await this.noteEntityService.packMany(redisTimeline, me);
+ } else { // fallback to db
+ //#region Construct query
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .innerJoin(this.userListMembershipsRepository.metadata.targetName, 'userListMemberships', 'userListMemberships.userId = note.userId')
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .andWhere('userListMemberships.userListId = :userListId', { userListId: list.id })
+ .andWhere('note.channelId IS NULL') // チャンネルノートではない
+ .andWhere(new Brackets(qb => {
+ qb
+ .where('note.replyId IS NULL') // 返信ではない
+ .orWhere(new Brackets(qb => {
+ qb // 返信だけど投稿者自身への返信
+ .where('note.replyId IS NOT NULL')
+ .andWhere('note.replyUserId = note.userId');
+ }))
+ .orWhere(new Brackets(qb => {
+ qb // 返信だけど自分宛ての返信
+ .where('note.replyId IS NOT NULL')
+ .andWhere('note.replyUserId = :meId', { meId: me.id });
+ }))
+ .orWhere(new Brackets(qb => {
+ qb // 返信だけどwithRepliesがtrueの場合
+ .where('note.replyId IS NOT NULL')
+ .andWhere('userListMemberships.withReplies = true');
+ }));
+ }));
+
+ this.queryService.generateVisibilityQuery(query, me);
+ this.queryService.generateMutedUserQuery(query, me);
+ this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
+
+ if (ps.includeMyRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.userId != :meId', { meId: me.id });
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+ }));
}
- return true;
- });
+ if (ps.includeRenotedMyNotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+ }));
+ }
- // TODO: フィルタした結果件数が足りなかった場合の対応
+ if (ps.includeLocalRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteUserHost IS NOT NULL');
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+ }));
+ }
- timeline.sort((a, b) => a.id > b.id ? -1 : 1);
+ if (ps.withRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere(new Brackets(qb => {
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ }));
+ }));
+ }
- this.activeUsersChart.read(me);
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
+ }
- return await this.noteEntityService.packMany(timeline, me);
+ if (ps.withCats) {
+ query.andWhere('(select "isCat" from "user" where id = note."userId")');
+ }
+ //#endregion
+
+ const timeline = await query.limit(ps.limit).getMany();
+
+ this.activeUsersChart.read(me);
+
+ return await this.noteEntityService.packMany(timeline, me);
+ }
});
}
}
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 92d95c69a0..ee183c0e1d 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -96,6 +96,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
{{ avatarDecoration.name }}
+
@@ -389,4 +390,10 @@ definePageMetadata({
font-weight: bold;
margin-bottom: 20px;
}
+
+.avatarDecorationLock {
+ position: absolute;
+ bottom: 12px;
+ right: 12px;
+}