mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-30 12:23:09 +02:00
Merge branch 'develop' of https://github.com/misskey-dev/misskey into storybook
This commit is contained in:
commit
8d90e88e16
22 changed files with 190 additions and 219 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -5,7 +5,13 @@
|
||||||
-
|
-
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
-
|
- 「にゃああああああああああああああ!!!!!!!!!!!!」 (`isCat`) 有効時にアバターに表示される猫耳について挙動を変更
|
||||||
|
- 「UIにぼかし効果を使用」 (`useBlurEffect`) で次の挙動が有効になります
|
||||||
|
- 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
|
||||||
|
- 猫耳の色がアバター上部のピクセルから決定されます(無効化時はアバター全体の平均色)
|
||||||
|
- 左耳は上からおよそ 10%, 左からおよそ 20% の位置で決定します
|
||||||
|
- 右耳は上からおよそ 10%, 左からおよそ 80% の位置で決定します
|
||||||
|
- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
-
|
-
|
||||||
|
@ -17,10 +23,13 @@
|
||||||
### General
|
### General
|
||||||
- チャンネルをお気に入りに登録できるように
|
- チャンネルをお気に入りに登録できるように
|
||||||
- チャンネルにノートをピン留めできるように
|
- チャンネルにノートをピン留めできるように
|
||||||
|
- アンテナのタイムライン取得時のパフォーマンスを向上
|
||||||
|
- チャンネルのタイムライン取得時のパフォーマンスを向上
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- 検索ページでURLを入力した際に照会したときと同等の挙動をするように
|
- 検索ページでURLを入力した際に照会したときと同等の挙動をするように
|
||||||
- ノートのリアクションを大きく表示するオプションを追加
|
- ノートのリアクションを大きく表示するオプションを追加
|
||||||
|
- オブジェクトストレージの設定画面を分かりやすく
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
-
|
-
|
||||||
|
|
|
@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフに
|
||||||
objectStorageUseProxy: "Proxyを利用する"
|
objectStorageUseProxy: "Proxyを利用する"
|
||||||
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
|
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
|
||||||
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
|
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
|
||||||
|
s3ForcePathStyleDesc: "s3ForcePathStyleを有効にすると、バケット名をURLのホスト名ではなくパスの一部として指定することを強制します。セルフホストされたMinioなどの使用時に有効にする必要がある場合があります。"
|
||||||
serverLogs: "サーバーログ"
|
serverLogs: "サーバーログ"
|
||||||
deleteAll: "全て削除"
|
deleteAll: "全て削除"
|
||||||
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
|
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
|
||||||
|
|
10
packages/backend/migration/1680491187535-cleanup.js
Normal file
10
packages/backend/migration/1680491187535-cleanup.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export class cleanup1680491187535 {
|
||||||
|
name = 'cleanup1680491187535'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP TABLE "antenna_note" `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import { PushNotificationService } from '@/core/PushNotificationService.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { DI } from '@/di-symbols.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 { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||||
|
@ -24,6 +24,9 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
private antennas: Antenna[];
|
private antennas: Antenna[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.redisSubscriber)
|
@Inject(DI.redisSubscriber)
|
||||||
private redisSubscriber: Redis.Redis,
|
private redisSubscriber: Redis.Redis,
|
||||||
|
|
||||||
|
@ -33,9 +36,6 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@Inject(DI.antennaNotesRepository)
|
|
||||||
private antennaNotesRepository: AntennaNotesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.antennasRepository)
|
@Inject(DI.antennasRepository)
|
||||||
private antennasRepository: AntennasRepository,
|
private antennasRepository: AntennasRepository,
|
||||||
|
|
||||||
|
@ -92,54 +92,13 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
|
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
|
||||||
// 通知しない設定になっているか、自分自身の投稿なら既読にする
|
this.redisClient.xadd(
|
||||||
const read = !antenna.notify || (antenna.userId === noteUser.id);
|
`antennaTimeline:${antenna.id}`,
|
||||||
|
'MAXLEN', '~', '200',
|
||||||
this.antennaNotesRepository.insert({
|
`${this.idService.parse(note.id).date.getTime()}-*`,
|
||||||
id: this.idService.genId(),
|
'note', note.id);
|
||||||
antennaId: antenna.id,
|
|
||||||
noteId: note.id,
|
|
||||||
read: read,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
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: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { ulid } from 'ulid';
|
import { ulid } from 'ulid';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.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 { genMeid } from '@/misc/id/meid.js';
|
||||||
import { genMeidg } from '@/misc/id/meidg.js';
|
import { genMeidg } from '@/misc/id/meidg.js';
|
||||||
import { genObjectId } from '@/misc/id/object-id.js';
|
import { genObjectId } from '@/misc/id/object-id.js';
|
||||||
|
@ -32,4 +32,17 @@ export class IdService {
|
||||||
default: throw new Error('unrecognized id generation method');
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { setImmediate } from 'node:timers/promises';
|
import { setImmediate } from 'node:timers/promises';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import { In, DataSource } from 'typeorm';
|
import { In, DataSource } from 'typeorm';
|
||||||
|
import Redis from 'ioredis';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { extractMentions } from '@/misc/extract-mentions.js';
|
import { extractMentions } from '@/misc/extract-mentions.js';
|
||||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||||
|
@ -150,6 +151,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -321,6 +325,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
|
|
||||||
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
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(
|
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
|
||||||
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
|
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
|
||||||
() => { /* aborted, ignore this */ },
|
() => { /* aborted, ignore this */ },
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { NotificationService } from './NotificationService.js';
|
import { NotificationService } from './NotificationService.js';
|
||||||
|
@ -38,9 +38,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
@Inject(DI.channelFollowingsRepository)
|
@Inject(DI.channelFollowingsRepository)
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
@Inject(DI.antennaNotesRepository)
|
|
||||||
private antennaNotesRepository: AntennaNotesRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
|
@ -121,7 +118,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
const readMentions: (Note | Packed<'Note'>)[] = [];
|
const readMentions: (Note | Packed<'Note'>)[] = [];
|
||||||
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
|
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
|
||||||
const readChannelNotes: (Note | Packed<'Note'>)[] = [];
|
const readChannelNotes: (Note | Packed<'Note'>)[] = [];
|
||||||
const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
|
|
||||||
|
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
if (note.mentions && note.mentions.includes(userId)) {
|
if (note.mentions && note.mentions.includes(userId)) {
|
||||||
|
@ -133,14 +129,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
if (note.channelId && followingChannels.has(note.channelId)) {
|
if (note.channelId && followingChannels.has(note.channelId)) {
|
||||||
readChannelNotes.push(note);
|
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)) {
|
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)]),
|
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 {
|
onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { Packed } from '@/misc/json-schema.js';
|
||||||
import type { Antenna } from '@/models/entities/Antenna.js';
|
import type { Antenna } from '@/models/entities/Antenna.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
@ -10,9 +10,6 @@ export class AntennaEntityService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.antennasRepository)
|
@Inject(DI.antennasRepository)
|
||||||
private antennasRepository: AntennasRepository,
|
private antennasRepository: AntennasRepository,
|
||||||
|
|
||||||
@Inject(DI.antennaNotesRepository)
|
|
||||||
private antennaNotesRepository: AntennaNotesRepository,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,8 +19,6 @@ export class AntennaEntityService {
|
||||||
): Promise<Packed<'Antenna'>> {
|
): Promise<Packed<'Antenna'>> {
|
||||||
const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src });
|
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 {
|
return {
|
||||||
id: antenna.id,
|
id: antenna.id,
|
||||||
createdAt: antenna.createdAt.toISOString(),
|
createdAt: antenna.createdAt.toISOString(),
|
||||||
|
@ -38,7 +33,7 @@ export class AntennaEntityService {
|
||||||
withReplies: antenna.withReplies,
|
withReplies: antenna.withReplies,
|
||||||
withFile: antenna.withFile,
|
withFile: antenna.withFile,
|
||||||
isActive: antenna.isActive,
|
isActive: antenna.isActive,
|
||||||
hasUnreadNote,
|
hasUnreadNote: false, // TODO
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { KVCache } from '@/misc/cache.js';
|
||||||
import type { Instance } from '@/models/entities/Instance.js';
|
import type { Instance } from '@/models/entities/Instance.js';
|
||||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||||
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } 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 { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
|
@ -108,9 +108,6 @@ export class UserEntityService implements OnModuleInit {
|
||||||
@Inject(DI.announcementsRepository)
|
@Inject(DI.announcementsRepository)
|
||||||
private announcementsRepository: AnnouncementsRepository,
|
private announcementsRepository: AnnouncementsRepository,
|
||||||
|
|
||||||
@Inject(DI.antennaNotesRepository)
|
|
||||||
private antennaNotesRepository: AntennaNotesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.pagesRepository)
|
@Inject(DI.pagesRepository)
|
||||||
private pagesRepository: PagesRepository,
|
private pagesRepository: PagesRepository,
|
||||||
|
|
||||||
|
@ -223,6 +220,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
|
public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
|
||||||
|
/*
|
||||||
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
|
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
|
||||||
|
|
||||||
const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({
|
const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({
|
||||||
|
@ -231,6 +229,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
}) : null;
|
}) : null;
|
||||||
|
|
||||||
return unread != null;
|
return unread != null;
|
||||||
|
*/
|
||||||
|
return false; // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -54,7 +54,6 @@ export const DI = {
|
||||||
clipNotesRepository: Symbol('clipNotesRepository'),
|
clipNotesRepository: Symbol('clipNotesRepository'),
|
||||||
clipFavoritesRepository: Symbol('clipFavoritesRepository'),
|
clipFavoritesRepository: Symbol('clipFavoritesRepository'),
|
||||||
antennasRepository: Symbol('antennasRepository'),
|
antennasRepository: Symbol('antennasRepository'),
|
||||||
antennaNotesRepository: Symbol('antennaNotesRepository'),
|
|
||||||
promoNotesRepository: Symbol('promoNotesRepository'),
|
promoNotesRepository: Symbol('promoNotesRepository'),
|
||||||
promoReadsRepository: Symbol('promoReadsRepository'),
|
promoReadsRepository: Symbol('promoReadsRepository'),
|
||||||
relaysRepository: Symbol('relaysRepository'),
|
relaysRepository: Symbol('relaysRepository'),
|
||||||
|
|
|
@ -23,3 +23,8 @@ export function genAid(date: Date): string {
|
||||||
counter++;
|
counter++;
|
||||||
return getTime(t) + getNoise();
|
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) };
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { DataSource } from 'typeorm';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
|
|
||||||
|
@ -298,12 +298,6 @@ const $antennasRepository: Provider = {
|
||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $antennaNotesRepository: Provider = {
|
|
||||||
provide: DI.antennaNotesRepository,
|
|
||||||
useFactory: (db: DataSource) => db.getRepository(AntennaNote),
|
|
||||||
inject: [DI.db],
|
|
||||||
};
|
|
||||||
|
|
||||||
const $promoNotesRepository: Provider = {
|
const $promoNotesRepository: Provider = {
|
||||||
provide: DI.promoNotesRepository,
|
provide: DI.promoNotesRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(PromoNote),
|
useFactory: (db: DataSource) => db.getRepository(PromoNote),
|
||||||
|
@ -453,7 +447,6 @@ const $roleAssignmentsRepository: Provider = {
|
||||||
$clipNotesRepository,
|
$clipNotesRepository,
|
||||||
$clipFavoritesRepository,
|
$clipFavoritesRepository,
|
||||||
$antennasRepository,
|
$antennasRepository,
|
||||||
$antennaNotesRepository,
|
|
||||||
$promoNotesRepository,
|
$promoNotesRepository,
|
||||||
$promoReadsRepository,
|
$promoReadsRepository,
|
||||||
$relaysRepository,
|
$relaysRepository,
|
||||||
|
@ -521,7 +514,6 @@ const $roleAssignmentsRepository: Provider = {
|
||||||
$clipNotesRepository,
|
$clipNotesRepository,
|
||||||
$clipFavoritesRepository,
|
$clipFavoritesRepository,
|
||||||
$antennasRepository,
|
$antennasRepository,
|
||||||
$antennaNotesRepository,
|
|
||||||
$promoNotesRepository,
|
$promoNotesRepository,
|
||||||
$promoReadsRepository,
|
$promoReadsRepository,
|
||||||
$relaysRepository,
|
$relaysRepository,
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -4,7 +4,6 @@ import { Ad } from '@/models/entities/Ad.js';
|
||||||
import { Announcement } from '@/models/entities/Announcement.js';
|
import { Announcement } from '@/models/entities/Announcement.js';
|
||||||
import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js';
|
import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js';
|
||||||
import { Antenna } from '@/models/entities/Antenna.js';
|
import { Antenna } from '@/models/entities/Antenna.js';
|
||||||
import { AntennaNote } from '@/models/entities/AntennaNote.js';
|
|
||||||
import { App } from '@/models/entities/App.js';
|
import { App } from '@/models/entities/App.js';
|
||||||
import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js';
|
import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js';
|
||||||
import { AuthSession } from '@/models/entities/AuthSession.js';
|
import { AuthSession } from '@/models/entities/AuthSession.js';
|
||||||
|
@ -73,7 +72,6 @@ export {
|
||||||
Announcement,
|
Announcement,
|
||||||
AnnouncementRead,
|
AnnouncementRead,
|
||||||
Antenna,
|
Antenna,
|
||||||
AntennaNote,
|
|
||||||
App,
|
App,
|
||||||
AttestationChallenge,
|
AttestationChallenge,
|
||||||
AuthSession,
|
AuthSession,
|
||||||
|
@ -141,7 +139,6 @@ export type AdsRepository = Repository<Ad>;
|
||||||
export type AnnouncementsRepository = Repository<Announcement>;
|
export type AnnouncementsRepository = Repository<Announcement>;
|
||||||
export type AnnouncementReadsRepository = Repository<AnnouncementRead>;
|
export type AnnouncementReadsRepository = Repository<AnnouncementRead>;
|
||||||
export type AntennasRepository = Repository<Antenna>;
|
export type AntennasRepository = Repository<Antenna>;
|
||||||
export type AntennaNotesRepository = Repository<AntennaNote>;
|
|
||||||
export type AppsRepository = Repository<App>;
|
export type AppsRepository = Repository<App>;
|
||||||
export type AttestationChallengesRepository = Repository<AttestationChallenge>;
|
export type AttestationChallengesRepository = Repository<AttestationChallenge>;
|
||||||
export type AuthSessionsRepository = Repository<AuthSession>;
|
export type AuthSessionsRepository = Repository<AuthSession>;
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { Ad } from '@/models/entities/Ad.js';
|
||||||
import { Announcement } from '@/models/entities/Announcement.js';
|
import { Announcement } from '@/models/entities/Announcement.js';
|
||||||
import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js';
|
import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js';
|
||||||
import { Antenna } from '@/models/entities/Antenna.js';
|
import { Antenna } from '@/models/entities/Antenna.js';
|
||||||
import { AntennaNote } from '@/models/entities/AntennaNote.js';
|
|
||||||
import { App } from '@/models/entities/App.js';
|
import { App } from '@/models/entities/App.js';
|
||||||
import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js';
|
import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js';
|
||||||
import { AuthSession } from '@/models/entities/AuthSession.js';
|
import { AuthSession } from '@/models/entities/AuthSession.js';
|
||||||
|
@ -168,7 +167,6 @@ export const entities = [
|
||||||
ClipNote,
|
ClipNote,
|
||||||
ClipFavorite,
|
ClipFavorite,
|
||||||
Antenna,
|
Antenna,
|
||||||
AntennaNote,
|
|
||||||
PromoNote,
|
PromoNote,
|
||||||
PromoRead,
|
PromoRead,
|
||||||
Relay,
|
Relay,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In, LessThan } from 'typeorm';
|
import { In, LessThan } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
@ -29,9 +29,6 @@ export class CleanProcessorService {
|
||||||
@Inject(DI.antennasRepository)
|
@Inject(DI.antennasRepository)
|
||||||
private antennasRepository: AntennasRepository,
|
private antennasRepository: AntennasRepository,
|
||||||
|
|
||||||
@Inject(DI.antennaNotesRepository)
|
|
||||||
private antennaNotesRepository: AntennaNotesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.roleAssignmentsRepository)
|
@Inject(DI.roleAssignmentsRepository)
|
||||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import Redis from 'ioredis';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
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 { QueryService } from '@/core/QueryService.js';
|
||||||
import { NoteReadService } from '@/core/NoteReadService.js';
|
import { NoteReadService } from '@/core/NoteReadService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -50,15 +52,16 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@Inject(DI.antennasRepository)
|
@Inject(DI.antennasRepository)
|
||||||
private antennasRepository: AntennasRepository,
|
private antennasRepository: AntennasRepository,
|
||||||
|
|
||||||
@Inject(DI.antennaNotesRepository)
|
private idService: IdService,
|
||||||
private antennaNotesRepository: AntennaNotesRepository,
|
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private noteReadService: NoteReadService,
|
private noteReadService: NoteReadService,
|
||||||
|
@ -73,9 +76,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw new ApiError(meta.errors.noSuchAntenna);
|
throw new ApiError(meta.errors.noSuchAntenna);
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
const noteIdsRes = await this.redisClient.xrevrange(
|
||||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
`antennaTimeline:${antenna.id}`,
|
||||||
.innerJoin(this.antennaNotesRepository.metadata.targetName, 'antennaNote', 'antennaNote.noteId = note.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')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||||
.leftJoinAndSelect('user.banner', 'banner')
|
.leftJoinAndSelect('user.banner', 'banner')
|
||||||
|
@ -86,16 +104,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
|
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||||
.andWhere('antennaNote.antennaId = :antennaId', { antennaId: antenna.id });
|
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
this.queryService.generateMutedUserQuery(query, me);
|
this.queryService.generateMutedUserQuery(query, me);
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
|
||||||
const notes = await query
|
const notes = await query.getMany();
|
||||||
.take(ps.limit)
|
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
.getMany();
|
|
||||||
|
|
||||||
if (notes.length > 0) {
|
if (notes.length > 0) {
|
||||||
this.noteReadService.read(me.id, notes);
|
this.noteReadService.read(me.id, notes);
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import Redis from 'ioredis';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { ChannelsRepository, NotesRepository } from '@/models/index.js';
|
import type { ChannelsRepository, NotesRepository } from '@/models/index.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -48,12 +50,16 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@Inject(DI.channelsRepository)
|
@Inject(DI.channelsRepository)
|
||||||
private channelsRepository: ChannelsRepository,
|
private channelsRepository: ChannelsRepository,
|
||||||
|
|
||||||
|
private idService: IdService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
|
@ -67,9 +73,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw new ApiError(meta.errors.noSuchChannel);
|
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
|
//#region Construct query
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
.andWhere('note.channelId = :channelId', { channelId: channel.id })
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||||
.leftJoinAndSelect('user.banner', 'banner')
|
.leftJoinAndSelect('user.banner', 'banner')
|
||||||
|
@ -90,7 +112,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
}
|
}
|
||||||
//#endregion
|
//#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);
|
if (me) this.activeUsersChart.read(me);
|
||||||
|
|
||||||
|
|
|
@ -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 : {};
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||||
await os.api(props.pagination.endpoint, {
|
await os.api(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: props.pagination.noPaging ? (props.pagination.limit || 10) : (props.pagination.limit || 10) + 1,
|
limit: props.pagination.limit ?? 10,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
for (let i = 0; i < res.length; i++) {
|
for (let i = 0; i < res.length; i++) {
|
||||||
const item = res[i];
|
const item = res[i];
|
||||||
if (i === 3) item._shouldInsertAd_ = true;
|
if (i === 3) item._shouldInsertAd_ = true;
|
||||||
}
|
}
|
||||||
if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) {
|
|
||||||
res.pop();
|
if (res.length === 0 || props.pagination.noPaging) {
|
||||||
|
items.value = res;
|
||||||
|
more.value = false;
|
||||||
|
} else {
|
||||||
if (props.pagination.reversed) moreFetching.value = true;
|
if (props.pagination.reversed) moreFetching.value = true;
|
||||||
items.value = res;
|
items.value = res;
|
||||||
more.value = true;
|
more.value = true;
|
||||||
} else {
|
|
||||||
items.value = res;
|
|
||||||
more.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
offset.value = res.length;
|
offset.value = res.length;
|
||||||
error.value = false;
|
error.value = false;
|
||||||
fetching.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 : {};
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||||
await os.api(props.pagination.endpoint, {
|
await os.api(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: SECOND_FETCH_LIMIT + 1,
|
limit: SECOND_FETCH_LIMIT,
|
||||||
...(props.pagination.offsetMode ? {
|
...(props.pagination.offsetMode ? {
|
||||||
offset: offset.value,
|
offset: offset.value,
|
||||||
} : {
|
} : {
|
||||||
|
@ -227,20 +228,7 @@ const fetchMore = async (): Promise<void> => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (res.length > SECOND_FETCH_LIMIT) {
|
if (res.length === 0) {
|
||||||
res.pop();
|
|
||||||
|
|
||||||
if (props.pagination.reversed) {
|
|
||||||
reverseConcat(res).then(() => {
|
|
||||||
more.value = true;
|
|
||||||
moreFetching.value = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
items.value = items.value.concat(res);
|
|
||||||
more.value = true;
|
|
||||||
moreFetching.value = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (props.pagination.reversed) {
|
if (props.pagination.reversed) {
|
||||||
reverseConcat(res).then(() => {
|
reverseConcat(res).then(() => {
|
||||||
more.value = false;
|
more.value = false;
|
||||||
|
@ -251,6 +239,17 @@ const fetchMore = async (): Promise<void> => {
|
||||||
more.value = false;
|
more.value = false;
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (props.pagination.reversed) {
|
||||||
|
reverseConcat(res).then(() => {
|
||||||
|
more.value = true;
|
||||||
|
moreFetching.value = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
items.value = items.value.concat(res);
|
||||||
|
more.value = true;
|
||||||
|
moreFetching.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
offset.value += res.length;
|
offset.value += res.length;
|
||||||
}, err => {
|
}, err => {
|
||||||
|
@ -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 : {};
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||||
await os.api(props.pagination.endpoint, {
|
await os.api(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: SECOND_FETCH_LIMIT + 1,
|
limit: SECOND_FETCH_LIMIT,
|
||||||
...(props.pagination.offsetMode ? {
|
...(props.pagination.offsetMode ? {
|
||||||
offset: offset.value,
|
offset: offset.value,
|
||||||
} : {
|
} : {
|
||||||
sinceId: items.value[items.value.length - 1].id,
|
sinceId: items.value[items.value.length - 1].id,
|
||||||
}),
|
}),
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
if (res.length > SECOND_FETCH_LIMIT) {
|
if (res.length === 0) {
|
||||||
res.pop();
|
|
||||||
items.value = items.value.concat(res);
|
|
||||||
more.value = true;
|
|
||||||
} else {
|
|
||||||
items.value = items.value.concat(res);
|
items.value = items.value.concat(res);
|
||||||
more.value = false;
|
more.value = false;
|
||||||
|
} else {
|
||||||
|
items.value = items.value.concat(res);
|
||||||
|
more.value = true;
|
||||||
}
|
}
|
||||||
offset.value += res.length;
|
offset.value += res.length;
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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"/>
|
<img :class="$style.inner" :src="url" decoding="async"/>
|
||||||
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
|
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
|
||||||
<div v-if="user.isCat" :class="[$style.ears, { [$style.mask]: useBlurEffect }]">
|
<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 MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
|
||||||
|
const animation = $ref(defaultStore.state.animation);
|
||||||
const squareAvatars = $ref(defaultStore.state.squareAvatars);
|
const squareAvatars = $ref(defaultStore.state.squareAvatars);
|
||||||
const useBlurEffect = $ref(defaultStore.state.useBlurEffect);
|
const useBlurEffect = $ref(defaultStore.state.useBlurEffect);
|
||||||
|
|
||||||
|
@ -86,6 +87,18 @@ watch(() => props.user.avatarBlurhash, () => {
|
||||||
to { transform: rotate(-37.6deg) skew(-30deg); }
|
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 {
|
.root {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -145,6 +158,14 @@ watch(() => props.user.avatarBlurhash, () => {
|
||||||
mask:
|
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%,
|
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)`
|
linear-gradient(#fff, #fff); // polyfill of `image(#fff)`
|
||||||
|
|
||||||
|
> .earLeft {
|
||||||
|
animation: eartightleft 6s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .earRight {
|
||||||
|
animation: eartightright 6s infinite;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .earLeft,
|
> .earLeft,
|
||||||
|
@ -226,7 +247,7 @@ watch(() => props.user.avatarBlurhash, () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&.animation:hover {
|
||||||
> .ears {
|
> .ears {
|
||||||
> .earLeft {
|
> .earLeft {
|
||||||
animation: earwiggleleft 1s infinite;
|
animation: earwiggleleft 1s infinite;
|
||||||
|
|
|
@ -8,7 +8,9 @@
|
||||||
|
|
||||||
<template v-if="metadata">
|
<template v-if="metadata">
|
||||||
<div v-if="!hideTitle" :class="$style.titleContainer" @click="top">
|
<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>
|
<i v-else-if="metadata.icon" :class="[$style.titleIcon, metadata.icon]"></i>
|
||||||
|
|
||||||
<div :class="$style.title">
|
<div :class="$style.title">
|
||||||
|
@ -249,13 +251,19 @@ onUnmounted(() => {
|
||||||
margin-left: 24px;
|
margin-left: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.titleAvatar {
|
.titleAvatarContainer {
|
||||||
$size: 32px;
|
$size: 32px;
|
||||||
display: inline-block;
|
contain: strict;
|
||||||
|
overflow: clip;
|
||||||
width: $size;
|
width: $size;
|
||||||
height: $size;
|
height: $size;
|
||||||
vertical-align: bottom;
|
padding: 8px;
|
||||||
margin: 0 8px;
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleAvatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
|
<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
|
||||||
|
|
||||||
<template v-if="useObjectStorage">
|
<template v-if="useObjectStorage">
|
||||||
<MkInput v-model="objectStorageBaseUrl">
|
<MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'">
|
||||||
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
|
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
|
||||||
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
|
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
@ -22,8 +22,9 @@
|
||||||
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
|
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="objectStorageEndpoint">
|
<MkInput v-model="objectStorageEndpoint" :placeholder="'example.com'">
|
||||||
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
|
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
|
||||||
|
<template #prefix>https://</template>
|
||||||
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
|
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
|
@ -60,6 +61,7 @@
|
||||||
|
|
||||||
<MkSwitch v-model="objectStorageS3ForcePathStyle">
|
<MkSwitch v-model="objectStorageS3ForcePathStyle">
|
||||||
<template #label>s3ForcePathStyle</template>
|
<template #label>s3ForcePathStyle</template>
|
||||||
|
<template #caption>{{ i18n.ts.s3ForcePathStyleDesc }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue