Merge branch 'develop' into stories/components-ab
This commit is contained in:
commit
310381ca80
|
@ -35,6 +35,7 @@
|
||||||
- アンテナのタイムライン取得時のパフォーマンスを向上
|
- アンテナのタイムライン取得時のパフォーマンスを向上
|
||||||
- チャンネルのタイムライン取得時のパフォーマンスを向上
|
- チャンネルのタイムライン取得時のパフォーマンスを向上
|
||||||
- 通知に関する全体的なパフォーマンスを向上
|
- 通知に関する全体的なパフォーマンスを向上
|
||||||
|
- webhookがcontent-type text/plain;charset=UTF-8 で飛んでくる問題を修正
|
||||||
|
|
||||||
## 13.10.3
|
## 13.10.3
|
||||||
|
|
||||||
|
|
|
@ -961,7 +961,9 @@ copyErrorInfo: "エラー情報をコピー"
|
||||||
joinThisServer: "このサーバーに登録する"
|
joinThisServer: "このサーバーに登録する"
|
||||||
exploreOtherServers: "他のサーバーを探す"
|
exploreOtherServers: "他のサーバーを探す"
|
||||||
letsLookAtTimeline: "タイムラインを見てみる"
|
letsLookAtTimeline: "タイムラインを見てみる"
|
||||||
disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。"
|
disableFederationConfirm: "連合なしにしますか?"
|
||||||
|
disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。"
|
||||||
|
disableFederationOk: "連合なしにする"
|
||||||
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
|
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
|
||||||
emailNotSupported: "このサーバーではメール配信はサポートされていません"
|
emailNotSupported: "このサーバーではメール配信はサポートされていません"
|
||||||
postToTheChannel: "チャンネルに投稿"
|
postToTheChannel: "チャンネルに投稿"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import type { UserProfile, UsersRepository } from '@/models/index.js';
|
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||||
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
@ -16,7 +16,12 @@ export class CacheService implements OnApplicationShutdown {
|
||||||
public localUserByIdCache: MemoryKVCache<LocalUser>;
|
public localUserByIdCache: MemoryKVCache<LocalUser>;
|
||||||
public uriPersonCache: MemoryKVCache<User | null>;
|
public uriPersonCache: MemoryKVCache<User | null>;
|
||||||
public userProfileCache: RedisKVCache<UserProfile>;
|
public userProfileCache: RedisKVCache<UserProfile>;
|
||||||
public userMutingsCache: RedisKVCache<string[]>;
|
public userMutingsCache: RedisKVCache<Set<string>>;
|
||||||
|
public userBlockingCache: RedisKVCache<Set<string>>;
|
||||||
|
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
||||||
|
public renoteMutingsCache: RedisKVCache<Set<string>>;
|
||||||
|
public userFollowingsCache: RedisKVCache<Set<string>>;
|
||||||
|
public userFollowingChannelsCache: RedisKVCache<Set<string>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redis)
|
||||||
|
@ -28,6 +33,24 @@ export class CacheService implements OnApplicationShutdown {
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.userProfilesRepository)
|
||||||
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.mutingsRepository)
|
||||||
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.blockingsRepository)
|
||||||
|
private blockingsRepository: BlockingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.renoteMutingsRepository)
|
||||||
|
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.channelFollowingsRepository)
|
||||||
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
) {
|
) {
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
//this.onMessage = this.onMessage.bind(this);
|
||||||
|
@ -36,8 +59,62 @@ export class CacheService implements OnApplicationShutdown {
|
||||||
this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity);
|
this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity);
|
||||||
this.localUserByIdCache = new MemoryKVCache<LocalUser>(Infinity);
|
this.localUserByIdCache = new MemoryKVCache<LocalUser>(Infinity);
|
||||||
this.uriPersonCache = new MemoryKVCache<User | null>(Infinity);
|
this.uriPersonCache = new MemoryKVCache<User | null>(Infinity);
|
||||||
this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', 1000 * 60 * 60 * 24, 1000 * 60);
|
|
||||||
this.userMutingsCache = new RedisKVCache<string[]>(this.redisClient, 'userMutings', 1000 * 60 * 60 * 24, 1000 * 60);
|
this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.userProfilesRepository.findOneByOrFail({ userId: key }),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(value),
|
||||||
|
fromRedisConverter: (value) => JSON.parse(value), // TODO: date型の考慮
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userMutingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userMutings', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.mutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userBlockingCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocking', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userBlockedCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocked', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renoteMutingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'renoteMutings', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.renoteMutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userFollowingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowings', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.channelFollowingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
|
});
|
||||||
|
|
||||||
this.redisSubscriber.on('message', this.onMessage);
|
this.redisSubscriber.on('message', this.onMessage);
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService
|
||||||
import { UserBlockingService } from './UserBlockingService.js';
|
import { UserBlockingService } from './UserBlockingService.js';
|
||||||
import { CacheService } from './CacheService.js';
|
import { CacheService } from './CacheService.js';
|
||||||
import { UserFollowingService } from './UserFollowingService.js';
|
import { UserFollowingService } from './UserFollowingService.js';
|
||||||
import { UserKeypairStoreService } from './UserKeypairStoreService.js';
|
import { UserKeypairService } from './UserKeypairService.js';
|
||||||
import { UserListService } from './UserListService.js';
|
import { UserListService } from './UserListService.js';
|
||||||
import { UserMutingService } from './UserMutingService.js';
|
import { UserMutingService } from './UserMutingService.js';
|
||||||
import { UserSuspendService } from './UserSuspendService.js';
|
import { UserSuspendService } from './UserSuspendService.js';
|
||||||
|
@ -161,7 +161,7 @@ const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthentic
|
||||||
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
|
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
|
||||||
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
|
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
|
||||||
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
|
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
|
||||||
const $UserKeypairStoreService: Provider = { provide: 'UserKeypairStoreService', useExisting: UserKeypairStoreService };
|
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
||||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||||
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
||||||
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||||
|
@ -284,7 +284,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
UserBlockingService,
|
UserBlockingService,
|
||||||
CacheService,
|
CacheService,
|
||||||
UserFollowingService,
|
UserFollowingService,
|
||||||
UserKeypairStoreService,
|
UserKeypairService,
|
||||||
UserListService,
|
UserListService,
|
||||||
UserMutingService,
|
UserMutingService,
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
|
@ -401,7 +401,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$UserBlockingService,
|
$UserBlockingService,
|
||||||
$CacheService,
|
$CacheService,
|
||||||
$UserFollowingService,
|
$UserFollowingService,
|
||||||
$UserKeypairStoreService,
|
$UserKeypairService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
$UserMutingService,
|
$UserMutingService,
|
||||||
$UserSuspendService,
|
$UserSuspendService,
|
||||||
|
@ -519,7 +519,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
UserBlockingService,
|
UserBlockingService,
|
||||||
CacheService,
|
CacheService,
|
||||||
UserFollowingService,
|
UserFollowingService,
|
||||||
UserKeypairStoreService,
|
UserKeypairService,
|
||||||
UserListService,
|
UserListService,
|
||||||
UserMutingService,
|
UserMutingService,
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
|
@ -635,7 +635,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$UserBlockingService,
|
$UserBlockingService,
|
||||||
$CacheService,
|
$CacheService,
|
||||||
$UserFollowingService,
|
$UserFollowingService,
|
||||||
$UserKeypairStoreService,
|
$UserKeypairService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
$UserMutingService,
|
$UserMutingService,
|
||||||
$UserSuspendService,
|
$UserSuspendService,
|
||||||
|
|
|
@ -36,8 +36,5 @@ export class DeleteAccountService {
|
||||||
await this.usersRepository.update(user.id, {
|
await this.usersRepository.update(user.id, {
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Terminate streaming
|
|
||||||
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ import type {
|
||||||
MainStreamTypes,
|
MainStreamTypes,
|
||||||
NoteStreamTypes,
|
NoteStreamTypes,
|
||||||
UserListStreamTypes,
|
UserListStreamTypes,
|
||||||
UserStreamTypes,
|
|
||||||
} from '@/server/api/stream/types.js';
|
} from '@/server/api/stream/types.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';
|
||||||
|
@ -49,11 +48,6 @@ export class GlobalEventService {
|
||||||
this.publish('internal', type, typeof value === 'undefined' ? null : value);
|
this.publish('internal', type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public publishUserEvent<K extends keyof UserStreamTypes>(userId: User['id'], type: K, value?: UserStreamTypes[K]): void {
|
|
||||||
this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishBroadcastStream<K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void {
|
public publishBroadcastStream<K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void {
|
||||||
this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
|
this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
|
||||||
|
|
|
@ -502,18 +502,6 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Channel
|
|
||||||
if (note.channelId) {
|
|
||||||
this.channelFollowingsRepository.findBy({ followeeId: note.channelId }).then(followings => {
|
|
||||||
for (const following of followings) {
|
|
||||||
this.noteReadService.insertNoteUnread(following.followerId, note, {
|
|
||||||
isSpecified: false,
|
|
||||||
isMentioned: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.reply) {
|
if (data.reply) {
|
||||||
this.saveReply(data.reply, note);
|
this.saveReply(data.reply, note);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,20 @@
|
||||||
import { setTimeout } from 'node:timers/promises';
|
import { setTimeout } from 'node:timers/promises';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { In, IsNull, Not } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import type { Channel } from '@/models/entities/Channel.js';
|
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
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 } from '@/models/index.js';
|
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/index.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { NotificationService } from './NotificationService.js';
|
|
||||||
import { AntennaService } from './AntennaService.js';
|
|
||||||
import { PushNotificationService } from './PushNotificationService.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteReadService implements OnApplicationShutdown {
|
export class NoteReadService implements OnApplicationShutdown {
|
||||||
#shutdownController = new AbortController();
|
#shutdownController = new AbortController();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.noteUnreadsRepository)
|
@Inject(DI.noteUnreadsRepository)
|
||||||
private noteUnreadsRepository: NoteUnreadsRepository,
|
private noteUnreadsRepository: NoteUnreadsRepository,
|
||||||
|
|
||||||
|
@ -32,18 +24,8 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
@Inject(DI.noteThreadMutingsRepository)
|
@Inject(DI.noteThreadMutingsRepository)
|
||||||
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
|
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
|
||||||
private followingsRepository: FollowingsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.channelFollowingsRepository)
|
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private notificationService: NotificationService,
|
|
||||||
private antennaService: AntennaService,
|
|
||||||
private pushNotificationService: PushNotificationService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +36,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
isMentioned: boolean;
|
isMentioned: boolean;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
//#region ミュートしているなら無視
|
//#region ミュートしているなら無視
|
||||||
// TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする
|
|
||||||
const mute = await this.mutingsRepository.findBy({
|
const mute = await this.mutingsRepository.findBy({
|
||||||
muterId: userId,
|
muterId: userId,
|
||||||
});
|
});
|
||||||
|
@ -74,7 +55,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
isSpecified: params.isSpecified,
|
isSpecified: params.isSpecified,
|
||||||
isMentioned: params.isMentioned,
|
isMentioned: params.isMentioned,
|
||||||
noteChannelId: note.channelId,
|
|
||||||
noteUserId: note.userId,
|
noteUserId: note.userId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -92,9 +72,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
if (params.isSpecified) {
|
if (params.isSpecified) {
|
||||||
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
|
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
|
||||||
}
|
}
|
||||||
if (note.channelId) {
|
|
||||||
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
|
|
||||||
}
|
|
||||||
}, () => { /* aborted, ignore it */ });
|
}, () => { /* aborted, ignore it */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,22 +79,9 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
public async read(
|
public async read(
|
||||||
userId: User['id'],
|
userId: User['id'],
|
||||||
notes: (Note | Packed<'Note'>)[],
|
notes: (Note | Packed<'Note'>)[],
|
||||||
info?: {
|
|
||||||
following: Set<User['id']>;
|
|
||||||
followingChannels: Set<Channel['id']>;
|
|
||||||
},
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const followingChannels = info?.followingChannels ? info.followingChannels : new Set<string>((await this.channelFollowingsRepository.find({
|
|
||||||
where: {
|
|
||||||
followerId: userId,
|
|
||||||
},
|
|
||||||
select: ['followeeId'],
|
|
||||||
})).map(x => x.followeeId));
|
|
||||||
|
|
||||||
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
|
|
||||||
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'>)[] = [];
|
|
||||||
|
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
if (note.mentions && note.mentions.includes(userId)) {
|
if (note.mentions && note.mentions.includes(userId)) {
|
||||||
|
@ -125,17 +89,13 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
|
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
|
||||||
readSpecifiedNotes.push(note);
|
readSpecifiedNotes.push(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.channelId && followingChannels.has(note.channelId)) {
|
|
||||||
readChannelNotes.push(note);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
|
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0)) {
|
||||||
// Remove the record
|
// Remove the record
|
||||||
await this.noteUnreadsRepository.delete({
|
await this.noteUnreadsRepository.delete({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]),
|
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: ↓まとめてクエリしたい
|
// TODO: ↓まとめてクエリしたい
|
||||||
|
@ -159,16 +119,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.noteUnreadsRepository.countBy({
|
|
||||||
userId: userId,
|
|
||||||
noteChannelId: Not(IsNull()),
|
|
||||||
}).then(channelNoteCount => {
|
|
||||||
if (channelNoteCount === 0) {
|
|
||||||
// 全て既読になったイベントを発行
|
|
||||||
this.globalEventService.publishMainStream(userId, 'readAllChannels');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
type: Notification['type'],
|
type: Notification['type'],
|
||||||
data: Partial<Notification>,
|
data: Partial<Notification>,
|
||||||
): Promise<Notification | null> {
|
): Promise<Notification | null> {
|
||||||
const profile = await this.cacheService.userProfileCache.fetch(notifieeId, () => this.userProfilesRepository.findOneByOrFail({ userId: notifieeId }));
|
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
|
||||||
const isMuted = profile.mutingNotificationTypes.includes(type);
|
const isMuted = profile.mutingNotificationTypes.includes(type);
|
||||||
if (isMuted) return null;
|
if (isMuted) return null;
|
||||||
|
|
||||||
|
@ -82,8 +82,8 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId, () => this.mutingsRepository.findBy({ muterId: notifieeId }).then(xs => xs.map(x => x.muteeId)));
|
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
|
||||||
if (mutings.includes(data.notifierId)) {
|
if (mutings.has(data.notifierId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,6 +158,7 @@ export class ReactionService {
|
||||||
// カスタム絵文字リアクションだったら絵文字情報も送る
|
// カスタム絵文字リアクションだったら絵文字情報も送る
|
||||||
const decodedReaction = this.decodeReaction(reaction);
|
const decodedReaction = this.decodeReaction(reaction);
|
||||||
|
|
||||||
|
// TODO: Cache
|
||||||
const emoji = await this.emojisRepository.findOne({
|
const emoji = await this.emojisRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
name: decodedReaction.name,
|
name: decodedReaction.name,
|
||||||
|
|
|
@ -1,39 +1,29 @@
|
||||||
|
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
import Redis from 'ioredis';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import type { Blocking } from '@/models/entities/Blocking.js';
|
import type { Blocking } from '@/models/entities/Blocking.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
|
import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { WebhookService } from '@/core/WebhookService.js';
|
import { WebhookService } from '@/core/WebhookService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MemoryKVCache } from '@/misc/cache.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserBlockingService implements OnApplicationShutdown {
|
export class UserBlockingService implements OnModuleInit {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
private userFollowingService: UserFollowingService;
|
||||||
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
|
|
||||||
private blockingsByUserIdCache: MemoryKVCache<User['id'][]>;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redisSubscriber)
|
private moduleRef: ModuleRef,
|
||||||
private redisSubscriber: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
|
||||||
private followingsRepository: FollowingsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.followRequestsRepository)
|
@Inject(DI.followRequestsRepository)
|
||||||
private followRequestsRepository: FollowRequestsRepository,
|
private followRequestsRepository: FollowRequestsRepository,
|
||||||
|
@ -47,47 +37,20 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListJoiningsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||||
|
|
||||||
|
private cacheService: CacheService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private webhookService: WebhookService,
|
private webhookService: WebhookService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private perUserFollowingChart: PerUserFollowingChart,
|
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('user-block');
|
this.logger = this.loggerService.getLogger('user-block');
|
||||||
|
|
||||||
this.blockingsByUserIdCache = new MemoryKVCache<User['id'][]>(Infinity);
|
|
||||||
|
|
||||||
this.redisSubscriber.on('message', this.onMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
onModuleInit() {
|
||||||
private async onMessage(_: string, data: string): Promise<void> {
|
this.userFollowingService = this.moduleRef.get('UserFollowingService');
|
||||||
const obj = JSON.parse(data);
|
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
|
||||||
switch (type) {
|
|
||||||
case 'blockingCreated': {
|
|
||||||
const cached = this.blockingsByUserIdCache.get(body.blockerId);
|
|
||||||
if (cached) {
|
|
||||||
this.blockingsByUserIdCache.set(body.blockerId, [...cached, ...[body.blockeeId]]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'blockingDeleted': {
|
|
||||||
const cached = this.blockingsByUserIdCache.get(body.blockerId);
|
|
||||||
if (cached) {
|
|
||||||
this.blockingsByUserIdCache.set(body.blockerId, cached.filter(x => x !== body.blockeeId));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -95,8 +58,8 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.cancelRequest(blocker, blockee),
|
this.cancelRequest(blocker, blockee),
|
||||||
this.cancelRequest(blockee, blocker),
|
this.cancelRequest(blockee, blocker),
|
||||||
this.unFollow(blocker, blockee),
|
this.userFollowingService.unfollow(blocker, blockee),
|
||||||
this.unFollow(blockee, blocker),
|
this.userFollowingService.unfollow(blockee, blocker),
|
||||||
this.removeFromList(blockee, blocker),
|
this.removeFromList(blockee, blocker),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -111,6 +74,9 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||||
|
|
||||||
await this.blockingsRepository.insert(blocking);
|
await this.blockingsRepository.insert(blocking);
|
||||||
|
|
||||||
|
this.cacheService.userBlockingCache.refresh(blocker.id);
|
||||||
|
this.cacheService.userBlockedCache.refresh(blockee.id);
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('blockingCreated', {
|
this.globalEventService.publishInternalEvent('blockingCreated', {
|
||||||
blockerId: blocker.id,
|
blockerId: blocker.id,
|
||||||
blockeeId: blockee.id,
|
blockeeId: blockee.id,
|
||||||
|
@ -148,7 +114,6 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||||
this.userEntityService.pack(followee, follower, {
|
this.userEntityService.pack(followee, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
|
||||||
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
|
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
|
@ -173,54 +138,6 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async unFollow(follower: User, followee: User) {
|
|
||||||
const following = await this.followingsRepository.findOneBy({
|
|
||||||
followerId: follower.id,
|
|
||||||
followeeId: followee.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (following == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
this.followingsRepository.delete(following.id),
|
|
||||||
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
|
|
||||||
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
|
|
||||||
this.perUserFollowingChart.update(follower, followee, false),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Publish unfollow event
|
|
||||||
if (this.userEntityService.isLocalUser(follower)) {
|
|
||||||
this.userEntityService.pack(followee, follower, {
|
|
||||||
detail: true,
|
|
||||||
}).then(async packed => {
|
|
||||||
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
|
||||||
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
|
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
|
||||||
for (const webhook of webhooks) {
|
|
||||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
|
||||||
user: packed,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// リモートにフォローをしていたらUndoFollow送信
|
|
||||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
|
||||||
this.queueService.deliver(follower, content, followee.inbox, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// リモートからフォローをされていたらRejectFollow送信
|
|
||||||
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
|
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
|
|
||||||
this.queueService.deliver(followee, content, follower.inbox, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async removeFromList(listOwner: User, user: User) {
|
private async removeFromList(listOwner: User, user: User) {
|
||||||
const userLists = await this.userListsRepository.findBy({
|
const userLists = await this.userListsRepository.findBy({
|
||||||
|
@ -254,6 +171,9 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||||
|
|
||||||
await this.blockingsRepository.delete(blocking.id);
|
await this.blockingsRepository.delete(blocking.id);
|
||||||
|
|
||||||
|
this.cacheService.userBlockingCache.refresh(blocker.id);
|
||||||
|
this.cacheService.userBlockedCache.refresh(blockee.id);
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('blockingDeleted', {
|
this.globalEventService.publishInternalEvent('blockingDeleted', {
|
||||||
blockerId: blocker.id,
|
blockerId: blocker.id,
|
||||||
blockeeId: blockee.id,
|
blockeeId: blockee.id,
|
||||||
|
@ -268,17 +188,6 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
|
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
|
||||||
const blockedUserIds = await this.blockingsByUserIdCache.fetch(blockerId, () => this.blockingsRepository.find({
|
return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId);
|
||||||
where: {
|
|
||||||
blockerId,
|
|
||||||
},
|
|
||||||
select: ['blockeeId'],
|
|
||||||
}).then(records => records.map(record => record.blockeeId)));
|
|
||||||
return blockedUserIds.includes(blockeeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
|
||||||
this.redisSubscriber.off('message', this.onMessage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
|
||||||
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
@ -18,6 +19,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import Logger from '../logger.js';
|
import Logger from '../logger.js';
|
||||||
|
|
||||||
const logger = new Logger('following/create');
|
const logger = new Logger('following/create');
|
||||||
|
@ -36,8 +38,12 @@ type Remote = RemoteUser | {
|
||||||
type Both = Local | Remote;
|
type Both = Local | Remote;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserFollowingService {
|
export class UserFollowingService implements OnModuleInit {
|
||||||
|
private userBlockingService: UserBlockingService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private moduleRef: ModuleRef,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -53,8 +59,8 @@ export class UserFollowingService {
|
||||||
@Inject(DI.instancesRepository)
|
@Inject(DI.instancesRepository)
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
|
private cacheService: CacheService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private userBlockingService: UserBlockingService,
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
|
@ -68,6 +74,10 @@ export class UserFollowingService {
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.userBlockingService = this.moduleRef.get('UserBlockingService');
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
|
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
|
||||||
const [follower, followee] = await Promise.all([
|
const [follower, followee] = await Promise.all([
|
||||||
|
@ -172,6 +182,8 @@ export class UserFollowingService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.cacheService.userFollowingsCache.refresh(follower.id);
|
||||||
|
|
||||||
const req = await this.followRequestsRepository.findOneBy({
|
const req = await this.followRequestsRepository.findOneBy({
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
|
@ -225,7 +237,6 @@ export class UserFollowingService {
|
||||||
this.userEntityService.pack(followee.id, follower, {
|
this.userEntityService.pack(followee.id, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
|
||||||
this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
||||||
|
@ -279,6 +290,8 @@ export class UserFollowingService {
|
||||||
|
|
||||||
await this.followingsRepository.delete(following.id);
|
await this.followingsRepository.delete(following.id);
|
||||||
|
|
||||||
|
this.cacheService.userFollowingsCache.refresh(follower.id);
|
||||||
|
|
||||||
this.decrementFollowing(follower, followee);
|
this.decrementFollowing(follower, followee);
|
||||||
|
|
||||||
// Publish unfollow event
|
// Publish unfollow event
|
||||||
|
@ -286,7 +299,6 @@ export class UserFollowingService {
|
||||||
this.userEntityService.pack(followee.id, follower, {
|
this.userEntityService.pack(followee.id, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
|
||||||
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
|
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
|
@ -579,7 +591,6 @@ export class UserFollowingService {
|
||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packedFollowee);
|
|
||||||
this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee);
|
this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee);
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
import type { User } from '@/models/entities/User.js';
|
||||||
|
import type { UserKeypairsRepository } from '@/models/index.js';
|
||||||
|
import { RedisKVCache } from '@/misc/cache.js';
|
||||||
|
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserKeypairService {
|
||||||
|
private cache: RedisKVCache<UserKeypair>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.userKeypairsRepository)
|
||||||
|
private userKeypairsRepository: UserKeypairsRepository,
|
||||||
|
) {
|
||||||
|
this.cache = new RedisKVCache<UserKeypair>(this.redisClient, 'userKeypair', {
|
||||||
|
lifetime: 1000 * 60 * 60 * 24, // 24h
|
||||||
|
memoryCacheLifetime: Infinity,
|
||||||
|
fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(value),
|
||||||
|
fromRedisConverter: (value) => JSON.parse(value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
|
||||||
|
return await this.cache.fetch(userId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +0,0 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import type { User } from '@/models/entities/User.js';
|
|
||||||
import type { UserKeypairsRepository } from '@/models/index.js';
|
|
||||||
import { MemoryKVCache } from '@/misc/cache.js';
|
|
||||||
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UserKeypairStoreService {
|
|
||||||
private cache: MemoryKVCache<UserKeypair>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@Inject(DI.userKeypairsRepository)
|
|
||||||
private userKeypairsRepository: UserKeypairsRepository,
|
|
||||||
) {
|
|
||||||
this.cache = new MemoryKVCache<UserKeypair>(Infinity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
|
|
||||||
return await this.cache.fetch(userId, () => this.userKeypairsRepository.findOneByOrFail({ userId: userId }));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +1,47 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UsersRepository, MutingsRepository } from '@/models/index.js';
|
import { In } from 'typeorm';
|
||||||
|
import type { MutingsRepository, Muting } from '@/models/index.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserMutingService {
|
export class UserMutingService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private cacheService: CacheService,
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async mute(user: User, target: User): Promise<void> {
|
public async mute(user: User, target: User, expiresAt: Date | null = null): Promise<void> {
|
||||||
await this.mutingsRepository.insert({
|
await this.mutingsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
expiresAt: expiresAt ?? null,
|
||||||
muterId: user.id,
|
muterId: user.id,
|
||||||
muteeId: target.id,
|
muteeId: target.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.cacheService.userMutingsCache.refresh(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async unmute(mutings: Muting[]): Promise<void> {
|
||||||
|
if (mutings.length === 0) return;
|
||||||
|
|
||||||
|
await this.mutingsRepository.delete({
|
||||||
|
id: In(mutings.map(m => m.id)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const muterIds = [...new Set(mutings.map(m => m.muterId))];
|
||||||
|
for (const muterId of muterIds) {
|
||||||
|
this.cacheService.userMutingsCache.refresh(muterId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import type { NoteReaction } from '@/models/entities/NoteReaction.js';
|
||||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||||
import type { Poll } from '@/models/entities/Poll.js';
|
import type { Poll } from '@/models/entities/Poll.js';
|
||||||
import type { PollVote } from '@/models/entities/PollVote.js';
|
import type { PollVote } from '@/models/entities/PollVote.js';
|
||||||
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
|
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||||
import { MfmService } from '@/core/MfmService.js';
|
import { MfmService } from '@/core/MfmService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
|
@ -53,7 +53,7 @@ export class ApRendererService {
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private driveFileEntityService: DriveFileEntityService,
|
private driveFileEntityService: DriveFileEntityService,
|
||||||
private ldSignatureService: LdSignatureService,
|
private ldSignatureService: LdSignatureService,
|
||||||
private userKeypairStoreService: UserKeypairStoreService,
|
private userKeypairService: UserKeypairService,
|
||||||
private apMfmService: ApMfmService,
|
private apMfmService: ApMfmService,
|
||||||
private mfmService: MfmService,
|
private mfmService: MfmService,
|
||||||
) {
|
) {
|
||||||
|
@ -473,7 +473,7 @@ export class ApRendererService {
|
||||||
...hashtagTags,
|
...hashtagTags,
|
||||||
];
|
];
|
||||||
|
|
||||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const person = {
|
const person = {
|
||||||
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
||||||
|
@ -640,7 +640,7 @@ export class ApRendererService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> {
|
public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> {
|
||||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const ldSignature = this.ldSignatureService.use();
|
const ldSignature = this.ldSignatureService.use();
|
||||||
ldSignature.debug = false;
|
ldSignature.debug = false;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
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 type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
|
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
@ -131,7 +131,7 @@ export class ApRequestService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
private userKeypairStoreService: UserKeypairStoreService,
|
private userKeypairService: UserKeypairService,
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
|
@ -143,7 +143,7 @@ export class ApRequestService {
|
||||||
public async signedPost(user: { id: User['id'] }, url: string, object: any) {
|
public async signedPost(user: { id: User['id'] }, url: string, object: any) {
|
||||||
const body = JSON.stringify(object);
|
const body = JSON.stringify(object);
|
||||||
|
|
||||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const req = ApRequestCreator.createSignedPost({
|
const req = ApRequestCreator.createSignedPost({
|
||||||
key: {
|
key: {
|
||||||
|
@ -170,7 +170,7 @@ export class ApRequestService {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async signedGet(url: string, user: { id: User['id'] }) {
|
public async signedGet(url: string, user: { id: User['id'] }) {
|
||||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const req = ApRequestCreator.createSignedGet({
|
const req = ApRequestCreator.createSignedGet({
|
||||||
key: {
|
key: {
|
||||||
|
|
|
@ -234,18 +234,6 @@ export class UserEntityService implements OnModuleInit {
|
||||||
return false; // TODO
|
return false; // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
|
|
||||||
const channels = await this.channelFollowingsRepository.findBy({ followerId: userId });
|
|
||||||
|
|
||||||
const unread = channels.length > 0 ? await this.noteUnreadsRepository.findOneBy({
|
|
||||||
userId: userId,
|
|
||||||
noteChannelId: In(channels.map(x => x.followeeId)),
|
|
||||||
}) : null;
|
|
||||||
|
|
||||||
return unread != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
||||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
||||||
|
@ -463,7 +451,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
}).then(count => count > 0),
|
}).then(count => count > 0),
|
||||||
hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
|
hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
|
||||||
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
||||||
hasUnreadChannel: this.getHasUnreadChannel(user.id),
|
hasUnreadChannel: false, // 後方互換性のため
|
||||||
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
||||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||||
mutedWords: profile!.mutedWords,
|
mutedWords: profile!.mutedWords,
|
||||||
|
|
|
@ -1,29 +1,29 @@
|
||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
// redis通すとDateのインスタンスはstringに変換されるので
|
|
||||||
type Serialized<T> = {
|
|
||||||
[K in keyof T]:
|
|
||||||
T[K] extends Date
|
|
||||||
? string
|
|
||||||
: T[K] extends (Date | null)
|
|
||||||
? (string | null)
|
|
||||||
: T[K] extends Record<string, any>
|
|
||||||
? Serialized<T[K]>
|
|
||||||
: T[K];
|
|
||||||
};
|
|
||||||
|
|
||||||
export class RedisKVCache<T> {
|
export class RedisKVCache<T> {
|
||||||
private redisClient: Redis.Redis;
|
private redisClient: Redis.Redis;
|
||||||
private name: string;
|
private name: string;
|
||||||
private lifetime: number;
|
private lifetime: number;
|
||||||
private memoryCache: MemoryKVCache<T>;
|
private memoryCache: MemoryKVCache<T>;
|
||||||
|
private fetcher: (key: string) => Promise<T>;
|
||||||
|
private toRedisConverter: (value: T) => string;
|
||||||
|
private fromRedisConverter: (value: string) => T;
|
||||||
|
|
||||||
constructor(redisClient: RedisKVCache<never>['redisClient'], name: RedisKVCache<never>['name'], lifetime: RedisKVCache<never>['lifetime'], memoryCacheLifetime: number) {
|
constructor(redisClient: RedisKVCache<T>['redisClient'], name: RedisKVCache<T>['name'], opts: {
|
||||||
|
lifetime: RedisKVCache<T>['lifetime'];
|
||||||
|
memoryCacheLifetime: number;
|
||||||
|
fetcher: RedisKVCache<T>['fetcher'];
|
||||||
|
toRedisConverter: RedisKVCache<T>['toRedisConverter'];
|
||||||
|
fromRedisConverter: RedisKVCache<T>['fromRedisConverter'];
|
||||||
|
}) {
|
||||||
this.redisClient = redisClient;
|
this.redisClient = redisClient;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.lifetime = lifetime;
|
this.lifetime = opts.lifetime;
|
||||||
this.memoryCache = new MemoryKVCache(memoryCacheLifetime);
|
this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime);
|
||||||
|
this.fetcher = opts.fetcher;
|
||||||
|
this.toRedisConverter = opts.toRedisConverter;
|
||||||
|
this.fromRedisConverter = opts.fromRedisConverter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -32,25 +32,25 @@ export class RedisKVCache<T> {
|
||||||
if (this.lifetime === Infinity) {
|
if (this.lifetime === Infinity) {
|
||||||
await this.redisClient.set(
|
await this.redisClient.set(
|
||||||
`kvcache:${this.name}:${key}`,
|
`kvcache:${this.name}:${key}`,
|
||||||
JSON.stringify(value),
|
this.toRedisConverter(value),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await this.redisClient.set(
|
await this.redisClient.set(
|
||||||
`kvcache:${this.name}:${key}`,
|
`kvcache:${this.name}:${key}`,
|
||||||
JSON.stringify(value),
|
this.toRedisConverter(value),
|
||||||
'ex', Math.round(this.lifetime / 1000),
|
'ex', Math.round(this.lifetime / 1000),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async get(key: string): Promise<Serialized<T> | T | undefined> {
|
public async get(key: string): Promise<T | undefined> {
|
||||||
const memoryCached = this.memoryCache.get(key);
|
const memoryCached = this.memoryCache.get(key);
|
||||||
if (memoryCached !== undefined) return memoryCached;
|
if (memoryCached !== undefined) return memoryCached;
|
||||||
|
|
||||||
const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`);
|
const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`);
|
||||||
if (cached == null) return undefined;
|
if (cached == null) return undefined;
|
||||||
return JSON.parse(cached);
|
return this.fromRedisConverter(cached);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -61,28 +61,28 @@ export class RedisKVCache<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetch(key: string, fetcher: () => Promise<T>, validator?: (cachedValue: Serialized<T> | T) => boolean): Promise<Serialized<T> | T> {
|
public async fetch(key: string): Promise<T> {
|
||||||
const cachedValue = await this.get(key);
|
const cachedValue = await this.get(key);
|
||||||
if (cachedValue !== undefined) {
|
if (cachedValue !== undefined) {
|
||||||
if (validator) {
|
|
||||||
if (validator(cachedValue)) {
|
|
||||||
// Cache HIT
|
// Cache HIT
|
||||||
return cachedValue;
|
return cachedValue;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Cache HIT
|
|
||||||
return cachedValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache MISS
|
// Cache MISS
|
||||||
const value = await fetcher();
|
const value = await this.fetcher(key);
|
||||||
this.set(key, value);
|
this.set(key, value);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async refresh(key: string) {
|
||||||
|
const value = await this.fetcher(key);
|
||||||
|
this.set(key, value);
|
||||||
|
|
||||||
|
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
||||||
|
|
|
@ -311,10 +311,6 @@ export const packedMeDetailedOnlySchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
hasUnreadChannel: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
hasUnreadNotification: {
|
hasUnreadNotification: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
|
|
|
@ -4,10 +4,10 @@ import { DI } from '@/di-symbols.js';
|
||||||
import type { MutingsRepository } from '@/models/index.js';
|
import type { MutingsRepository } 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 { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { UserMutingService } from '@/core/UserMutingService.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type Bull from 'bull';
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CheckExpiredMutingsProcessorService {
|
export class CheckExpiredMutingsProcessorService {
|
||||||
|
@ -20,7 +20,7 @@ export class CheckExpiredMutingsProcessorService {
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
private globalEventService: GlobalEventService,
|
private userMutingService: UserMutingService,
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('check-expired-mutings');
|
this.logger = this.queueLoggerService.logger.createSubLogger('check-expired-mutings');
|
||||||
|
@ -37,13 +37,7 @@ export class CheckExpiredMutingsProcessorService {
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
if (expired.length > 0) {
|
if (expired.length > 0) {
|
||||||
await this.mutingsRepository.delete({
|
await this.userMutingService.unmute(expired);
|
||||||
id: In(expired.map(m => m.id)),
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const m of expired) {
|
|
||||||
this.globalEventService.publishUserEvent(m.muterId, 'unmute', m.mutee!);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.succ('All expired mutings checked.');
|
this.logger.succ('All expired mutings checked.');
|
||||||
|
|
|
@ -39,6 +39,7 @@ export class WebhookDeliverProcessorService {
|
||||||
'X-Misskey-Host': this.config.host,
|
'X-Misskey-Host': this.config.host,
|
||||||
'X-Misskey-Hook-Id': job.data.webhookId,
|
'X-Misskey-Hook-Id': job.data.webhookId,
|
||||||
'X-Misskey-Hook-Secret': job.data.secret,
|
'X-Misskey-Hook-Secret': job.data.secret,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
hookId: job.data.webhookId,
|
hookId: job.data.webhookId,
|
||||||
|
|
|
@ -12,7 +12,7 @@ import type { Config } from '@/config.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||||
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
|
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||||
import type { Following } from '@/models/entities/Following.js';
|
import type { Following } from '@/models/entities/Following.js';
|
||||||
import { countIf } from '@/misc/prelude/array.js';
|
import { countIf } from '@/misc/prelude/array.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
|
@ -58,7 +58,7 @@ export class ActivityPubServerService {
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private userKeypairStoreService: UserKeypairStoreService,
|
private userKeypairService: UserKeypairService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
) {
|
) {
|
||||||
//this.createServer = this.createServer.bind(this);
|
//this.createServer = this.createServer.bind(this);
|
||||||
|
@ -540,7 +540,7 @@ export class ActivityPubServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
reply.header('Cache-Control', 'public, max-age=180');
|
reply.header('Cache-Control', 'public, max-age=180');
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { NoteReadService } from '@/core/NoteReadService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { AuthenticateService } from './AuthenticateService.js';
|
import { AuthenticateService } from './AuthenticateService.js';
|
||||||
import MainStreamConnection from './stream/index.js';
|
import MainStreamConnection from './stream/index.js';
|
||||||
import { ChannelsService } from './stream/ChannelsService.js';
|
import { ChannelsService } from './stream/ChannelsService.js';
|
||||||
|
@ -45,7 +46,7 @@ export class StreamingApiServerService {
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
private globalEventService: GlobalEventService,
|
private cacheService: CacheService,
|
||||||
private noteReadService: NoteReadService,
|
private noteReadService: NoteReadService,
|
||||||
private authenticateService: AuthenticateService,
|
private authenticateService: AuthenticateService,
|
||||||
private channelsService: ChannelsService,
|
private channelsService: ChannelsService,
|
||||||
|
@ -73,8 +74,6 @@ export class StreamingApiServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const connection = request.accept();
|
|
||||||
|
|
||||||
const ev = new EventEmitter();
|
const ev = new EventEmitter();
|
||||||
|
|
||||||
async function onRedisMessage(_: string, data: string): Promise<void> {
|
async function onRedisMessage(_: string, data: string): Promise<void> {
|
||||||
|
@ -85,19 +84,19 @@ export class StreamingApiServerService {
|
||||||
this.redisSubscriber.on('message', onRedisMessage);
|
this.redisSubscriber.on('message', onRedisMessage);
|
||||||
|
|
||||||
const main = new MainStreamConnection(
|
const main = new MainStreamConnection(
|
||||||
this.followingsRepository,
|
|
||||||
this.mutingsRepository,
|
|
||||||
this.renoteMutingsRepository,
|
|
||||||
this.blockingsRepository,
|
|
||||||
this.channelFollowingsRepository,
|
|
||||||
this.userProfilesRepository,
|
|
||||||
this.channelsService,
|
this.channelsService,
|
||||||
this.globalEventService,
|
|
||||||
this.noteReadService,
|
this.noteReadService,
|
||||||
this.notificationService,
|
this.notificationService,
|
||||||
connection, ev, user, miapp,
|
this.cacheService,
|
||||||
|
ev, user, miapp,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await main.init();
|
||||||
|
|
||||||
|
const connection = request.accept();
|
||||||
|
|
||||||
|
main.init2(connection);
|
||||||
|
|
||||||
const intervalId = user ? setInterval(() => {
|
const intervalId = user ? setInterval(() => {
|
||||||
this.usersRepository.update(user.id, {
|
this.usersRepository.update(user.id, {
|
||||||
lastActiveDate: new Date(),
|
lastActiveDate: new Date(),
|
||||||
|
|
|
@ -61,11 +61,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
await this.usersRepository.update(user.id, {
|
await this.usersRepository.update(user.id, {
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
|
||||||
// Terminate streaming
|
|
||||||
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,11 +62,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
targetId: user.id,
|
targetId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Terminate streaming
|
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
|
||||||
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
||||||
await this.unFollowAll(user).catch(e => {});
|
await this.unFollowAll(user).catch(e => {});
|
||||||
|
|
|
@ -41,7 +41,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const channel = await this.channelsRepository.findOneBy({
|
const channel = await this.channelsRepository.findOneBy({
|
||||||
|
@ -58,8 +57,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
followerId: me.id,
|
followerId: me.id,
|
||||||
followeeId: channel.id,
|
followeeId: channel.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventService.publishUserEvent(me.id, 'followChannel', channel);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,8 +38,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
@Inject(DI.channelFollowingsRepository)
|
@Inject(DI.channelFollowingsRepository)
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const channel = await this.channelsRepository.findOneBy({
|
const channel = await this.channelsRepository.findOneBy({
|
||||||
|
@ -54,8 +52,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
followerId: me.id,
|
followerId: me.id,
|
||||||
followeeId: channel.id,
|
followeeId: channel.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventService.publishUserEvent(me.id, 'unfollowChannel', channel);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,11 +54,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
// Publish event
|
// Publish event
|
||||||
this.globalEventService.publishInternalEvent('userTokenRegenerated', { id: me.id, oldToken, newToken });
|
this.globalEventService.publishInternalEvent('userTokenRegenerated', { id: me.id, oldToken, newToken });
|
||||||
this.globalEventService.publishMainStream(me.id, 'myTokenRegenerated');
|
this.globalEventService.publishMainStream(me.id, 'myTokenRegenerated');
|
||||||
|
|
||||||
// Terminate streaming
|
|
||||||
setTimeout(() => {
|
|
||||||
this.globalEventService.publishUserEvent(me.id, 'terminate', {});
|
|
||||||
}, 5000);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,9 +35,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
id: ps.tokenId,
|
id: ps.tokenId,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Terminate streaming
|
|
||||||
this.globalEventService.publishUserEvent(me.id, 'terminate');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -284,7 +284,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
// Publish meUpdated event
|
// Publish meUpdated event
|
||||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', iObj);
|
this.globalEventService.publishMainStream(user.id, 'meUpdated', iObj);
|
||||||
this.globalEventService.publishUserEvent(user.id, 'updateUserProfile', updatedProfile);
|
|
||||||
|
|
||||||
// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
|
// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
|
||||||
if (user.isLocked && ps.isLocked === false) {
|
if (user.isLocked && ps.isLocked === false) {
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
|
||||||
import type { MutingsRepository } from '@/models/index.js';
|
import type { MutingsRepository } from '@/models/index.js';
|
||||||
import type { Muting } from '@/models/entities/Muting.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { UserMutingService } from '@/core/UserMutingService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -63,10 +60,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
private idService: IdService,
|
private userMutingService: UserMutingService,
|
||||||
private cacheService: CacheService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const muter = me;
|
const muter = me;
|
||||||
|
@ -96,17 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create mute
|
await this.userMutingService.mute(muter, mutee, ps.expiresAt ? new Date(ps.expiresAt) : null);
|
||||||
await this.mutingsRepository.insert({
|
|
||||||
id: this.idService.genId(),
|
|
||||||
createdAt: new Date(),
|
|
||||||
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
|
|
||||||
muterId: muter.id,
|
|
||||||
muteeId: mutee.id,
|
|
||||||
} as Muting);
|
|
||||||
|
|
||||||
this.cacheService.userMutingsCache.delete(muter.id);
|
|
||||||
this.globalEventService.publishUserEvent(me.id, 'mute', mutee);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { MutingsRepository } from '@/models/index.js';
|
import type { MutingsRepository } from '@/models/index.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../error.js';
|
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
|
import { UserMutingService } from '@/core/UserMutingService.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['account'],
|
tags: ['account'],
|
||||||
|
@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
private globalEventService: GlobalEventService,
|
private userMutingService: UserMutingService,
|
||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
@ -76,12 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw new ApiError(meta.errors.notMuting);
|
throw new ApiError(meta.errors.notMuting);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete mute
|
await this.userMutingService.unmute([exist]);
|
||||||
await this.mutingsRepository.delete({
|
|
||||||
id: exist.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.globalEventService.publishUserEvent(me.id, 'unmute', mutee);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,8 +80,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
await this.renoteMutingsRepository.delete({
|
await this.renoteMutingsRepository.delete({
|
||||||
id: exist.id,
|
id: exist.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// publishUserEvent(user.id, 'unmute', mutee);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,16 +23,16 @@ export default abstract class Channel {
|
||||||
return this.connection.following;
|
return this.connection.following;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get muting() {
|
protected get userIdsWhoMeMuting() {
|
||||||
return this.connection.muting;
|
return this.connection.userIdsWhoMeMuting;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get renoteMuting() {
|
protected get userIdsWhoMeMutingRenotes() {
|
||||||
return this.connection.renoteMuting;
|
return this.connection.userIdsWhoMeMutingRenotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get blocking() {
|
protected get userIdsWhoBlockingMe() {
|
||||||
return this.connection.blocking;
|
return this.connection.userIdsWhoBlockingMe;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get followingChannels() {
|
protected get followingChannels() {
|
||||||
|
|
|
@ -35,11 +35,11 @@ class AntennaChannel extends Channel {
|
||||||
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
|
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
|
||||||
|
|
||||||
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
|
|
|
@ -47,11 +47,11 @@ class ChannelChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
|
||||||
|
|
||||||
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
|
|
|
@ -64,11 +64,11 @@ class GlobalTimelineChannel extends Channel {
|
||||||
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
|
||||||
|
|
||||||
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
|
|
@ -46,11 +46,11 @@ class HashtagChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
|
||||||
|
|
||||||
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ class HomeTimelineChannel extends Channel {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async init(params: any) {
|
public async init(params: any) {
|
||||||
// Subscribe events
|
|
||||||
this.subscriber.on('notesStream', this.onNote);
|
this.subscriber.on('notesStream', this.onNote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +37,7 @@ class HomeTimelineChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore notes from instances the user has muted
|
// Ignore notes from instances the user has muted
|
||||||
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
|
||||||
|
|
||||||
if (['followers', 'specified'].includes(note.visibility)) {
|
if (['followers', 'specified'].includes(note.visibility)) {
|
||||||
note = await this.noteEntityService.pack(note.id, this.user!, {
|
note = await this.noteEntityService.pack(note.id, this.user!, {
|
||||||
|
@ -71,18 +70,18 @@ class HomeTimelineChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
|
||||||
|
|
||||||
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
||||||
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
|
if (await checkWordMute(note, this.user, this.userProfile!.mutedWords)) return;
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ class HybridTimelineChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore notes from instances the user has muted
|
// Ignore notes from instances the user has muted
|
||||||
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
|
||||||
|
|
||||||
// 関係ない返信は除外
|
// 関係ない返信は除外
|
||||||
if (note.reply && !this.user!.showTimelineReplies) {
|
if (note.reply && !this.user!.showTimelineReplies) {
|
||||||
|
@ -82,11 +82,11 @@ class HybridTimelineChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
|
||||||
|
|
||||||
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
|
|
@ -61,11 +61,11 @@ class LocalTimelineChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
|
||||||
|
|
||||||
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
|
|
@ -26,7 +26,7 @@ class MainChannel extends Channel {
|
||||||
case 'notification': {
|
case 'notification': {
|
||||||
// Ignore notifications from instances the user has muted
|
// Ignore notifications from instances the user has muted
|
||||||
if (isUserFromMutedInstance(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
if (isUserFromMutedInstance(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||||
if (data.body.userId && this.muting.has(data.body.userId)) return;
|
if (data.body.userId && this.userIdsWhoMeMuting.has(data.body.userId)) return;
|
||||||
|
|
||||||
if (data.body.note && data.body.note.isHidden) {
|
if (data.body.note && data.body.note.isHidden) {
|
||||||
const note = await this.noteEntityService.pack(data.body.note.id, this.user, {
|
const note = await this.noteEntityService.pack(data.body.note.id, this.user, {
|
||||||
|
@ -40,7 +40,7 @@ class MainChannel extends Channel {
|
||||||
case 'mention': {
|
case 'mention': {
|
||||||
if (isInstanceMuted(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
if (isInstanceMuted(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||||
|
|
||||||
if (this.muting.has(data.body.userId)) return;
|
if (this.userIdsWhoMeMuting.has(data.body.userId)) return;
|
||||||
if (data.body.isHidden) {
|
if (data.body.isHidden) {
|
||||||
const note = await this.noteEntityService.pack(data.body.id, this.user, {
|
const note = await this.noteEntityService.pack(data.body.id, this.user, {
|
||||||
detail: true,
|
detail: true,
|
||||||
|
|
|
@ -89,11 +89,11 @@ class UserListChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
|
||||||
|
|
||||||
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import type { Channel as ChannelModel } from '@/models/entities/Channel.js';
|
|
||||||
import type { FollowingsRepository, MutingsRepository, RenoteMutingsRepository, UserProfilesRepository, ChannelFollowingsRepository, BlockingsRepository } from '@/models/index.js';
|
|
||||||
import type { AccessToken } from '@/models/entities/AccessToken.js';
|
import type { AccessToken } from '@/models/entities/AccessToken.js';
|
||||||
import type { UserProfile } from '@/models/entities/UserProfile.js';
|
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import type { NoteReadService } from '@/core/NoteReadService.js';
|
import type { NoteReadService } from '@/core/NoteReadService.js';
|
||||||
import type { NotificationService } from '@/core/NotificationService.js';
|
import type { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
import { UserProfile } from '@/models/index.js';
|
||||||
import type { ChannelsService } from './ChannelsService.js';
|
import type { ChannelsService } from './ChannelsService.js';
|
||||||
import type * as websocket from 'websocket';
|
import type * as websocket from 'websocket';
|
||||||
import type { EventEmitter } from 'events';
|
import type { EventEmitter } from 'events';
|
||||||
|
@ -19,106 +17,71 @@ import type { StreamEventEmitter, StreamMessages } from './types.js';
|
||||||
*/
|
*/
|
||||||
export default class Connection {
|
export default class Connection {
|
||||||
public user?: User;
|
public user?: User;
|
||||||
public userProfile?: UserProfile | null;
|
|
||||||
public following: Set<User['id']> = new Set();
|
|
||||||
public muting: Set<User['id']> = new Set();
|
|
||||||
public renoteMuting: Set<User['id']> = new Set();
|
|
||||||
public blocking: Set<User['id']> = new Set(); // "被"blocking
|
|
||||||
public followingChannels: Set<ChannelModel['id']> = new Set();
|
|
||||||
public token?: AccessToken;
|
public token?: AccessToken;
|
||||||
private wsConnection: websocket.connection;
|
private wsConnection: websocket.connection;
|
||||||
public subscriber: StreamEventEmitter;
|
public subscriber: StreamEventEmitter;
|
||||||
private channels: Channel[] = [];
|
private channels: Channel[] = [];
|
||||||
private subscribingNotes: any = {};
|
private subscribingNotes: any = {};
|
||||||
private cachedNotes: Packed<'Note'>[] = [];
|
private cachedNotes: Packed<'Note'>[] = [];
|
||||||
|
public userProfile: UserProfile | null = null;
|
||||||
|
public following: Set<string> = new Set();
|
||||||
|
public followingChannels: Set<string> = new Set();
|
||||||
|
public userIdsWhoMeMuting: Set<string> = new Set();
|
||||||
|
public userIdsWhoBlockingMe: Set<string> = new Set();
|
||||||
|
public userIdsWhoMeMutingRenotes: Set<string> = new Set();
|
||||||
|
private fetchIntervalId: NodeJS.Timer | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private followingsRepository: FollowingsRepository,
|
|
||||||
private mutingsRepository: MutingsRepository,
|
|
||||||
private renoteMutingsRepository: RenoteMutingsRepository,
|
|
||||||
private blockingsRepository: BlockingsRepository,
|
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
|
||||||
private channelsService: ChannelsService,
|
private channelsService: ChannelsService,
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
private noteReadService: NoteReadService,
|
private noteReadService: NoteReadService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
|
private cacheService: CacheService,
|
||||||
|
|
||||||
wsConnection: websocket.connection,
|
|
||||||
subscriber: EventEmitter,
|
subscriber: EventEmitter,
|
||||||
user: User | null | undefined,
|
user: User | null | undefined,
|
||||||
token: AccessToken | null | undefined,
|
token: AccessToken | null | undefined,
|
||||||
) {
|
) {
|
||||||
this.wsConnection = wsConnection;
|
|
||||||
this.subscriber = subscriber;
|
this.subscriber = subscriber;
|
||||||
if (user) this.user = user;
|
if (user) this.user = user;
|
||||||
if (token) this.token = token;
|
if (token) this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
//this.onWsConnectionMessage = this.onWsConnectionMessage.bind(this);
|
@bindThis
|
||||||
//this.onUserEvent = this.onUserEvent.bind(this);
|
public async fetch() {
|
||||||
//this.onNoteStreamMessage = this.onNoteStreamMessage.bind(this);
|
if (this.user == null) return;
|
||||||
//this.onBroadcastMessage = this.onBroadcastMessage.bind(this);
|
const [userProfile, following, followingChannels, userIdsWhoMeMuting, userIdsWhoBlockingMe, userIdsWhoMeMutingRenotes] = await Promise.all([
|
||||||
|
this.cacheService.userProfileCache.fetch(this.user.id),
|
||||||
|
this.cacheService.userFollowingsCache.fetch(this.user.id),
|
||||||
|
this.cacheService.userFollowingChannelsCache.fetch(this.user.id),
|
||||||
|
this.cacheService.userMutingsCache.fetch(this.user.id),
|
||||||
|
this.cacheService.userBlockedCache.fetch(this.user.id),
|
||||||
|
this.cacheService.renoteMutingsCache.fetch(this.user.id),
|
||||||
|
]);
|
||||||
|
this.userProfile = userProfile;
|
||||||
|
this.following = following;
|
||||||
|
this.followingChannels = followingChannels;
|
||||||
|
this.userIdsWhoMeMuting = userIdsWhoMeMuting;
|
||||||
|
this.userIdsWhoBlockingMe = userIdsWhoBlockingMe;
|
||||||
|
this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async init() {
|
||||||
|
if (this.user != null) {
|
||||||
|
await this.fetch();
|
||||||
|
|
||||||
|
this.fetchIntervalId = setInterval(this.fetch, 1000 * 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async init2(wsConnection: websocket.connection) {
|
||||||
|
this.wsConnection = wsConnection;
|
||||||
this.wsConnection.on('message', this.onWsConnectionMessage);
|
this.wsConnection.on('message', this.onWsConnectionMessage);
|
||||||
|
|
||||||
this.subscriber.on('broadcast', data => {
|
this.subscriber.on('broadcast', data => {
|
||||||
this.onBroadcastMessage(data);
|
this.onBroadcastMessage(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.user) {
|
|
||||||
this.updateFollowing();
|
|
||||||
this.updateMuting();
|
|
||||||
this.updateRenoteMuting();
|
|
||||||
this.updateBlocking();
|
|
||||||
this.updateFollowingChannels();
|
|
||||||
this.updateUserProfile();
|
|
||||||
|
|
||||||
this.subscriber.on(`user:${this.user.id}`, this.onUserEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private onUserEvent(data: StreamMessages['user']['payload']) { // { type, body }と展開するとそれぞれ型が分離してしまう
|
|
||||||
switch (data.type) {
|
|
||||||
case 'follow':
|
|
||||||
this.following.add(data.body.id);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'unfollow':
|
|
||||||
this.following.delete(data.body.id);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'mute':
|
|
||||||
this.muting.add(data.body.id);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'unmute':
|
|
||||||
this.muting.delete(data.body.id);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// TODO: renote mute events
|
|
||||||
// TODO: block events
|
|
||||||
|
|
||||||
case 'followChannel':
|
|
||||||
this.followingChannels.add(data.body.id);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'unfollowChannel':
|
|
||||||
this.followingChannels.delete(data.body.id);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'updateUserProfile':
|
|
||||||
this.userProfile = data.body;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'terminate':
|
|
||||||
this.wsConnection.close();
|
|
||||||
this.dispose();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -186,10 +149,7 @@ export default class Connection {
|
||||||
if (note == null) return;
|
if (note == null) return;
|
||||||
|
|
||||||
if (this.user && (note.userId !== this.user.id)) {
|
if (this.user && (note.userId !== this.user.id)) {
|
||||||
this.noteReadService.read(this.user.id, [note], {
|
this.noteReadService.read(this.user.id, [note]);
|
||||||
following: this.following,
|
|
||||||
followingChannels: this.followingChannels,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,78 +281,12 @@ export default class Connection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async updateFollowing() {
|
|
||||||
const followings = await this.followingsRepository.find({
|
|
||||||
where: {
|
|
||||||
followerId: this.user!.id,
|
|
||||||
},
|
|
||||||
select: ['followeeId'],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.following = new Set<string>(followings.map(x => x.followeeId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async updateMuting() {
|
|
||||||
const mutings = await this.mutingsRepository.find({
|
|
||||||
where: {
|
|
||||||
muterId: this.user!.id,
|
|
||||||
},
|
|
||||||
select: ['muteeId'],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.muting = new Set<string>(mutings.map(x => x.muteeId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async updateRenoteMuting() {
|
|
||||||
const renoteMutings = await this.renoteMutingsRepository.find({
|
|
||||||
where: {
|
|
||||||
muterId: this.user!.id,
|
|
||||||
},
|
|
||||||
select: ['muteeId'],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.renoteMuting = new Set<string>(renoteMutings.map(x => x.muteeId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async updateBlocking() { // ここでいうBlockingは被Blockingの意
|
|
||||||
const blockings = await this.blockingsRepository.find({
|
|
||||||
where: {
|
|
||||||
blockeeId: this.user!.id,
|
|
||||||
},
|
|
||||||
select: ['blockerId'],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.blocking = new Set<string>(blockings.map(x => x.blockerId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async updateFollowingChannels() {
|
|
||||||
const followings = await this.channelFollowingsRepository.find({
|
|
||||||
where: {
|
|
||||||
followerId: this.user!.id,
|
|
||||||
},
|
|
||||||
select: ['followeeId'],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.followingChannels = new Set<string>(followings.map(x => x.followeeId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async updateUserProfile() {
|
|
||||||
this.userProfile = await this.userProfilesRepository.findOneBy({
|
|
||||||
userId: this.user!.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ストリームが切れたとき
|
* ストリームが切れたとき
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose() {
|
public dispose() {
|
||||||
|
if (this.fetchIntervalId) clearInterval(this.fetchIntervalId);
|
||||||
for (const c of this.channels.filter(c => c.dispose)) {
|
for (const c of this.channels.filter(c => c.dispose)) {
|
||||||
if (c.dispose) c.dispose();
|
if (c.dispose) c.dispose();
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,11 @@ export interface InternalStreamTypes {
|
||||||
antennaDeleted: Antenna;
|
antennaDeleted: Antenna;
|
||||||
antennaUpdated: Antenna;
|
antennaUpdated: Antenna;
|
||||||
metaUpdated: Meta;
|
metaUpdated: Meta;
|
||||||
|
followChannel: { userId: User['id']; channelId: Channel['id']; };
|
||||||
|
unfollowChannel: { userId: User['id']; channelId: Channel['id']; };
|
||||||
|
updateUserProfile: UserProfile;
|
||||||
|
mute: { muterId: User['id']; muteeId: User['id']; };
|
||||||
|
unmute: { muterId: User['id']; muteeId: User['id']; };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BroadcastTypes {
|
export interface BroadcastTypes {
|
||||||
|
@ -56,18 +61,6 @@ export interface BroadcastTypes {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserStreamTypes {
|
|
||||||
terminate: Record<string, unknown>;
|
|
||||||
followChannel: Channel;
|
|
||||||
unfollowChannel: Channel;
|
|
||||||
updateUserProfile: UserProfile;
|
|
||||||
mute: User;
|
|
||||||
unmute: User;
|
|
||||||
follow: Packed<'UserDetailedNotMe'>;
|
|
||||||
unfollow: Packed<'User'>;
|
|
||||||
userAdded: Packed<'User'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MainStreamTypes {
|
export interface MainStreamTypes {
|
||||||
notification: Packed<'Notification'>;
|
notification: Packed<'Notification'>;
|
||||||
mention: Packed<'Note'>;
|
mention: Packed<'Note'>;
|
||||||
|
@ -97,8 +90,6 @@ export interface MainStreamTypes {
|
||||||
readAllAntennas: undefined;
|
readAllAntennas: undefined;
|
||||||
unreadAntenna: Antenna;
|
unreadAntenna: Antenna;
|
||||||
readAllAnnouncements: undefined;
|
readAllAnnouncements: undefined;
|
||||||
readAllChannels: undefined;
|
|
||||||
unreadChannel: Note['id'];
|
|
||||||
myTokenRegenerated: undefined;
|
myTokenRegenerated: undefined;
|
||||||
signin: Signin;
|
signin: Signin;
|
||||||
registryUpdated: {
|
registryUpdated: {
|
||||||
|
@ -202,10 +193,6 @@ export type StreamMessages = {
|
||||||
name: 'broadcast';
|
name: 'broadcast';
|
||||||
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
|
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
|
||||||
};
|
};
|
||||||
user: {
|
|
||||||
name: `user:${User['id']}`;
|
|
||||||
payload: EventUnionFromDictionary<SerializedAll<UserStreamTypes>>;
|
|
||||||
};
|
|
||||||
main: {
|
main: {
|
||||||
name: `mainStream:${User['id']}`;
|
name: `mainStream:${User['id']}`;
|
||||||
payload: EventUnionFromDictionary<SerializedAll<MainStreamTypes>>;
|
payload: EventUnionFromDictionary<SerializedAll<MainStreamTypes>>;
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="actions" :class="$style.buttons">
|
<div v-if="actions" :class="$style.buttons">
|
||||||
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); modal?.close(); }">{{ action.text }}</MkButton>
|
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" :danger="action.danger" @click="() => { action.callback(); modal?.close(); }">{{ action.text }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
|
@ -84,6 +84,7 @@ const props = withDefaults(defineProps<{
|
||||||
actions?: {
|
actions?: {
|
||||||
text: string;
|
text: string;
|
||||||
primary?: boolean,
|
primary?: boolean,
|
||||||
|
danger?: boolean,
|
||||||
callback: (...args: any[]) => void;
|
callback: (...args: any[]) => void;
|
||||||
}[];
|
}[];
|
||||||
showOkButton?: boolean;
|
showOkButton?: boolean;
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span>
|
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
||||||
<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
|
<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span>
|
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<article class="article" @contextmenu.stop="onContextmenu">
|
<article class="article" @contextmenu.stop="onContextmenu">
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i>
|
<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span>
|
<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="username"><MkAcct :user="appearNote.user"/></div>
|
<div class="username"><MkAcct :user="appearNote.user"/></div>
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span>
|
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
|
||||||
<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
|
<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<MkAvatar :class="$style.avatar" :user="$i" link preview/>
|
<MkAvatar :class="$style.avatar" :user="$i" link preview/>
|
||||||
<div :class="$style.main">
|
<div :class="$style.main">
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<MkUserName :user="$i"/>
|
<MkUserName :user="$i" :nowrap="true"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div :class="$style.content">
|
<div :class="$style.content">
|
||||||
|
@ -50,6 +50,9 @@ const props = defineProps<{
|
||||||
.header {
|
.header {
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
width: 100%;
|
||||||
|
overflow: clip;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (min-width: 350px) {
|
@container (min-width: 350px) {
|
||||||
|
|
|
@ -7,20 +7,35 @@
|
||||||
@drop.stop="onDrop"
|
@drop.stop="onDrop"
|
||||||
>
|
>
|
||||||
<header :class="$style.header">
|
<header :class="$style.header">
|
||||||
|
<div :class="$style.headerLeft">
|
||||||
<button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button>
|
<button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button>
|
||||||
<button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu">
|
<button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu">
|
||||||
<MkAvatar :user="postAccount ?? $i" :class="$style.avatar"/>
|
<MkAvatar :user="postAccount ?? $i" :class="$style.avatar"/>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
<div :class="$style.headerRight">
|
<div :class="$style.headerRight">
|
||||||
<span :class="[$style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</span>
|
<template v-if="!(channel != null && fixed)">
|
||||||
<span v-if="localOnly" :class="$style.localOnly"><i class="ti ti-world-off"></i></span>
|
<button v-if="channel == null" ref="visibilityButton" v-click-anime v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility">
|
||||||
<button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button" :class="$style.visibility" :disabled="channel != null" @click="setVisibility">
|
|
||||||
<span v-if="visibility === 'public'"><i class="ti ti-world"></i></span>
|
<span v-if="visibility === 'public'"><i class="ti ti-world"></i></span>
|
||||||
<span v-if="visibility === 'home'"><i class="ti ti-home"></i></span>
|
<span v-if="visibility === 'home'"><i class="ti ti-home"></i></span>
|
||||||
<span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span>
|
<span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span>
|
||||||
<span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span>
|
<span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span>
|
||||||
|
<span :class="$style.headerRightButtonText">{{ i18n.ts._visibility[visibility] }}</span>
|
||||||
|
</button>
|
||||||
|
<button v-else :class="['_button', $style.headerRightItem, $style.visibility]" disabled>
|
||||||
|
<span><i class="ti ti-device-tv"></i></span>
|
||||||
|
<span :class="$style.headerRightButtonText">{{ channel.name }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" :class="['_button', $style.headerRightItem, $style.localOnly, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly">
|
||||||
|
<span v-if="!localOnly"><i class="ti ti-rocket"></i></span>
|
||||||
|
<span v-else><i class="ti ti-rocket-off"></i></span>
|
||||||
|
</button>
|
||||||
|
<button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" :class="['_button', $style.headerRightItem, $style.reactionAcceptance, { [$style.danger]: reactionAcceptance }]" @click="toggleReactionAcceptance">
|
||||||
|
<span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span>
|
||||||
|
<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span>
|
||||||
|
<span v-else><i class="ti ti-icons"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.previewButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
|
|
||||||
<button v-click-anime class="_button" :class="[$style.submit, { [$style.submitPosting]: posting }]" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
|
<button v-click-anime class="_button" :class="[$style.submit, { [$style.submitPosting]: posting }]" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
|
||||||
<div :class="$style.submitInner">
|
<div :class="$style.submitInner">
|
||||||
<template v-if="posted"></template>
|
<template v-if="posted"></template>
|
||||||
|
@ -31,7 +46,6 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div :class="[$style.form]">
|
|
||||||
<MkNoteSimple v-if="reply" :class="$style.targetNote" :note="reply"/>
|
<MkNoteSimple v-if="reply" :class="$style.targetNote" :note="reply"/>
|
||||||
<MkNoteSimple v-if="renote" :class="$style.targetNote" :note="renote"/>
|
<MkNoteSimple v-if="renote" :class="$style.targetNote" :note="renote"/>
|
||||||
<div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div>
|
<div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div>
|
||||||
|
@ -45,36 +59,36 @@
|
||||||
<button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
|
<button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MkInfo v-if="localOnly && channel == null" warn :class="$style.disableFederationWarn">{{ i18n.ts.disableFederationWarn }}</MkInfo>
|
|
||||||
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
|
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
|
||||||
<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
|
<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
|
||||||
<textarea ref="textareaEl" v-model="text" :class="[$style.text, { [$style.withCw]: useCw }]" :disabled="posting || posted" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
|
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
|
||||||
|
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
|
||||||
|
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
||||||
|
</div>
|
||||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||||
<XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @change-sensitive="updateFileSensitive" @change-name="updateFileName"/>
|
<XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @change-sensitive="updateFileSensitive" @change-name="updateFileName"/>
|
||||||
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
||||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
|
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
|
||||||
<div v-if="showingOptions" style="padding: 0 16px;">
|
<div v-if="showingOptions" style="padding: 8px 16px;">
|
||||||
<MkSelect v-model="reactionAcceptance" small>
|
|
||||||
<template #label>{{ i18n.ts.reactionAcceptance }}</template>
|
|
||||||
<option :value="null">{{ i18n.ts.all }}</option>
|
|
||||||
<option value="likeOnly">{{ i18n.ts.likeOnly }}</option>
|
|
||||||
<option value="likeOnlyForRemote">{{ i18n.ts.likeOnlyForRemote }}</option>
|
|
||||||
</MkSelect>
|
|
||||||
</div>
|
</div>
|
||||||
<button v-tooltip="i18n.ts.emoji" class="_button" :class="$style.emojiButton" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
|
||||||
<footer :class="$style.footer">
|
<footer :class="$style.footer">
|
||||||
|
<div :class="$style.footerLeft">
|
||||||
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
|
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
|
||||||
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
|
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
|
||||||
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
||||||
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
||||||
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
||||||
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
|
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
|
||||||
<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ti ti-dots"></i></button>
|
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.footerRight">
|
||||||
|
<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
|
||||||
|
<!--<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ti ti-dots"></i></button>-->
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<datalist id="hashtags">
|
<datalist id="hashtags">
|
||||||
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
|
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
|
||||||
</datalist>
|
</datalist>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -85,7 +99,6 @@ import * as misskey from 'misskey-js';
|
||||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||||
import { toASCII } from 'punycode/';
|
import { toASCII } from 'punycode/';
|
||||||
import * as Acct from 'misskey-js/built/acct';
|
import * as Acct from 'misskey-js/built/acct';
|
||||||
import MkSelect from './MkSelect.vue';
|
|
||||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
||||||
|
@ -113,7 +126,7 @@ const modal = inject('modal');
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
reply?: misskey.entities.Note;
|
reply?: misskey.entities.Note;
|
||||||
renote?: misskey.entities.Note;
|
renote?: misskey.entities.Note;
|
||||||
channel?: any; // TODO
|
channel?: misskey.entities.Channel; // TODO
|
||||||
mention?: misskey.entities.User;
|
mention?: misskey.entities.User;
|
||||||
specified?: misskey.entities.User;
|
specified?: misskey.entities.User;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
|
@ -401,13 +414,14 @@ function upload(file: File, name?: string) {
|
||||||
|
|
||||||
function setVisibility() {
|
function setVisibility() {
|
||||||
if (props.channel) {
|
if (props.channel) {
|
||||||
// TODO: information dialog
|
visibility = 'public';
|
||||||
|
localOnly = true; // TODO: チャンネルが連合するようになった折には消す
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
|
os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
|
||||||
currentVisibility: visibility,
|
currentVisibility: visibility,
|
||||||
currentLocalOnly: localOnly,
|
localOnly: localOnly,
|
||||||
src: visibilityButton,
|
src: visibilityButton,
|
||||||
}, {
|
}, {
|
||||||
changeVisibility: v => {
|
changeVisibility: v => {
|
||||||
|
@ -416,15 +430,65 @@ function setVisibility() {
|
||||||
defaultStore.set('visibility', visibility);
|
defaultStore.set('visibility', visibility);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
changeLocalOnly: v => {
|
|
||||||
localOnly = v;
|
|
||||||
if (defaultStore.state.rememberNoteVisibility) {
|
|
||||||
defaultStore.set('localOnly', localOnly);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleLocalOnly() {
|
||||||
|
if (props.channel) {
|
||||||
|
visibility = 'public';
|
||||||
|
localOnly = true; // TODO: チャンネルが連合するようになった折には消す
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const neverShowInfo = miLocalStorage.getItem('neverShowLocalOnlyInfo');
|
||||||
|
|
||||||
|
if (!localOnly && neverShowInfo !== 'true') {
|
||||||
|
const confirm = await os.actions({
|
||||||
|
type: 'question',
|
||||||
|
title: i18n.ts.disableFederationConfirm,
|
||||||
|
text: i18n.ts.disableFederationConfirmWarn,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
value: 'yes' as const,
|
||||||
|
text: i18n.ts.disableFederationOk,
|
||||||
|
primary: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'neverShow' as const,
|
||||||
|
text: `${i18n.ts.disableFederationOk} (${i18n.ts.neverShow})`,
|
||||||
|
danger: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'no' as const,
|
||||||
|
text: i18n.ts.cancel,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (confirm.canceled) return;
|
||||||
|
if (confirm.result === 'no') return;
|
||||||
|
|
||||||
|
if (confirm.result === 'neverShow') {
|
||||||
|
miLocalStorage.setItem('neverShowLocalOnlyInfo', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localOnly = !localOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleReactionAcceptance() {
|
||||||
|
const select = await os.select({
|
||||||
|
title: i18n.ts.reactionAcceptance,
|
||||||
|
items: [
|
||||||
|
{ value: null, text: i18n.ts.all },
|
||||||
|
{ value: 'likeOnly' as const, text: i18n.ts.likeOnly },
|
||||||
|
{ value: 'likeOnlyForRemote' as const, text: i18n.ts.likeOnlyForRemote },
|
||||||
|
],
|
||||||
|
default: reactionAcceptance,
|
||||||
|
});
|
||||||
|
if (select.canceled) return;
|
||||||
|
reactionAcceptance = select.result;
|
||||||
|
}
|
||||||
|
|
||||||
function pushVisibleUser(user) {
|
function pushVisibleUser(user) {
|
||||||
if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) {
|
if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) {
|
||||||
visibleUsers.push(user);
|
visibleUsers.push(user);
|
||||||
|
@ -591,7 +655,8 @@ async function post(ev?: MouseEvent) {
|
||||||
text.includes('$[x4') ||
|
text.includes('$[x4') ||
|
||||||
text.includes('$[scale') ||
|
text.includes('$[scale') ||
|
||||||
text.includes('$[position');
|
text.includes('$[position');
|
||||||
if (annoying) {
|
|
||||||
|
if (annoying && visibility === 'public') {
|
||||||
const { canceled, result } = await os.actions({
|
const { canceled, result } = await os.actions({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
text: i18n.ts.thisPostMayBeAnnoying,
|
text: i18n.ts.thisPostMayBeAnnoying,
|
||||||
|
@ -817,6 +882,7 @@ defineExpose({
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.root {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
container-type: inline-size;
|
||||||
|
|
||||||
&.modal {
|
&.modal {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -824,21 +890,29 @@ defineExpose({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#region header
|
||||||
.header {
|
.header {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
height: 66px;
|
min-height: 50px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerLeft {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(36px, 50px));
|
||||||
|
grid-template-rows: minmax(40px, 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel {
|
.cancel {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
width: 64px;
|
height: 100%;
|
||||||
line-height: 66px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.account {
|
.account {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
aspect-ratio: 1/1;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
@ -846,55 +920,23 @@ defineExpose({
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
margin: auto;
|
margin: auto 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerRight {
|
.headerRight {
|
||||||
position: absolute;
|
display: flex;
|
||||||
top: 0;
|
min-height: 48px;
|
||||||
right: 0;
|
font-size: 0.9em;
|
||||||
}
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
.textCount {
|
margin-left: auto;
|
||||||
opacity: 0.7;
|
gap: 4px;
|
||||||
line-height: 66px;
|
overflow: clip;
|
||||||
}
|
padding-left: 4px;
|
||||||
|
|
||||||
.visibility {
|
|
||||||
height: 34px;
|
|
||||||
width: 34px;
|
|
||||||
margin: 0 0 0 8px;
|
|
||||||
|
|
||||||
& + .localOnly {
|
|
||||||
margin-left: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.localOnly {
|
|
||||||
margin: 0 0 0 12px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.previewButton {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 8px 0 0;
|
|
||||||
font-size: 16px;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
border-radius: 6px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--X5);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.previewButtonActive {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit {
|
.submit {
|
||||||
margin: 16px 16px 16px 0;
|
margin: 12px 12px 12px 6px;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
|
@ -923,16 +965,47 @@ defineExpose({
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.9em;
|
|
||||||
min-width: 90px;
|
min-width: 90px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: var(--fgOnAccent);
|
color: var(--fgOnAccent);
|
||||||
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
.headerRightItem {
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--X5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
color: #ff2a2a;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headerRightButtonText {
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility {
|
||||||
|
overflow: clip;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:enabled {
|
||||||
|
> .headerRightButtonText {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
.preview {
|
.preview {
|
||||||
padding: 16px 20px 0 20px;
|
padding: 16px 20px 0 20px;
|
||||||
}
|
}
|
||||||
|
@ -966,10 +1039,6 @@ defineExpose({
|
||||||
background: var(--X4);
|
background: var(--X4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.disableFederationWarn {
|
|
||||||
margin: 0 20px 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hasNotSpecifiedMentions {
|
.hasNotSpecifiedMentions {
|
||||||
margin: 0 20px 16px 20px;
|
margin: 0 20px 16px 20px;
|
||||||
}
|
}
|
||||||
|
@ -1011,18 +1080,61 @@ defineExpose({
|
||||||
border-top: solid 0.5px var(--divider);
|
border-top: solid 0.5px var(--divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.textOuter {
|
||||||
max-width: 100%;
|
width: 100%;
|
||||||
min-width: 100%;
|
position: relative;
|
||||||
min-height: 90px;
|
|
||||||
|
|
||||||
&.withCw {
|
&.withCw {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 90px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textCount {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 2px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-size: .9em;
|
||||||
|
color: var(--warn);
|
||||||
|
border-radius: 6px;
|
||||||
|
min-width: 1.6em;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&.textOver {
|
||||||
|
color: #ff2a2a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
|
display: flex;
|
||||||
padding: 0 16px 16px 16px;
|
padding: 0 16px 16px 16px;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerLeft {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(42px, 1fr));
|
||||||
|
grid-auto-rows: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerRight {
|
||||||
|
flex: 0.3;
|
||||||
|
margin-left: auto;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(42px, 1fr));
|
||||||
|
grid-auto-rows: 46px;
|
||||||
|
direction: rtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footerButton {
|
.footerButton {
|
||||||
|
@ -1030,8 +1142,8 @@ defineExpose({
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
width: 46px;
|
width: auto;
|
||||||
height: 46px;
|
height: 100%;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -1043,42 +1155,34 @@ defineExpose({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emojiButton {
|
.previewButtonActive {
|
||||||
position: absolute;
|
color: var(--accent);
|
||||||
top: 55px;
|
|
||||||
right: 13px;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1em;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (max-width: 500px) {
|
@container (max-width: 500px) {
|
||||||
.header {
|
.headerRight {
|
||||||
height: 50px;
|
font-size: .9em;
|
||||||
|
|
||||||
> .cancel {
|
|
||||||
width: 50px;
|
|
||||||
line-height: 50px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .headerRight {
|
.headerRightButtonText {
|
||||||
> .textCount {
|
display: none;
|
||||||
line-height: 50px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .submit {
|
.visibility {
|
||||||
margin: 8px;
|
overflow: initial;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
margin: 8px 8px 8px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toSpecified {
|
.toSpecified {
|
||||||
padding: 6px 16px;
|
padding: 6px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
padding: 16px 14px 0 14px;
|
||||||
|
}
|
||||||
.cw,
|
.cw,
|
||||||
.hashtags,
|
.hashtags,
|
||||||
.text {
|
.text {
|
||||||
|
@ -1094,11 +1198,13 @@ defineExpose({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (max-width: 310px) {
|
@container (max-width: 330px) {
|
||||||
.footerButton {
|
.headerRight {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -24,19 +24,19 @@ const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.d
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: any[];
|
modelValue: any[];
|
||||||
detachMediaFn: () => void;
|
detachMediaFn?: (id: string) => void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', value: any[]): void;
|
(ev: 'update:modelValue', value: any[]): void;
|
||||||
(ev: 'detach'): void;
|
(ev: 'detach', id: string): void;
|
||||||
(ev: 'changeSensitive'): void;
|
(ev: 'changeSensitive'): void;
|
||||||
(ev: 'changeName'): void;
|
(ev: 'changeName'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let menuShowing = false;
|
let menuShowing = false;
|
||||||
|
|
||||||
function detachMedia(id) {
|
function detachMedia(id: string) {
|
||||||
if (props.detachMediaFn) {
|
if (props.detachMediaFn) {
|
||||||
props.detachMediaFn(id);
|
props.detachMediaFn(id);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
|
<MkModal ref="modal" v-slot="{ type }" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
|
||||||
<div class="_popup" :class="$style.root">
|
<div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }">
|
||||||
|
<div :class="[$style.label, $style.item]">
|
||||||
|
{{ i18n.ts.visibility }}
|
||||||
|
</div>
|
||||||
<button key="public" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')">
|
<button key="public" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')">
|
||||||
<div :class="$style.icon"><i class="ti ti-world"></i></div>
|
<div :class="$style.icon"><i class="ti ti-world"></i></div>
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
|
@ -29,21 +32,12 @@
|
||||||
<span :class="$style.itemDescription">{{ i18n.ts._visibility.specifiedDescription }}</span>
|
<span :class="$style.itemDescription">{{ i18n.ts._visibility.specifiedDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div :class="$style.divider"></div>
|
|
||||||
<button key="localOnly" class="_button" :class="[$style.item, $style.localOnly, { [$style.active]: localOnly }]" data-index="5" @click="localOnly = !localOnly">
|
|
||||||
<div :class="$style.icon"><i class="ti ti-world-off"></i></div>
|
|
||||||
<div :class="$style.body">
|
|
||||||
<span :class="$style.itemTitle">{{ i18n.ts._visibility.disableFederation }}</span>
|
|
||||||
<span :class="$style.itemDescription">{{ i18n.ts._visibility.disableFederationDescription }}</span>
|
|
||||||
</div>
|
|
||||||
<div :class="$style.toggle"><i :class="localOnly ? 'ti ti-toggle-right' : 'ti ti-toggle-left'"></i></div>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, watch } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
@ -52,42 +46,58 @@ const modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
currentVisibility: typeof misskey.noteVisibilities[number];
|
currentVisibility: typeof misskey.noteVisibilities[number];
|
||||||
currentLocalOnly: boolean;
|
localOnly: boolean;
|
||||||
src?: HTMLElement;
|
src?: HTMLElement;
|
||||||
}>(), {
|
}>(), {
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'changeVisibility', v: typeof misskey.noteVisibilities[number]): void;
|
(ev: 'changeVisibility', v: typeof misskey.noteVisibilities[number]): void;
|
||||||
(ev: 'changeLocalOnly', v: boolean): void;
|
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let v = $ref(props.currentVisibility);
|
let v = $ref(props.currentVisibility);
|
||||||
let localOnly = $ref(props.currentLocalOnly);
|
|
||||||
|
|
||||||
watch($$(localOnly), () => {
|
|
||||||
emit('changeLocalOnly', localOnly);
|
|
||||||
});
|
|
||||||
|
|
||||||
function choose(visibility: typeof misskey.noteVisibilities[number]): void {
|
function choose(visibility: typeof misskey.noteVisibilities[number]): void {
|
||||||
v = visibility;
|
v = visibility;
|
||||||
emit('changeVisibility', visibility);
|
emit('changeVisibility', visibility);
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
modal.close();
|
if (modal) modal.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.root {
|
||||||
width: 240px;
|
min-width: 240px;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
|
|
||||||
|
&.asDrawer {
|
||||||
|
padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 24px;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 12px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.label {
|
||||||
margin: 8px 0;
|
pointer-events: none;
|
||||||
border-top: solid 0.5px var(--divider);
|
font-size: 10px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
|
@ -107,13 +117,7 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: var(--fgOnAccent);
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.localOnly.active {
|
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background: inherit;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,16 +148,4 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
|
||||||
.itemDescription {
|
.itemDescription {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 10px;
|
|
||||||
width: 16px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -513,15 +513,6 @@ if ($i) {
|
||||||
updateAccount({ hasUnreadAnnouncement: false });
|
updateAccount({ hasUnreadAnnouncement: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
main.on('readAllChannels', () => {
|
|
||||||
updateAccount({ hasUnreadChannel: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
main.on('unreadChannel', () => {
|
|
||||||
updateAccount({ hasUnreadChannel: true });
|
|
||||||
sound.play('channel');
|
|
||||||
});
|
|
||||||
|
|
||||||
// トークンが再生成されたとき
|
// トークンが再生成されたとき
|
||||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
||||||
main.on('myTokenRegenerated', () => {
|
main.on('myTokenRegenerated', () => {
|
||||||
|
|
|
@ -6,6 +6,7 @@ type Keys =
|
||||||
'accounts' |
|
'accounts' |
|
||||||
'latestDonationInfoShownAt' |
|
'latestDonationInfoShownAt' |
|
||||||
'neverShowDonationInfo' |
|
'neverShowDonationInfo' |
|
||||||
|
'neverShowLocalOnlyInfo' |
|
||||||
'lastUsed' |
|
'lastUsed' |
|
||||||
'lang' |
|
'lang' |
|
||||||
'drafts' |
|
'drafts' |
|
||||||
|
|
|
@ -215,6 +215,7 @@ export function actions<T extends {
|
||||||
value: string;
|
value: string;
|
||||||
text: string;
|
text: string;
|
||||||
primary?: boolean,
|
primary?: boolean,
|
||||||
|
danger?: boolean,
|
||||||
}[]>(props: {
|
}[]>(props: {
|
||||||
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
|
@ -229,6 +230,7 @@ export function actions<T extends {
|
||||||
actions: props.actions.map(a => ({
|
actions: props.actions.map(a => ({
|
||||||
text: a.text,
|
text: a.text,
|
||||||
primary: a.primary,
|
primary: a.primary,
|
||||||
|
danger: a.danger,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
resolve({ canceled: false, result: a.value });
|
resolve({ canceled: false, result: a.value });
|
||||||
},
|
},
|
||||||
|
|
|
@ -98,9 +98,7 @@ function edit() {
|
||||||
|
|
||||||
function openPostForm() {
|
function openPostForm() {
|
||||||
os.post({
|
os.post({
|
||||||
channel: {
|
channel,
|
||||||
id: channel.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ import * as os from '@/os';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
user: misskey.entities.UserDetailed;
|
user: misskey.entities.UserDetailed;
|
||||||
|
|
|
@ -88,7 +88,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
},
|
},
|
||||||
reactionAcceptance: {
|
reactionAcceptance: {
|
||||||
where: 'account',
|
where: 'account',
|
||||||
default: null,
|
default: null as 'likeOnly' | 'likeOnlyForRemote' | null,
|
||||||
},
|
},
|
||||||
mutedWords: {
|
mutedWords: {
|
||||||
where: 'account',
|
where: 'account',
|
||||||
|
|
|
@ -14,13 +14,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
|
||||||
import XColumn from './column.vue';
|
import XColumn from './column.vue';
|
||||||
import { updateColumn, Column } from './deck-store';
|
import { updateColumn, Column } from './deck-store';
|
||||||
import MkTimeline from '@/components/MkTimeline.vue';
|
import MkTimeline from '@/components/MkTimeline.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
import * as misskey from 'misskey-js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
column: Column;
|
column: Column;
|
||||||
|
@ -33,6 +33,7 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
|
let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
|
||||||
|
let channel = $shallowRef<misskey.entities.Channel>();
|
||||||
|
|
||||||
if (props.column.channelId == null) {
|
if (props.column.channelId == null) {
|
||||||
setChannel();
|
setChannel();
|
||||||
|
@ -56,11 +57,15 @@ async function setChannel() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function post() {
|
async function post() {
|
||||||
|
if (!channel || channel.id !== props.column.channelId) {
|
||||||
|
channel = await os.api('channels/show', {
|
||||||
|
channelId: props.column.channelId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
os.post({
|
os.post({
|
||||||
channel: {
|
channel,
|
||||||
id: props.column.channelId,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,31 @@ import { describe, test, assert, afterEach } from 'vitest';
|
||||||
import { render, cleanup, type RenderResult } from '@testing-library/vue';
|
import { render, cleanup, type RenderResult } from '@testing-library/vue';
|
||||||
import './init';
|
import './init';
|
||||||
import type { DriveFile } from 'misskey-js/built/entities';
|
import type { DriveFile } from 'misskey-js/built/entities';
|
||||||
|
import { components } from '@/components';
|
||||||
import { directives } from '@/directives';
|
import { directives } from '@/directives';
|
||||||
import MkMediaImage from '@/components/MkMediaImage.vue';
|
import MkMediaImage from '@/components/MkMediaImage.vue';
|
||||||
|
|
||||||
describe('MkMediaImage', () => {
|
describe('MkMediaImage', () => {
|
||||||
const renderMediaImage = (image: Partial<DriveFile>): RenderResult => {
|
const renderMediaImage = (image: Partial<DriveFile>): RenderResult => {
|
||||||
return render(MkMediaImage, {
|
return render(MkMediaImage, {
|
||||||
props: { image },
|
props: {
|
||||||
global: { directives },
|
image: {
|
||||||
|
id: 'xxxxxxxx',
|
||||||
|
createdAt: (new Date()).toJSON(),
|
||||||
|
isSensitive: false,
|
||||||
|
name: 'example.png',
|
||||||
|
thumbnailUrl: null,
|
||||||
|
url: '',
|
||||||
|
type: 'application/octet-stream',
|
||||||
|
size: 1,
|
||||||
|
md5: '15eca7fba0480996e2245f5185bf39f2',
|
||||||
|
blurhash: null,
|
||||||
|
comment: null,
|
||||||
|
properties: {},
|
||||||
|
...image,
|
||||||
|
} as DriveFile,
|
||||||
|
},
|
||||||
|
global: { directives, components },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { describe, test, assert, afterEach } from 'vitest';
|
||||||
import { render, cleanup, type RenderResult } from '@testing-library/vue';
|
import { render, cleanup, type RenderResult } from '@testing-library/vue';
|
||||||
import './init';
|
import './init';
|
||||||
import type { summaly } from 'summaly';
|
import type { summaly } from 'summaly';
|
||||||
|
import { components } from '@/components';
|
||||||
import { directives } from '@/directives';
|
import { directives } from '@/directives';
|
||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ describe('MkMediaImage', () => {
|
||||||
|
|
||||||
const result = render(MkUrlPreview, {
|
const result = render(MkUrlPreview, {
|
||||||
props: { url: summary.url },
|
props: { url: summary.url },
|
||||||
global: { directives },
|
global: { directives, components },
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise<void>(resolve => {
|
await new Promise<void>(resolve => {
|
||||||
|
|
|
@ -88,7 +88,6 @@ export type MeDetailed = UserDetailed & {
|
||||||
hasPendingReceivedFollowRequest: boolean;
|
hasPendingReceivedFollowRequest: boolean;
|
||||||
hasUnreadAnnouncement: boolean;
|
hasUnreadAnnouncement: boolean;
|
||||||
hasUnreadAntenna: boolean;
|
hasUnreadAntenna: boolean;
|
||||||
hasUnreadChannel: boolean;
|
|
||||||
hasUnreadMentions: boolean;
|
hasUnreadMentions: boolean;
|
||||||
hasUnreadMessagingMessage: boolean;
|
hasUnreadMessagingMessage: boolean;
|
||||||
hasUnreadNotification: boolean;
|
hasUnreadNotification: boolean;
|
||||||
|
|
|
@ -28,8 +28,6 @@ export type Channels = {
|
||||||
readAllAntennas: () => void;
|
readAllAntennas: () => void;
|
||||||
unreadAntenna: (payload: Antenna) => void;
|
unreadAntenna: (payload: Antenna) => void;
|
||||||
readAllAnnouncements: () => void;
|
readAllAnnouncements: () => void;
|
||||||
readAllChannels: () => void;
|
|
||||||
unreadChannel: (payload: Note['id']) => void;
|
|
||||||
myTokenRegenerated: () => void;
|
myTokenRegenerated: () => void;
|
||||||
reversiNoInvites: () => void;
|
reversiNoInvites: () => void;
|
||||||
reversiInvited: (payload: FIXME) => void;
|
reversiInvited: (payload: FIXME) => void;
|
||||||
|
|
Loading…
Reference in New Issue