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
|
||||
|
||||
|
|
|
@ -961,7 +961,9 @@ copyErrorInfo: "エラー情報をコピー"
|
|||
joinThisServer: "このサーバーに登録する"
|
||||
exploreOtherServers: "他のサーバーを探す"
|
||||
letsLookAtTimeline: "タイムラインを見てみる"
|
||||
disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。"
|
||||
disableFederationConfirm: "連合なしにしますか?"
|
||||
disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。"
|
||||
disableFederationOk: "連合なしにする"
|
||||
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
|
||||
emailNotSupported: "このサーバーではメール配信はサポートされていません"
|
||||
postToTheChannel: "チャンネルに投稿"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
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 type { LocalUser, User } from '@/models/entities/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
@ -16,7 +16,12 @@ export class CacheService implements OnApplicationShutdown {
|
|||
public localUserByIdCache: MemoryKVCache<LocalUser>;
|
||||
public uriPersonCache: MemoryKVCache<User | null>;
|
||||
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(
|
||||
@Inject(DI.redis)
|
||||
|
@ -28,6 +33,24 @@ export class CacheService implements OnApplicationShutdown {
|
|||
@Inject(DI.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,
|
||||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
|
@ -36,8 +59,62 @@ export class CacheService implements OnApplicationShutdown {
|
|||
this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity);
|
||||
this.localUserByIdCache = new MemoryKVCache<LocalUser>(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);
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService
|
|||
import { UserBlockingService } from './UserBlockingService.js';
|
||||
import { CacheService } from './CacheService.js';
|
||||
import { UserFollowingService } from './UserFollowingService.js';
|
||||
import { UserKeypairStoreService } from './UserKeypairStoreService.js';
|
||||
import { UserKeypairService } from './UserKeypairService.js';
|
||||
import { UserListService } from './UserListService.js';
|
||||
import { UserMutingService } from './UserMutingService.js';
|
||||
import { UserSuspendService } from './UserSuspendService.js';
|
||||
|
@ -161,7 +161,7 @@ const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthentic
|
|||
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
|
||||
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
|
||||
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 $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
||||
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||
|
@ -284,7 +284,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
UserBlockingService,
|
||||
CacheService,
|
||||
UserFollowingService,
|
||||
UserKeypairStoreService,
|
||||
UserKeypairService,
|
||||
UserListService,
|
||||
UserMutingService,
|
||||
UserSuspendService,
|
||||
|
@ -401,7 +401,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$UserBlockingService,
|
||||
$CacheService,
|
||||
$UserFollowingService,
|
||||
$UserKeypairStoreService,
|
||||
$UserKeypairService,
|
||||
$UserListService,
|
||||
$UserMutingService,
|
||||
$UserSuspendService,
|
||||
|
@ -519,7 +519,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
UserBlockingService,
|
||||
CacheService,
|
||||
UserFollowingService,
|
||||
UserKeypairStoreService,
|
||||
UserKeypairService,
|
||||
UserListService,
|
||||
UserMutingService,
|
||||
UserSuspendService,
|
||||
|
@ -635,7 +635,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$UserBlockingService,
|
||||
$CacheService,
|
||||
$UserFollowingService,
|
||||
$UserKeypairStoreService,
|
||||
$UserKeypairService,
|
||||
$UserListService,
|
||||
$UserMutingService,
|
||||
$UserSuspendService,
|
||||
|
|
|
@ -36,8 +36,5 @@ export class DeleteAccountService {
|
|||
await this.usersRepository.update(user.id, {
|
||||
isDeleted: true,
|
||||
});
|
||||
|
||||
// Terminate streaming
|
||||
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import type {
|
|||
MainStreamTypes,
|
||||
NoteStreamTypes,
|
||||
UserListStreamTypes,
|
||||
UserStreamTypes,
|
||||
} from '@/server/api/stream/types.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
@ -49,11 +48,6 @@ export class GlobalEventService {
|
|||
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
|
||||
public publishBroadcastStream<K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void {
|
||||
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) {
|
||||
this.saveReply(data.reply, note);
|
||||
}
|
||||
|
|
|
@ -1,28 +1,20 @@
|
|||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { In, IsNull, Not } from 'typeorm';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.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 { Note } from '@/models/entities/Note.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository } from '@/models/index.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { NotificationService } from './NotificationService.js';
|
||||
import { AntennaService } from './AntennaService.js';
|
||||
import { PushNotificationService } from './PushNotificationService.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteReadService implements OnApplicationShutdown {
|
||||
#shutdownController = new AbortController();
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.noteUnreadsRepository)
|
||||
private noteUnreadsRepository: NoteUnreadsRepository,
|
||||
|
||||
|
@ -32,18 +24,8 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
@Inject(DI.noteThreadMutingsRepository)
|
||||
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.channelFollowingsRepository)
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private notificationService: NotificationService,
|
||||
private antennaService: AntennaService,
|
||||
private pushNotificationService: PushNotificationService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -54,7 +36,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
isMentioned: boolean;
|
||||
}): Promise<void> {
|
||||
//#region ミュートしているなら無視
|
||||
// TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする
|
||||
const mute = await this.mutingsRepository.findBy({
|
||||
muterId: userId,
|
||||
});
|
||||
|
@ -74,7 +55,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
userId: userId,
|
||||
isSpecified: params.isSpecified,
|
||||
isMentioned: params.isMentioned,
|
||||
noteChannelId: note.channelId,
|
||||
noteUserId: note.userId,
|
||||
};
|
||||
|
||||
|
@ -92,9 +72,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
if (params.isSpecified) {
|
||||
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
|
||||
}
|
||||
if (note.channelId) {
|
||||
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
|
||||
}
|
||||
}, () => { /* aborted, ignore it */ });
|
||||
}
|
||||
|
||||
|
@ -102,22 +79,9 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
public async read(
|
||||
userId: User['id'],
|
||||
notes: (Note | Packed<'Note'>)[],
|
||||
info?: {
|
||||
following: Set<User['id']>;
|
||||
followingChannels: Set<Channel['id']>;
|
||||
},
|
||||
): 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 readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
|
||||
const readChannelNotes: (Note | Packed<'Note'>)[] = [];
|
||||
|
||||
for (const note of notes) {
|
||||
if (note.mentions && note.mentions.includes(userId)) {
|
||||
|
@ -125,17 +89,13 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
|
||||
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
|
||||
await this.noteUnreadsRepository.delete({
|
||||
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: ↓まとめてクエリしたい
|
||||
|
@ -159,16 +119,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
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'],
|
||||
data: Partial<Notification>,
|
||||
): 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);
|
||||
if (isMuted) return null;
|
||||
|
||||
|
@ -82,8 +82,8 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
return null;
|
||||
}
|
||||
|
||||
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId, () => this.mutingsRepository.findBy({ muterId: notifieeId }).then(xs => xs.map(x => x.muteeId)));
|
||||
if (mutings.includes(data.notifierId)) {
|
||||
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
|
||||
if (mutings.has(data.notifierId)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -158,6 +158,7 @@ export class ReactionService {
|
|||
// カスタム絵文字リアクションだったら絵文字情報も送る
|
||||
const decodedReaction = this.decodeReaction(reaction);
|
||||
|
||||
// TODO: Cache
|
||||
const emoji = await this.emojisRepository.findOne({
|
||||
where: {
|
||||
name: decodedReaction.name,
|
||||
|
|
|
@ -1,40 +1,30 @@
|
|||
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import type { Blocking } from '@/models/entities/Blocking.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { WebhookService } from '@/core/WebhookService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MemoryKVCache } from '@/misc/cache.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserBlockingService implements OnApplicationShutdown {
|
||||
export class UserBlockingService implements OnModuleInit {
|
||||
private logger: Logger;
|
||||
|
||||
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
|
||||
private blockingsByUserIdCache: MemoryKVCache<User['id'][]>;
|
||||
private userFollowingService: UserFollowingService;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
@Inject(DI.followRequestsRepository)
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
|
@ -47,47 +37,20 @@ export class UserBlockingService implements OnApplicationShutdown {
|
|||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private webhookService: WebhookService,
|
||||
private apRendererService: ApRendererService,
|
||||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('user-block');
|
||||
|
||||
this.blockingsByUserIdCache = new MemoryKVCache<User['id'][]>(Infinity);
|
||||
|
||||
this.redisSubscriber.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
onModuleInit() {
|
||||
this.userFollowingService = this.moduleRef.get('UserFollowingService');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -95,8 +58,8 @@ export class UserBlockingService implements OnApplicationShutdown {
|
|||
await Promise.all([
|
||||
this.cancelRequest(blocker, blockee),
|
||||
this.cancelRequest(blockee, blocker),
|
||||
this.unFollow(blocker, blockee),
|
||||
this.unFollow(blockee, blocker),
|
||||
this.userFollowingService.unfollow(blocker, blockee),
|
||||
this.userFollowingService.unfollow(blockee, blocker),
|
||||
this.removeFromList(blockee, blocker),
|
||||
]);
|
||||
|
||||
|
@ -111,6 +74,9 @@ export class UserBlockingService implements OnApplicationShutdown {
|
|||
|
||||
await this.blockingsRepository.insert(blocking);
|
||||
|
||||
this.cacheService.userBlockingCache.refresh(blocker.id);
|
||||
this.cacheService.userBlockedCache.refresh(blockee.id);
|
||||
|
||||
this.globalEventService.publishInternalEvent('blockingCreated', {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
|
@ -148,7 +114,6 @@ export class UserBlockingService implements OnApplicationShutdown {
|
|||
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'));
|
||||
|
@ -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
|
||||
private async removeFromList(listOwner: User, user: User) {
|
||||
const userLists = await this.userListsRepository.findBy({
|
||||
|
@ -254,6 +171,9 @@ export class UserBlockingService implements OnApplicationShutdown {
|
|||
|
||||
await this.blockingsRepository.delete(blocking.id);
|
||||
|
||||
this.cacheService.userBlockingCache.refresh(blocker.id);
|
||||
this.cacheService.userBlockedCache.refresh(blockee.id);
|
||||
|
||||
this.globalEventService.publishInternalEvent('blockingDeleted', {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
|
@ -268,17 +188,6 @@ export class UserBlockingService implements OnApplicationShutdown {
|
|||
|
||||
@bindThis
|
||||
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
|
||||
const blockedUserIds = await this.blockingsByUserIdCache.fetch(blockerId, () => this.blockingsRepository.find({
|
||||
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);
|
||||
return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
@ -18,6 +19,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import Logger from '../logger.js';
|
||||
|
||||
const logger = new Logger('following/create');
|
||||
|
@ -36,8 +38,12 @@ type Remote = RemoteUser | {
|
|||
type Both = Local | Remote;
|
||||
|
||||
@Injectable()
|
||||
export class UserFollowingService {
|
||||
export class UserFollowingService implements OnModuleInit {
|
||||
private userBlockingService: UserBlockingService;
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
|
@ -53,8 +59,8 @@ export class UserFollowingService {
|
|||
@Inject(DI.instancesRepository)
|
||||
private instancesRepository: InstancesRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private userEntityService: UserEntityService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
|
@ -68,6 +74,10 @@ export class UserFollowingService {
|
|||
) {
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
this.userBlockingService = this.moduleRef.get('UserBlockingService');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
|
||||
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({
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
|
@ -225,7 +237,6 @@ export class UserFollowingService {
|
|||
this.userEntityService.pack(followee.id, follower, {
|
||||
detail: true,
|
||||
}).then(async packed => {
|
||||
this.globalEventService.publishUserEvent(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'));
|
||||
|
@ -279,6 +290,8 @@ export class UserFollowingService {
|
|||
|
||||
await this.followingsRepository.delete(following.id);
|
||||
|
||||
this.cacheService.userFollowingsCache.refresh(follower.id);
|
||||
|
||||
this.decrementFollowing(follower, followee);
|
||||
|
||||
// Publish unfollow event
|
||||
|
@ -286,7 +299,6 @@ export class UserFollowingService {
|
|||
this.userEntityService.pack(followee.id, 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'));
|
||||
|
@ -579,7 +591,6 @@ export class UserFollowingService {
|
|||
detail: true,
|
||||
});
|
||||
|
||||
this.globalEventService.publishUserEvent(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'));
|
||||
|
|
|
@ -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 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 { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserMutingService {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
}
|
||||
|
||||
@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({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: expiresAt ?? null,
|
||||
muterId: user.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 { Poll } from '@/models/entities/Poll.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
|
@ -53,7 +53,7 @@ export class ApRendererService {
|
|||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private ldSignatureService: LdSignatureService,
|
||||
private userKeypairStoreService: UserKeypairStoreService,
|
||||
private userKeypairService: UserKeypairService,
|
||||
private apMfmService: ApMfmService,
|
||||
private mfmService: MfmService,
|
||||
) {
|
||||
|
@ -473,7 +473,7 @@ export class ApRendererService {
|
|||
...hashtagTags,
|
||||
];
|
||||
|
||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||
|
||||
const person = {
|
||||
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
||||
|
@ -640,7 +640,7 @@ export class ApRendererService {
|
|||
|
||||
@bindThis
|
||||
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();
|
||||
ldSignature.debug = false;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.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 { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
@ -131,7 +131,7 @@ export class ApRequestService {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
private userKeypairStoreService: UserKeypairStoreService,
|
||||
private userKeypairService: UserKeypairService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
|
@ -143,7 +143,7 @@ export class ApRequestService {
|
|||
public async signedPost(user: { id: User['id'] }, url: string, object: any) {
|
||||
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({
|
||||
key: {
|
||||
|
@ -170,7 +170,7 @@ export class ApRequestService {
|
|||
*/
|
||||
@bindThis
|
||||
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({
|
||||
key: {
|
||||
|
|
|
@ -234,18 +234,6 @@ export class UserEntityService implements OnModuleInit {
|
|||
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
|
||||
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
||||
|
@ -463,7 +451,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
}).then(count => count > 0),
|
||||
hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
|
||||
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
||||
hasUnreadChannel: this.getHasUnreadChannel(user.id),
|
||||
hasUnreadChannel: false, // 後方互換性のため
|
||||
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||
mutedWords: profile!.mutedWords,
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
import Redis from 'ioredis';
|
||||
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> {
|
||||
private redisClient: Redis.Redis;
|
||||
private name: string;
|
||||
private lifetime: number;
|
||||
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.name = name;
|
||||
this.lifetime = lifetime;
|
||||
this.memoryCache = new MemoryKVCache(memoryCacheLifetime);
|
||||
this.lifetime = opts.lifetime;
|
||||
this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime);
|
||||
this.fetcher = opts.fetcher;
|
||||
this.toRedisConverter = opts.toRedisConverter;
|
||||
this.fromRedisConverter = opts.fromRedisConverter;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -32,25 +32,25 @@ export class RedisKVCache<T> {
|
|||
if (this.lifetime === Infinity) {
|
||||
await this.redisClient.set(
|
||||
`kvcache:${this.name}:${key}`,
|
||||
JSON.stringify(value),
|
||||
this.toRedisConverter(value),
|
||||
);
|
||||
} else {
|
||||
await this.redisClient.set(
|
||||
`kvcache:${this.name}:${key}`,
|
||||
JSON.stringify(value),
|
||||
this.toRedisConverter(value),
|
||||
'ex', Math.round(this.lifetime / 1000),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
if (memoryCached !== undefined) return memoryCached;
|
||||
|
||||
const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`);
|
||||
if (cached == null) return undefined;
|
||||
return JSON.parse(cached);
|
||||
return this.fromRedisConverter(cached);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -60,29 +60,29 @@ export class RedisKVCache<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||
*/
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
*/
|
||||
@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);
|
||||
if (cachedValue !== undefined) {
|
||||
if (validator) {
|
||||
if (validator(cachedValue)) {
|
||||
// Cache HIT
|
||||
return cachedValue;
|
||||
}
|
||||
} else {
|
||||
// Cache HIT
|
||||
return cachedValue;
|
||||
}
|
||||
// Cache HIT
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
// Cache MISS
|
||||
const value = await fetcher();
|
||||
const value = await this.fetcher(key);
|
||||
this.set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async refresh(key: string) {
|
||||
const value = await this.fetcher(key);
|
||||
this.set(key, value);
|
||||
|
||||
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
||||
|
|
|
@ -311,10 +311,6 @@ export const packedMeDetailedOnlySchema = {
|
|||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
hasUnreadChannel: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
hasUnreadNotification: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
|
|
|
@ -4,10 +4,10 @@ import { DI } from '@/di-symbols.js';
|
|||
import type { MutingsRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.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 type Bull from 'bull';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class CheckExpiredMutingsProcessorService {
|
||||
|
@ -20,7 +20,7 @@ export class CheckExpiredMutingsProcessorService {
|
|||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
private userMutingService: UserMutingService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('check-expired-mutings');
|
||||
|
@ -37,13 +37,7 @@ export class CheckExpiredMutingsProcessorService {
|
|||
.getMany();
|
||||
|
||||
if (expired.length > 0) {
|
||||
await this.mutingsRepository.delete({
|
||||
id: In(expired.map(m => m.id)),
|
||||
});
|
||||
|
||||
for (const m of expired) {
|
||||
this.globalEventService.publishUserEvent(m.muterId, 'unmute', m.mutee!);
|
||||
}
|
||||
await this.userMutingService.unmute(expired);
|
||||
}
|
||||
|
||||
this.logger.succ('All expired mutings checked.');
|
||||
|
|
|
@ -39,6 +39,7 @@ export class WebhookDeliverProcessorService {
|
|||
'X-Misskey-Host': this.config.host,
|
||||
'X-Misskey-Hook-Id': job.data.webhookId,
|
||||
'X-Misskey-Hook-Secret': job.data.secret,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
hookId: job.data.webhookId,
|
||||
|
|
|
@ -12,7 +12,7 @@ import type { Config } from '@/config.js';
|
|||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { QueueService } from '@/core/QueueService.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 { countIf } from '@/misc/prelude/array.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
|
@ -58,7 +58,7 @@ export class ActivityPubServerService {
|
|||
private userEntityService: UserEntityService,
|
||||
private apRendererService: ApRendererService,
|
||||
private queueService: QueueService,
|
||||
private userKeypairStoreService: UserKeypairStoreService,
|
||||
private userKeypairService: UserKeypairService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
//this.createServer = this.createServer.bind(this);
|
||||
|
@ -540,7 +540,7 @@ export class ActivityPubServerService {
|
|||
return;
|
||||
}
|
||||
|
||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
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 { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { AuthenticateService } from './AuthenticateService.js';
|
||||
import MainStreamConnection from './stream/index.js';
|
||||
import { ChannelsService } from './stream/ChannelsService.js';
|
||||
|
@ -45,7 +46,7 @@ export class StreamingApiServerService {
|
|||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
private cacheService: CacheService,
|
||||
private noteReadService: NoteReadService,
|
||||
private authenticateService: AuthenticateService,
|
||||
private channelsService: ChannelsService,
|
||||
|
@ -73,8 +74,6 @@ export class StreamingApiServerService {
|
|||
return;
|
||||
}
|
||||
|
||||
const connection = request.accept();
|
||||
|
||||
const ev = new EventEmitter();
|
||||
|
||||
async function onRedisMessage(_: string, data: string): Promise<void> {
|
||||
|
@ -85,19 +84,19 @@ export class StreamingApiServerService {
|
|||
this.redisSubscriber.on('message', onRedisMessage);
|
||||
|
||||
const main = new MainStreamConnection(
|
||||
this.followingsRepository,
|
||||
this.mutingsRepository,
|
||||
this.renoteMutingsRepository,
|
||||
this.blockingsRepository,
|
||||
this.channelFollowingsRepository,
|
||||
this.userProfilesRepository,
|
||||
this.channelsService,
|
||||
this.globalEventService,
|
||||
this.noteReadService,
|
||||
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(() => {
|
||||
this.usersRepository.update(user.id, {
|
||||
lastActiveDate: new Date(),
|
||||
|
|
|
@ -61,11 +61,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
await this.usersRepository.update(user.id, {
|
||||
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,
|
||||
});
|
||||
|
||||
// Terminate streaming
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await this.userSuspendService.doPostSuspend(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 idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const channel = await this.channelsRepository.findOneBy({
|
||||
|
@ -58,8 +57,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
followerId: me.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)
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const channel = await this.channelsRepository.findOneBy({
|
||||
|
@ -54,8 +52,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
followerId: me.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
|
||||
this.globalEventService.publishInternalEvent('userTokenRegenerated', { id: me.id, oldToken, newToken });
|
||||
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,
|
||||
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
|
||||
this.globalEventService.publishMainStream(user.id, 'meUpdated', iObj);
|
||||
this.globalEventService.publishUserEvent(user.id, 'updateUserProfile', updatedProfile);
|
||||
|
||||
// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
|
||||
if (user.isLocked && ps.isLocked === false) {
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { IdService } from '@/core/IdService.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 { GetterService } from '@/server/api/GetterService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserMutingService } from '@/core/UserMutingService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -63,10 +60,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
private getterService: GetterService,
|
||||
private idService: IdService,
|
||||
private cacheService: CacheService,
|
||||
private userMutingService: UserMutingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const muter = me;
|
||||
|
@ -96,17 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
return;
|
||||
}
|
||||
|
||||
// Create mute
|
||||
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);
|
||||
await this.userMutingService.mute(muter, mutee, ps.expiresAt ? new Date(ps.expiresAt) : null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { MutingsRepository } from '@/models/index.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { UserMutingService } from '@/core/UserMutingService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
private globalEventService: GlobalEventService,
|
||||
private userMutingService: UserMutingService,
|
||||
private getterService: GetterService,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Delete mute
|
||||
await this.mutingsRepository.delete({
|
||||
id: exist.id,
|
||||
});
|
||||
|
||||
this.globalEventService.publishUserEvent(me.id, 'unmute', mutee);
|
||||
await this.userMutingService.unmute([exist]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,8 +80,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
await this.renoteMutingsRepository.delete({
|
||||
id: exist.id,
|
||||
});
|
||||
|
||||
// publishUserEvent(user.id, 'unmute', mutee);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,16 +23,16 @@ export default abstract class Channel {
|
|||
return this.connection.following;
|
||||
}
|
||||
|
||||
protected get muting() {
|
||||
return this.connection.muting;
|
||||
protected get userIdsWhoMeMuting() {
|
||||
return this.connection.userIdsWhoMeMuting;
|
||||
}
|
||||
|
||||
protected get renoteMuting() {
|
||||
return this.connection.renoteMuting;
|
||||
protected get userIdsWhoMeMutingRenotes() {
|
||||
return this.connection.userIdsWhoMeMutingRenotes;
|
||||
}
|
||||
|
||||
protected get blocking() {
|
||||
return this.connection.blocking;
|
||||
protected get userIdsWhoBlockingMe() {
|
||||
return this.connection.userIdsWhoBlockingMe;
|
||||
}
|
||||
|
||||
protected get followingChannels() {
|
||||
|
|
|
@ -35,11 +35,11 @@ class AntennaChannel extends Channel {
|
|||
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきた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);
|
||||
|
||||
|
|
|
@ -47,11 +47,11 @@ class ChannelChannel extends Channel {
|
|||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきた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);
|
||||
|
||||
|
|
|
@ -64,11 +64,11 @@ class GlobalTimelineChannel extends Channel {
|
|||
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきた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だったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
@ -46,11 +46,11 @@ class HashtagChannel extends Channel {
|
|||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきた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);
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ class HomeTimelineChannel extends Channel {
|
|||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
|
||||
|
@ -38,7 +37,7 @@ class HomeTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
// 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)) {
|
||||
note = await this.noteEntityService.pack(note.id, this.user!, {
|
||||
|
@ -71,18 +70,18 @@ class HomeTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきた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だったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||
// そのためレコードが存在するかのチェックでは不十分なので、改めて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);
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ class HybridTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
// 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) {
|
||||
|
@ -82,11 +82,11 @@ class HybridTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきた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だったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
@ -61,11 +61,11 @@ class LocalTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきた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だったら無視する
|
||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
@ -26,7 +26,7 @@ class MainChannel extends Channel {
|
|||
case 'notification': {
|
||||
// Ignore notifications from instances the user has muted
|
||||
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) {
|
||||
const note = await this.noteEntityService.pack(data.body.note.id, this.user, {
|
||||
|
@ -40,7 +40,7 @@ class MainChannel extends Channel {
|
|||
case 'mention': {
|
||||
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) {
|
||||
const note = await this.noteEntityService.pack(data.body.id, this.user, {
|
||||
detail: true,
|
||||
|
|
|
@ -89,11 +89,11 @@ class UserListChannel extends Channel {
|
|||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.muting)) return;
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきた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);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
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 { UserProfile } from '@/models/entities/UserProfile.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 { NotificationService } from '@/core/NotificationService.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 * as websocket from 'websocket';
|
||||
import type { EventEmitter } from 'events';
|
||||
|
@ -19,106 +17,71 @@ import type { StreamEventEmitter, StreamMessages } from './types.js';
|
|||
*/
|
||||
export default class Connection {
|
||||
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;
|
||||
private wsConnection: websocket.connection;
|
||||
public subscriber: StreamEventEmitter;
|
||||
private channels: Channel[] = [];
|
||||
private subscribingNotes: any = {};
|
||||
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(
|
||||
private followingsRepository: FollowingsRepository,
|
||||
private mutingsRepository: MutingsRepository,
|
||||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
private channelsService: ChannelsService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private noteReadService: NoteReadService,
|
||||
private notificationService: NotificationService,
|
||||
private cacheService: CacheService,
|
||||
|
||||
wsConnection: websocket.connection,
|
||||
subscriber: EventEmitter,
|
||||
user: User | null | undefined,
|
||||
token: AccessToken | null | undefined,
|
||||
) {
|
||||
this.wsConnection = wsConnection;
|
||||
this.subscriber = subscriber;
|
||||
if (user) this.user = user;
|
||||
if (token) this.token = token;
|
||||
}
|
||||
|
||||
//this.onWsConnectionMessage = this.onWsConnectionMessage.bind(this);
|
||||
//this.onUserEvent = this.onUserEvent.bind(this);
|
||||
//this.onNoteStreamMessage = this.onNoteStreamMessage.bind(this);
|
||||
//this.onBroadcastMessage = this.onBroadcastMessage.bind(this);
|
||||
@bindThis
|
||||
public async fetch() {
|
||||
if (this.user == null) return;
|
||||
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.subscriber.on('broadcast', 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 (this.user && (note.userId !== this.user.id)) {
|
||||
this.noteReadService.read(this.user.id, [note], {
|
||||
following: this.following,
|
||||
followingChannels: this.followingChannels,
|
||||
});
|
||||
this.noteReadService.read(this.user.id, [note]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
public dispose() {
|
||||
if (this.fetchIntervalId) clearInterval(this.fetchIntervalId);
|
||||
for (const c of this.channels.filter(c => c.dispose)) {
|
||||
if (c.dispose) c.dispose();
|
||||
}
|
||||
|
|
|
@ -38,6 +38,11 @@ export interface InternalStreamTypes {
|
|||
antennaDeleted: Antenna;
|
||||
antennaUpdated: Antenna;
|
||||
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 {
|
||||
|
@ -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 {
|
||||
notification: Packed<'Notification'>;
|
||||
mention: Packed<'Note'>;
|
||||
|
@ -97,8 +90,6 @@ export interface MainStreamTypes {
|
|||
readAllAntennas: undefined;
|
||||
unreadAntenna: Antenna;
|
||||
readAllAnnouncements: undefined;
|
||||
readAllChannels: undefined;
|
||||
unreadChannel: Note['id'];
|
||||
myTokenRegenerated: undefined;
|
||||
signin: Signin;
|
||||
registryUpdated: {
|
||||
|
@ -202,10 +193,6 @@ export type StreamMessages = {
|
|||
name: 'broadcast';
|
||||
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
|
||||
};
|
||||
user: {
|
||||
name: `user:${User['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<UserStreamTypes>>;
|
||||
};
|
||||
main: {
|
||||
name: `mainStream:${User['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<MainStreamTypes>>;
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
||||
</div>
|
||||
<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>
|
||||
</MkModal>
|
||||
|
@ -84,6 +84,7 @@ const props = withDefaults(defineProps<{
|
|||
actions?: {
|
||||
text: string;
|
||||
primary?: boolean,
|
||||
danger?: boolean,
|
||||
callback: (...args: any[]) => void;
|
||||
}[];
|
||||
showOkButton?: boolean;
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<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>
|
||||
</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>
|
||||
<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 === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</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 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 === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</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>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<MkAvatar :class="$style.avatar" :user="$i" link preview/>
|
||||
<div :class="$style.main">
|
||||
<div :class="$style.header">
|
||||
<MkUserName :user="$i"/>
|
||||
<MkUserName :user="$i" :nowrap="true"/>
|
||||
</div>
|
||||
<div>
|
||||
<div :class="$style.content">
|
||||
|
@ -50,6 +50,9 @@ const props = defineProps<{
|
|||
.header {
|
||||
margin-bottom: 2px;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
overflow: clip;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@container (min-width: 350px) {
|
||||
|
|
|
@ -7,20 +7,35 @@
|
|||
@drop.stop="onDrop"
|
||||
>
|
||||
<header :class="$style.header">
|
||||
<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">
|
||||
<MkAvatar :user="postAccount ?? $i" :class="$style.avatar"/>
|
||||
</button>
|
||||
<div :class="$style.headerRight">
|
||||
<span :class="[$style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</span>
|
||||
<span v-if="localOnly" :class="$style.localOnly"><i class="ti ti-world-off"></i></span>
|
||||
<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 === 'home'"><i class="ti ti-home"></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>
|
||||
<div :class="$style.headerLeft">
|
||||
<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">
|
||||
<MkAvatar :user="postAccount ?? $i" :class="$style.avatar"/>
|
||||
</button>
|
||||
</div>
|
||||
<div :class="$style.headerRight">
|
||||
<template v-if="!(channel != null && fixed)">
|
||||
<button v-if="channel == null" ref="visibilityButton" v-click-anime v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility">
|
||||
<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 === 'followers'"><i class="ti ti-lock"></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 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">
|
||||
<div :class="$style.submitInner">
|
||||
<template v-if="posted"></template>
|
||||
|
@ -31,50 +46,49 @@
|
|||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div :class="[$style.form]">
|
||||
<MkNoteSimple v-if="reply" :class="$style.targetNote" :note="reply"/>
|
||||
<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="visibility === 'specified'" :class="$style.toSpecified">
|
||||
<span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span>
|
||||
<div :class="$style.visibleUsers">
|
||||
<span v-for="u in visibleUsers" :key="u.id" :class="$style.visibleUser">
|
||||
<MkAcct :user="u"/>
|
||||
<button class="_button" style="padding: 4px 8px;" @click="removeVisibleUser(u)"><i class="ti ti-x"></i></button>
|
||||
</span>
|
||||
<button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
|
||||
</div>
|
||||
<MkNoteSimple v-if="reply" :class="$style.targetNote" :note="reply"/>
|
||||
<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="visibility === 'specified'" :class="$style.toSpecified">
|
||||
<span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span>
|
||||
<div :class="$style.visibleUsers">
|
||||
<span v-for="u in visibleUsers" :key="u.id" :class="$style.visibleUser">
|
||||
<MkAcct :user="u"/>
|
||||
<button class="_button" style="padding: 4px 8px;" @click="removeVisibleUser(u)"><i class="ti ti-x"></i></button>
|
||||
</span>
|
||||
<button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
|
||||
</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>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
|
||||
<div v-if="showingOptions" style="padding: 0 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>
|
||||
<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">
|
||||
</div>
|
||||
<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">
|
||||
<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">
|
||||
<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"/>
|
||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
|
||||
<div v-if="showingOptions" style="padding: 8px 16px;">
|
||||
</div>
|
||||
<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.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.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-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>
|
||||
</footer>
|
||||
<datalist id="hashtags">
|
||||
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
|
||||
</datalist>
|
||||
</div>
|
||||
<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>
|
||||
<datalist id="hashtags">
|
||||
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
|
||||
</datalist>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -85,7 +99,6 @@ import * as misskey from 'misskey-js';
|
|||
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
import { toASCII } from 'punycode/';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import MkSelect from './MkSelect.vue';
|
||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
||||
|
@ -113,7 +126,7 @@ const modal = inject('modal');
|
|||
const props = withDefaults(defineProps<{
|
||||
reply?: misskey.entities.Note;
|
||||
renote?: misskey.entities.Note;
|
||||
channel?: any; // TODO
|
||||
channel?: misskey.entities.Channel; // TODO
|
||||
mention?: misskey.entities.User;
|
||||
specified?: misskey.entities.User;
|
||||
initialText?: string;
|
||||
|
@ -401,13 +414,14 @@ function upload(file: File, name?: string) {
|
|||
|
||||
function setVisibility() {
|
||||
if (props.channel) {
|
||||
// TODO: information dialog
|
||||
visibility = 'public';
|
||||
localOnly = true; // TODO: チャンネルが連合するようになった折には消す
|
||||
return;
|
||||
}
|
||||
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
|
||||
currentVisibility: visibility,
|
||||
currentLocalOnly: localOnly,
|
||||
localOnly: localOnly,
|
||||
src: visibilityButton,
|
||||
}, {
|
||||
changeVisibility: v => {
|
||||
|
@ -416,15 +430,65 @@ function setVisibility() {
|
|||
defaultStore.set('visibility', visibility);
|
||||
}
|
||||
},
|
||||
changeLocalOnly: v => {
|
||||
localOnly = v;
|
||||
if (defaultStore.state.rememberNoteVisibility) {
|
||||
defaultStore.set('localOnly', localOnly);
|
||||
}
|
||||
},
|
||||
}, '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) {
|
||||
if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) {
|
||||
visibleUsers.push(user);
|
||||
|
@ -591,7 +655,8 @@ async function post(ev?: MouseEvent) {
|
|||
text.includes('$[x4') ||
|
||||
text.includes('$[scale') ||
|
||||
text.includes('$[position');
|
||||
if (annoying) {
|
||||
|
||||
if (annoying && visibility === 'public') {
|
||||
const { canceled, result } = await os.actions({
|
||||
type: 'warning',
|
||||
text: i18n.ts.thisPostMayBeAnnoying,
|
||||
|
@ -817,6 +882,7 @@ defineExpose({
|
|||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
container-type: inline-size;
|
||||
|
||||
&.modal {
|
||||
width: 100%;
|
||||
|
@ -824,21 +890,29 @@ defineExpose({
|
|||
}
|
||||
}
|
||||
|
||||
//#region header
|
||||
.header {
|
||||
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 {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
width: 64px;
|
||||
line-height: 66px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.account {
|
||||
height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
display: inline-flex;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
@ -846,55 +920,23 @@ defineExpose({
|
|||
.avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: auto;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.headerRight {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.textCount {
|
||||
opacity: 0.7;
|
||||
line-height: 66px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
display: flex;
|
||||
min-height: 48px;
|
||||
font-size: 0.9em;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
gap: 4px;
|
||||
overflow: clip;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin: 16px 16px 16px 0;
|
||||
margin: 12px 12px 12px 6px;
|
||||
vertical-align: bottom;
|
||||
|
||||
&:disabled {
|
||||
|
@ -923,16 +965,47 @@ defineExpose({
|
|||
line-height: 34px;
|
||||
font-weight: bold;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
min-width: 90px;
|
||||
box-sizing: border-box;
|
||||
color: var(--fgOnAccent);
|
||||
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 {
|
||||
padding: 16px 20px 0 20px;
|
||||
}
|
||||
|
@ -966,10 +1039,6 @@ defineExpose({
|
|||
background: var(--X4);
|
||||
}
|
||||
|
||||
.disableFederationWarn {
|
||||
margin: 0 20px 16px 20px;
|
||||
}
|
||||
|
||||
.hasNotSpecifiedMentions {
|
||||
margin: 0 20px 16px 20px;
|
||||
}
|
||||
|
@ -1011,18 +1080,61 @@ defineExpose({
|
|||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
|
||||
.text {
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
min-height: 90px;
|
||||
.textOuter {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
&.withCw {
|
||||
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 {
|
||||
display: flex;
|
||||
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 {
|
||||
|
@ -1030,8 +1142,8 @@ defineExpose({
|
|||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
|
@ -1043,42 +1155,34 @@ defineExpose({
|
|||
}
|
||||
}
|
||||
|
||||
.emojiButton {
|
||||
position: absolute;
|
||||
top: 55px;
|
||||
right: 13px;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
.previewButtonActive {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@container (max-width: 500px) {
|
||||
.header {
|
||||
height: 50px;
|
||||
.headerRight {
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
> .cancel {
|
||||
width: 50px;
|
||||
line-height: 50px;
|
||||
}
|
||||
.headerRightButtonText {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .headerRight {
|
||||
> .textCount {
|
||||
line-height: 50px;
|
||||
}
|
||||
.visibility {
|
||||
overflow: initial;
|
||||
}
|
||||
|
||||
> .submit {
|
||||
margin: 8px;
|
||||
}
|
||||
}
|
||||
.submit {
|
||||
margin: 8px 8px 8px 4px;
|
||||
}
|
||||
|
||||
.toSpecified {
|
||||
padding: 6px 16px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
padding: 16px 14px 0 14px;
|
||||
}
|
||||
.cw,
|
||||
.hashtags,
|
||||
.text {
|
||||
|
@ -1094,11 +1198,13 @@ defineExpose({
|
|||
}
|
||||
}
|
||||
|
||||
@container (max-width: 310px) {
|
||||
.footerButton {
|
||||
@container (max-width: 330px) {
|
||||
.headerRight {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 14px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -24,19 +24,19 @@ const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.d
|
|||
|
||||
const props = defineProps<{
|
||||
modelValue: any[];
|
||||
detachMediaFn: () => void;
|
||||
detachMediaFn?: (id: string) => void;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any[]): void;
|
||||
(ev: 'detach'): void;
|
||||
(ev: 'detach', id: string): void;
|
||||
(ev: 'changeSensitive'): void;
|
||||
(ev: 'changeName'): void;
|
||||
}>();
|
||||
|
||||
let menuShowing = false;
|
||||
|
||||
function detachMedia(id) {
|
||||
function detachMedia(id: string) {
|
||||
if (props.detachMediaFn) {
|
||||
props.detachMediaFn(id);
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
<template>
|
||||
<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
|
||||
<div class="_popup" :class="$style.root">
|
||||
<MkModal ref="modal" v-slot="{ type }" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
|
||||
<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')">
|
||||
<div :class="$style.icon"><i class="ti ti-world"></i></div>
|
||||
<div :class="$style.body">
|
||||
|
@ -29,21 +32,12 @@
|
|||
<span :class="$style.itemDescription">{{ i18n.ts._visibility.specifiedDescription }}</span>
|
||||
</div>
|
||||
</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>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, watch } from 'vue';
|
||||
import { nextTick } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
@ -52,42 +46,58 @@ const modal = $shallowRef<InstanceType<typeof MkModal>>();
|
|||
|
||||
const props = withDefaults(defineProps<{
|
||||
currentVisibility: typeof misskey.noteVisibilities[number];
|
||||
currentLocalOnly: boolean;
|
||||
localOnly: boolean;
|
||||
src?: HTMLElement;
|
||||
}>(), {
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'changeVisibility', v: typeof misskey.noteVisibilities[number]): void;
|
||||
(ev: 'changeLocalOnly', v: boolean): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
let v = $ref(props.currentVisibility);
|
||||
let localOnly = $ref(props.currentLocalOnly);
|
||||
|
||||
watch($$(localOnly), () => {
|
||||
emit('changeLocalOnly', localOnly);
|
||||
});
|
||||
|
||||
function choose(visibility: typeof misskey.noteVisibilities[number]): void {
|
||||
v = visibility;
|
||||
emit('changeVisibility', visibility);
|
||||
nextTick(() => {
|
||||
modal.close();
|
||||
if (modal) modal.close();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
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 {
|
||||
margin: 8px 0;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
.label {
|
||||
pointer-events: none;
|
||||
font-size: 10px;
|
||||
padding-bottom: 4px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.item {
|
||||
|
@ -107,13 +117,7 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
|
|||
}
|
||||
|
||||
&.active {
|
||||
color: var(--fgOnAccent);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
&.localOnly.active {
|
||||
color: var(--accent);
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,16 +148,4 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
|
|||
.itemDescription {
|
||||
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>
|
||||
|
|
|
@ -513,15 +513,6 @@ if ($i) {
|
|||
updateAccount({ hasUnreadAnnouncement: false });
|
||||
});
|
||||
|
||||
main.on('readAllChannels', () => {
|
||||
updateAccount({ hasUnreadChannel: false });
|
||||
});
|
||||
|
||||
main.on('unreadChannel', () => {
|
||||
updateAccount({ hasUnreadChannel: true });
|
||||
sound.play('channel');
|
||||
});
|
||||
|
||||
// トークンが再生成されたとき
|
||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
||||
main.on('myTokenRegenerated', () => {
|
||||
|
|
|
@ -6,6 +6,7 @@ type Keys =
|
|||
'accounts' |
|
||||
'latestDonationInfoShownAt' |
|
||||
'neverShowDonationInfo' |
|
||||
'neverShowLocalOnlyInfo' |
|
||||
'lastUsed' |
|
||||
'lang' |
|
||||
'drafts' |
|
||||
|
|
|
@ -215,6 +215,7 @@ export function actions<T extends {
|
|||
value: string;
|
||||
text: string;
|
||||
primary?: boolean,
|
||||
danger?: boolean,
|
||||
}[]>(props: {
|
||||
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
||||
title?: string | null;
|
||||
|
@ -229,6 +230,7 @@ export function actions<T extends {
|
|||
actions: props.actions.map(a => ({
|
||||
text: a.text,
|
||||
primary: a.primary,
|
||||
danger: a.danger,
|
||||
callback: () => {
|
||||
resolve({ canceled: false, result: a.value });
|
||||
},
|
||||
|
|
|
@ -98,9 +98,7 @@ function edit() {
|
|||
|
||||
function openPostForm() {
|
||||
os.post({
|
||||
channel: {
|
||||
id: channel.id,
|
||||
},
|
||||
channel,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import * as os from '@/os';
|
|||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
user: misskey.entities.UserDetailed;
|
||||
|
|
|
@ -88,7 +88,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
},
|
||||
reactionAcceptance: {
|
||||
where: 'account',
|
||||
default: null,
|
||||
default: null as 'likeOnly' | 'likeOnlyForRemote' | null,
|
||||
},
|
||||
mutedWords: {
|
||||
where: 'account',
|
||||
|
|
|
@ -14,13 +14,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XColumn from './column.vue';
|
||||
import { updateColumn, Column } from './deck-store';
|
||||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as misskey from 'misskey-js';
|
||||
|
||||
const props = defineProps<{
|
||||
column: Column;
|
||||
|
@ -33,6 +33,7 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
|
||||
let channel = $shallowRef<misskey.entities.Channel>();
|
||||
|
||||
if (props.column.channelId == null) {
|
||||
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({
|
||||
channel: {
|
||||
id: props.column.channelId,
|
||||
},
|
||||
channel,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,31 @@ import { describe, test, assert, afterEach } from 'vitest';
|
|||
import { render, cleanup, type RenderResult } from '@testing-library/vue';
|
||||
import './init';
|
||||
import type { DriveFile } from 'misskey-js/built/entities';
|
||||
import { components } from '@/components';
|
||||
import { directives } from '@/directives';
|
||||
import MkMediaImage from '@/components/MkMediaImage.vue';
|
||||
|
||||
describe('MkMediaImage', () => {
|
||||
const renderMediaImage = (image: Partial<DriveFile>): RenderResult => {
|
||||
return render(MkMediaImage, {
|
||||
props: { image },
|
||||
global: { directives },
|
||||
props: {
|
||||
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 './init';
|
||||
import type { summaly } from 'summaly';
|
||||
import { components } from '@/components';
|
||||
import { directives } from '@/directives';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
|
||||
|
@ -27,7 +28,7 @@ describe('MkMediaImage', () => {
|
|||
|
||||
const result = render(MkUrlPreview, {
|
||||
props: { url: summary.url },
|
||||
global: { directives },
|
||||
global: { directives, components },
|
||||
});
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
|
|
|
@ -88,7 +88,6 @@ export type MeDetailed = UserDetailed & {
|
|||
hasPendingReceivedFollowRequest: boolean;
|
||||
hasUnreadAnnouncement: boolean;
|
||||
hasUnreadAntenna: boolean;
|
||||
hasUnreadChannel: boolean;
|
||||
hasUnreadMentions: boolean;
|
||||
hasUnreadMessagingMessage: boolean;
|
||||
hasUnreadNotification: boolean;
|
||||
|
|
|
@ -28,8 +28,6 @@ export type Channels = {
|
|||
readAllAntennas: () => void;
|
||||
unreadAntenna: (payload: Antenna) => void;
|
||||
readAllAnnouncements: () => void;
|
||||
readAllChannels: () => void;
|
||||
unreadChannel: (payload: Note['id']) => void;
|
||||
myTokenRegenerated: () => void;
|
||||
reversiNoInvites: () => void;
|
||||
reversiInvited: (payload: FIXME) => void;
|
||||
|
|
Loading…
Reference in New Issue