Merge branch 'develop' into ed25519
This commit is contained in:
commit
2bc4221f40
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -14,7 +14,10 @@
|
||||||
## 202x.x.x (unreleased)
|
## 202x.x.x (unreleased)
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
- 通知がミュート、凍結を考慮するようになりました
|
||||||
- Enhance: サーバーごとにモデレーションノートを残せるように
|
- Enhance: サーバーごとにモデレーションノートを残せるように
|
||||||
|
- Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加
|
||||||
|
- Enhance: 通知の受信設定に「フォロー中またはフォロワー」を追加
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整
|
- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整
|
||||||
|
@ -22,16 +25,23 @@
|
||||||
- Fix: MFMのオートコンプリートが出るべき状況で出ないことがある問題を修正
|
- Fix: MFMのオートコンプリートが出るべき状況で出ないことがある問題を修正
|
||||||
- Fix: チャートのラベルが消えている問題を修正
|
- Fix: チャートのラベルが消えている問題を修正
|
||||||
- Fix: 画面表示後最初の音声再生が爆音になることがある問題を修正
|
- Fix: 画面表示後最初の音声再生が爆音になることがある問題を修正
|
||||||
|
- Fix: 設定のバックアップ作成時に名前を入力しなかった場合、ローカライゼーションがおかしくなる問題を修正
|
||||||
|
- Fix: ページ`/admin/emojis`の絵文字編集ダイアログで「リアクションとして使えるロール」を追加する際に何も選択せずOKを押下すると画面が固まる問題を修正
|
||||||
- Fix: 絵文字サジェストの順位で、絵文字自体の名前が同じものよりもタグで一致しているものが優先されてしまう問題を修正
|
- Fix: 絵文字サジェストの順位で、絵文字自体の名前が同じものよりもタグで一致しているものが優先されてしまう問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正
|
- Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正
|
||||||
|
- Fix: 破損した通知をクライアントに送信しないように
|
||||||
|
* 通知欄が無限にリロードされる問題が改善する可能性があります
|
||||||
- エンドポイント`flash/update`の`flashId`以外のパラメータは必須ではなくなりました
|
- エンドポイント`flash/update`の`flashId`以外のパラメータは必須ではなくなりました
|
||||||
- Fix: 禁止キーワードを含むノートがDelayed Queueに追加されて再処理される問題を修正
|
- Fix: 禁止キーワードを含むノートがDelayed Queueに追加されて再処理される問題を修正
|
||||||
|
- Fix: 自分がフォローしていないアカウントのフォロワー限定ノートが閲覧できることがある問題を修正
|
||||||
|
- Fix: タイムラインのオプションで「リノートを表示」を無効にしている際、投票のみの引用リノートが流れてこない問題を修正
|
||||||
- エンドポイント`admin/emoji/update`の各種修正
|
- エンドポイント`admin/emoji/update`の各種修正
|
||||||
- 必須パラメータを`id`または`name`のいずれかのみに
|
- 必須パラメータを`id`または`name`のいずれかのみに
|
||||||
- `id`の代わりに`name`で絵文字を指定可能に(`id`・`name`両指定時は従来通り`name`を変更する挙動)
|
- `id`の代わりに`name`で絵文字を指定可能に(`id`・`name`両指定時は従来通り`name`を変更する挙動)
|
||||||
- `category`および`licence`が指定なしの時勝手にnullに上書きされる挙動を修正
|
- `category`および`licence`が指定なしの時勝手にnullに上書きされる挙動を修正
|
||||||
|
- Fix: 通知の受信設定で「相互フォロー」が正しく動作しない問題を修正
|
||||||
|
|
||||||
## 2024.2.0
|
## 2024.2.0
|
||||||
|
|
||||||
|
|
|
@ -4656,6 +4656,10 @@ export interface Locale extends ILocale {
|
||||||
* 相互フォロー
|
* 相互フォロー
|
||||||
*/
|
*/
|
||||||
"mutualFollow": string;
|
"mutualFollow": string;
|
||||||
|
/**
|
||||||
|
* フォロー中またはフォロワー
|
||||||
|
*/
|
||||||
|
"followingOrFollower": string;
|
||||||
/**
|
/**
|
||||||
* ファイル付きのみ
|
* ファイル付きのみ
|
||||||
*/
|
*/
|
||||||
|
@ -6528,6 +6532,10 @@ export interface Locale extends ILocale {
|
||||||
"avatarDecorationLimit": string;
|
"avatarDecorationLimit": string;
|
||||||
};
|
};
|
||||||
"_condition": {
|
"_condition": {
|
||||||
|
/**
|
||||||
|
* マニュアルロールにアサイン済み
|
||||||
|
*/
|
||||||
|
"roleAssignedTo": string;
|
||||||
/**
|
/**
|
||||||
* ローカルユーザー
|
* ローカルユーザー
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1160,6 +1160,7 @@ showRenotes: "リノートを表示"
|
||||||
edited: "編集済み"
|
edited: "編集済み"
|
||||||
notificationRecieveConfig: "通知の受信設定"
|
notificationRecieveConfig: "通知の受信設定"
|
||||||
mutualFollow: "相互フォロー"
|
mutualFollow: "相互フォロー"
|
||||||
|
followingOrFollower: "フォロー中またはフォロワー"
|
||||||
fileAttachedOnly: "ファイル付きのみ"
|
fileAttachedOnly: "ファイル付きのみ"
|
||||||
showRepliesToOthersInTimeline: "TLに他の人への返信を含める"
|
showRepliesToOthersInTimeline: "TLに他の人への返信を含める"
|
||||||
hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない"
|
hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない"
|
||||||
|
@ -1687,6 +1688,7 @@ _role:
|
||||||
canUseTranslator: "翻訳機能の利用"
|
canUseTranslator: "翻訳機能の利用"
|
||||||
avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
|
avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
|
||||||
_condition:
|
_condition:
|
||||||
|
roleAssignedTo: "マニュアルロールにアサイン済み"
|
||||||
isLocal: "ローカルユーザー"
|
isLocal: "ローカルユーザー"
|
||||||
isRemote: "リモートユーザー"
|
isRemote: "リモートユーザー"
|
||||||
createdLessThan: "アカウント作成から~以内"
|
createdLessThan: "アカウント作成から~以内"
|
||||||
|
|
|
@ -122,6 +122,14 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else if (recieveConfig?.type === 'mutualFollow') {
|
} else if (recieveConfig?.type === 'mutualFollow') {
|
||||||
|
const [isFollowing, isFollower] = await Promise.all([
|
||||||
|
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)),
|
||||||
|
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)),
|
||||||
|
]);
|
||||||
|
if (!(isFollowing && isFollower)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (recieveConfig?.type === 'followingOrFollower') {
|
||||||
const [isFollowing, isFollower] = await Promise.all([
|
const [isFollowing, isFollower] = await Promise.all([
|
||||||
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)),
|
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)),
|
||||||
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)),
|
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)),
|
||||||
|
@ -155,6 +163,8 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
|
|
||||||
const packed = await this.notificationEntityService.pack(notification, notifieeId, {});
|
const packed = await this.notificationEntityService.pack(notification, notifieeId, {});
|
||||||
|
|
||||||
|
if (packed == null) return null;
|
||||||
|
|
||||||
// Publish notification event
|
// Publish notification event
|
||||||
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
||||||
|
|
||||||
|
|
|
@ -200,17 +200,20 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private evalCond(user: MiUser, value: RoleCondFormulaValue): boolean {
|
private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue): boolean {
|
||||||
try {
|
try {
|
||||||
switch (value.type) {
|
switch (value.type) {
|
||||||
case 'and': {
|
case 'and': {
|
||||||
return value.values.every(v => this.evalCond(user, v));
|
return value.values.every(v => this.evalCond(user, roles, v));
|
||||||
}
|
}
|
||||||
case 'or': {
|
case 'or': {
|
||||||
return value.values.some(v => this.evalCond(user, v));
|
return value.values.some(v => this.evalCond(user, roles, v));
|
||||||
}
|
}
|
||||||
case 'not': {
|
case 'not': {
|
||||||
return !this.evalCond(user, value.value);
|
return !this.evalCond(user, roles, value.value);
|
||||||
|
}
|
||||||
|
case 'roleAssignedTo': {
|
||||||
|
return roles.some(r => r.id === value.roleId);
|
||||||
}
|
}
|
||||||
case 'isLocal': {
|
case 'isLocal': {
|
||||||
return this.userEntityService.isLocalUser(user);
|
return this.userEntityService.isLocalUser(user);
|
||||||
|
@ -272,7 +275,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
const assigns = await this.getUserAssigns(userId);
|
const assigns = await this.getUserAssigns(userId);
|
||||||
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
|
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
|
||||||
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
|
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
|
||||||
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula));
|
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, assignedRoles, r.condFormula));
|
||||||
return [...assignedRoles, ...matchedCondRoles];
|
return [...assignedRoles, ...matchedCondRoles];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,13 +288,13 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
|
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
|
||||||
// 期限切れのロールを除外
|
// 期限切れのロールを除外
|
||||||
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
||||||
const assignedRoleIds = assigns.map(x => x.roleId);
|
|
||||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||||
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
|
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
|
||||||
|
const assignedBadgeRoles = assignedRoles.filter(r => r.asBadge);
|
||||||
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
|
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
|
||||||
if (badgeCondRoles.length > 0) {
|
if (badgeCondRoles.length > 0) {
|
||||||
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
|
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
|
||||||
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula));
|
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, assignedRoles, r.condFormula));
|
||||||
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
|
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
|
||||||
} else {
|
} else {
|
||||||
return assignedBadgeRoles;
|
return assignedBadgeRoles;
|
||||||
|
|
|
@ -14,14 +14,14 @@ import type { MiNote } from '@/models/Note.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
import { isNotNull } from '@/misc/is-not-null.js';
|
||||||
import { FilterUnionByProperty, notificationTypes } from '@/types.js';
|
import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { RoleEntityService } from './RoleEntityService.js';
|
import { RoleEntityService } from './RoleEntityService.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
import type { UserEntityService } from './UserEntityService.js';
|
import type { UserEntityService } from './UserEntityService.js';
|
||||||
import type { NoteEntityService } from './NoteEntityService.js';
|
import type { NoteEntityService } from './NoteEntityService.js';
|
||||||
|
|
||||||
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]);
|
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded'] as (typeof groupedNotificationTypes[number])[]);
|
||||||
const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded']);
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationEntityService implements OnModuleInit {
|
export class NotificationEntityService implements OnModuleInit {
|
||||||
|
@ -41,6 +41,8 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
@Inject(DI.followRequestsRepository)
|
@Inject(DI.followRequestsRepository)
|
||||||
private followRequestsRepository: FollowRequestsRepository,
|
private followRequestsRepository: FollowRequestsRepository,
|
||||||
|
|
||||||
|
private cacheService: CacheService,
|
||||||
|
|
||||||
//private userEntityService: UserEntityService,
|
//private userEntityService: UserEntityService,
|
||||||
//private noteEntityService: NoteEntityService,
|
//private noteEntityService: NoteEntityService,
|
||||||
) {
|
) {
|
||||||
|
@ -52,130 +54,48 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
this.roleEntityService = this.moduleRef.get('RoleEntityService');
|
this.roleEntityService = this.moduleRef.get('RoleEntityService');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
/**
|
||||||
public async pack(
|
* 通知をパックする共通処理
|
||||||
src: MiNotification,
|
*/
|
||||||
|
async #packInternal <T extends MiNotification | MiGroupedNotification> (
|
||||||
|
src: T,
|
||||||
meId: MiUser['id'],
|
meId: MiUser['id'],
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
options: {
|
options: {
|
||||||
|
checkValidNotifier?: boolean;
|
||||||
},
|
},
|
||||||
hint?: {
|
hint?: {
|
||||||
packedNotes: Map<MiNote['id'], Packed<'Note'>>;
|
packedNotes: Map<MiNote['id'], Packed<'Note'>>;
|
||||||
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
|
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
|
||||||
},
|
},
|
||||||
): Promise<Packed<'Notification'>> {
|
): Promise<Packed<'Notification'> | null> {
|
||||||
const notification = src;
|
const notification = src;
|
||||||
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification ? (
|
|
||||||
|
if (options.checkValidNotifier !== false && !(await this.#isValidNotifier(notification, meId))) return null;
|
||||||
|
|
||||||
|
const needsNote = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification;
|
||||||
|
const noteIfNeed = needsNote ? (
|
||||||
hint?.packedNotes != null
|
hint?.packedNotes != null
|
||||||
? hint.packedNotes.get(notification.noteId)
|
? hint.packedNotes.get(notification.noteId)
|
||||||
: this.noteEntityService.pack(notification.noteId, { id: meId }, {
|
: this.noteEntityService.pack(notification.noteId, { id: meId }, {
|
||||||
detail: true,
|
detail: true,
|
||||||
})
|
})
|
||||||
) : undefined;
|
) : undefined;
|
||||||
const userIfNeed = 'notifierId' in notification ? (
|
// if the note has been deleted, don't show this notification
|
||||||
hint?.packedUsers != null
|
if (needsNote && !noteIfNeed) return null;
|
||||||
? hint.packedUsers.get(notification.notifierId)
|
|
||||||
: this.userEntityService.pack(notification.notifierId, { id: meId })
|
const needsUser = 'notifierId' in notification;
|
||||||
) : undefined;
|
const userIfNeed = needsUser ? (
|
||||||
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
|
|
||||||
|
|
||||||
return await awaitAll({
|
|
||||||
id: notification.id,
|
|
||||||
createdAt: new Date(notification.createdAt).toISOString(),
|
|
||||||
type: notification.type,
|
|
||||||
userId: 'notifierId' in notification ? notification.notifierId : undefined,
|
|
||||||
...(userIfNeed != null ? { user: userIfNeed } : {}),
|
|
||||||
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
|
|
||||||
...(notification.type === 'reaction' ? {
|
|
||||||
reaction: notification.reaction,
|
|
||||||
} : {}),
|
|
||||||
...(notification.type === 'roleAssigned' ? {
|
|
||||||
role: role,
|
|
||||||
} : {}),
|
|
||||||
...(notification.type === 'achievementEarned' ? {
|
|
||||||
achievement: notification.achievement,
|
|
||||||
} : {}),
|
|
||||||
...(notification.type === 'app' ? {
|
|
||||||
body: notification.customBody,
|
|
||||||
header: notification.customHeader,
|
|
||||||
icon: notification.customIcon,
|
|
||||||
} : {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public async packMany(
|
|
||||||
notifications: MiNotification[],
|
|
||||||
meId: MiUser['id'],
|
|
||||||
) {
|
|
||||||
if (notifications.length === 0) return [];
|
|
||||||
|
|
||||||
let validNotifications = notifications;
|
|
||||||
|
|
||||||
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
|
|
||||||
const notes = noteIds.length > 0 ? await this.notesRepository.find({
|
|
||||||
where: { id: In(noteIds) },
|
|
||||||
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
|
|
||||||
}) : [];
|
|
||||||
const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, {
|
|
||||||
detail: true,
|
|
||||||
});
|
|
||||||
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));
|
|
||||||
|
|
||||||
validNotifications = validNotifications.filter(x => !('noteId' in x) || packedNotes.has(x.noteId));
|
|
||||||
|
|
||||||
const userIds = validNotifications.map(x => 'notifierId' in x ? x.notifierId : null).filter(isNotNull);
|
|
||||||
const users = userIds.length > 0 ? await this.usersRepository.find({
|
|
||||||
where: { id: In(userIds) },
|
|
||||||
}) : [];
|
|
||||||
const packedUsersArray = await this.userEntityService.packMany(users, { id: meId });
|
|
||||||
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
|
|
||||||
|
|
||||||
// 既に解決されたフォローリクエストの通知を除外
|
|
||||||
const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty<MiGroupedNotification, 'type', 'receiveFollowRequest'> => x.type === 'receiveFollowRequest');
|
|
||||||
if (followRequestNotifications.length > 0) {
|
|
||||||
const reqs = await this.followRequestsRepository.find({
|
|
||||||
where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) },
|
|
||||||
});
|
|
||||||
validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await Promise.all(validNotifications.map(x => this.pack(x, meId, {}, {
|
|
||||||
packedNotes,
|
|
||||||
packedUsers,
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public async packGrouped(
|
|
||||||
src: MiGroupedNotification,
|
|
||||||
meId: MiUser['id'],
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
options: {
|
|
||||||
|
|
||||||
},
|
|
||||||
hint?: {
|
|
||||||
packedNotes: Map<MiNote['id'], Packed<'Note'>>;
|
|
||||||
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
|
|
||||||
},
|
|
||||||
): Promise<Packed<'Notification'>> {
|
|
||||||
const notification = src;
|
|
||||||
const noteIfNeed = NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification ? (
|
|
||||||
hint?.packedNotes != null
|
|
||||||
? hint.packedNotes.get(notification.noteId)
|
|
||||||
: this.noteEntityService.pack(notification.noteId, { id: meId }, {
|
|
||||||
detail: true,
|
|
||||||
})
|
|
||||||
) : undefined;
|
|
||||||
const userIfNeed = 'notifierId' in notification ? (
|
|
||||||
hint?.packedUsers != null
|
hint?.packedUsers != null
|
||||||
? hint.packedUsers.get(notification.notifierId)
|
? hint.packedUsers.get(notification.notifierId)
|
||||||
: this.userEntityService.pack(notification.notifierId, { id: meId })
|
: this.userEntityService.pack(notification.notifierId, { id: meId })
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
// if the user has been deleted, don't show this notification
|
||||||
|
if (needsUser && !userIfNeed) return null;
|
||||||
|
|
||||||
|
// #region Grouped notifications
|
||||||
if (notification.type === 'reaction:grouped') {
|
if (notification.type === 'reaction:grouped') {
|
||||||
const reactions = await Promise.all(notification.reactions.map(async reaction => {
|
const reactions = (await Promise.all(notification.reactions.map(async reaction => {
|
||||||
const user = hint?.packedUsers != null
|
const user = hint?.packedUsers != null
|
||||||
? hint.packedUsers.get(reaction.userId)!
|
? hint.packedUsers.get(reaction.userId)!
|
||||||
: await this.userEntityService.pack(reaction.userId, { id: meId });
|
: await this.userEntityService.pack(reaction.userId, { id: meId });
|
||||||
|
@ -183,7 +103,12 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
user,
|
user,
|
||||||
reaction: reaction.reaction,
|
reaction: reaction.reaction,
|
||||||
};
|
};
|
||||||
}));
|
}))).filter(r => isNotNull(r.user));
|
||||||
|
// if all users have been deleted, don't show this notification
|
||||||
|
if (reactions.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return await awaitAll({
|
return await awaitAll({
|
||||||
id: notification.id,
|
id: notification.id,
|
||||||
createdAt: new Date(notification.createdAt).toISOString(),
|
createdAt: new Date(notification.createdAt).toISOString(),
|
||||||
|
@ -192,14 +117,19 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
reactions,
|
reactions,
|
||||||
});
|
});
|
||||||
} else if (notification.type === 'renote:grouped') {
|
} else if (notification.type === 'renote:grouped') {
|
||||||
const users = await Promise.all(notification.userIds.map(userId => {
|
const users = (await Promise.all(notification.userIds.map(userId => {
|
||||||
const packedUser = hint?.packedUsers != null ? hint.packedUsers.get(userId) : null;
|
const packedUser = hint?.packedUsers != null ? hint.packedUsers.get(userId) : null;
|
||||||
if (packedUser) {
|
if (packedUser) {
|
||||||
return packedUser;
|
return packedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.userEntityService.pack(userId, { id: meId });
|
return this.userEntityService.pack(userId, { id: meId });
|
||||||
}));
|
}))).filter(isNotNull);
|
||||||
|
// if all users have been deleted, don't show this notification
|
||||||
|
if (users.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return await awaitAll({
|
return await awaitAll({
|
||||||
id: notification.id,
|
id: notification.id,
|
||||||
createdAt: new Date(notification.createdAt).toISOString(),
|
createdAt: new Date(notification.createdAt).toISOString(),
|
||||||
|
@ -208,8 +138,14 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
users,
|
users,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
|
const needsRole = notification.type === 'roleAssigned';
|
||||||
|
const role = needsRole ? await this.roleEntityService.pack(notification.roleId) : undefined;
|
||||||
|
// if the role has been deleted, don't show this notification
|
||||||
|
if (needsRole && !role) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return await awaitAll({
|
return await awaitAll({
|
||||||
id: notification.id,
|
id: notification.id,
|
||||||
|
@ -235,15 +171,16 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
async #packManyInternal <T extends MiNotification | MiGroupedNotification> (
|
||||||
public async packGroupedMany(
|
notifications: T[],
|
||||||
notifications: MiGroupedNotification[],
|
|
||||||
meId: MiUser['id'],
|
meId: MiUser['id'],
|
||||||
) {
|
): Promise<T[]> {
|
||||||
if (notifications.length === 0) return [];
|
if (notifications.length === 0) return [];
|
||||||
|
|
||||||
let validNotifications = notifications;
|
let validNotifications = notifications;
|
||||||
|
|
||||||
|
validNotifications = await this.#filterValidNotifier(validNotifications, meId);
|
||||||
|
|
||||||
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
|
const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
|
||||||
const notes = noteIds.length > 0 ? await this.notesRepository.find({
|
const notes = noteIds.length > 0 ? await this.notesRepository.find({
|
||||||
where: { id: In(noteIds) },
|
where: { id: In(noteIds) },
|
||||||
|
@ -269,7 +206,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
|
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
|
||||||
|
|
||||||
// 既に解決されたフォローリクエストの通知を除外
|
// 既に解決されたフォローリクエストの通知を除外
|
||||||
const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty<MiGroupedNotification, 'type', 'receiveFollowRequest'> => x.type === 'receiveFollowRequest');
|
const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty<T, 'type', 'receiveFollowRequest'> => x.type === 'receiveFollowRequest');
|
||||||
if (followRequestNotifications.length > 0) {
|
if (followRequestNotifications.length > 0) {
|
||||||
const reqs = await this.followRequestsRepository.find({
|
const reqs = await this.followRequestsRepository.find({
|
||||||
where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) },
|
where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) },
|
||||||
|
@ -277,9 +214,107 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId));
|
validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId));
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Promise.all(validNotifications.map(x => this.packGrouped(x, meId, {}, {
|
const packPromises = validNotifications.map(x => {
|
||||||
packedNotes,
|
return this.pack(
|
||||||
packedUsers,
|
x,
|
||||||
})));
|
meId,
|
||||||
|
{ checkValidNotifier: false },
|
||||||
|
{ packedNotes, packedUsers },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (await Promise.all(packPromises)).filter(isNotNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async pack(
|
||||||
|
src: MiNotification | MiGroupedNotification,
|
||||||
|
meId: MiUser['id'],
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
options: {
|
||||||
|
checkValidNotifier?: boolean;
|
||||||
|
},
|
||||||
|
hint?: {
|
||||||
|
packedNotes: Map<MiNote['id'], Packed<'Note'>>;
|
||||||
|
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
|
||||||
|
},
|
||||||
|
): Promise<Packed<'Notification'> | null> {
|
||||||
|
return await this.#packInternal(src, meId, options, hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async packMany(
|
||||||
|
notifications: MiNotification[],
|
||||||
|
meId: MiUser['id'],
|
||||||
|
): Promise<MiNotification[]> {
|
||||||
|
return await this.#packManyInternal(notifications, meId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async packGroupedMany(
|
||||||
|
notifications: MiGroupedNotification[],
|
||||||
|
meId: MiUser['id'],
|
||||||
|
): Promise<MiGroupedNotification[]> {
|
||||||
|
return await this.#packManyInternal(notifications, meId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* notifierが存在するか、ミュートされていないか、サスペンドされていないかを確認するvalidator
|
||||||
|
*/
|
||||||
|
#validateNotifier <T extends MiNotification | MiGroupedNotification> (
|
||||||
|
notification: T,
|
||||||
|
userIdsWhoMeMuting: Set<MiUser['id']>,
|
||||||
|
userMutedInstances: Set<string>,
|
||||||
|
notifiers: MiUser[],
|
||||||
|
): boolean {
|
||||||
|
if (!('notifierId' in notification)) return true;
|
||||||
|
if (userIdsWhoMeMuting.has(notification.notifierId)) return false;
|
||||||
|
|
||||||
|
const notifier = notifiers.find(x => x.id === notification.notifierId) ?? null;
|
||||||
|
|
||||||
|
if (notifier == null) return false;
|
||||||
|
if (notifier.host && userMutedInstances.has(notifier.host)) return false;
|
||||||
|
|
||||||
|
if (notifier.isSuspended) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* notifierが存在するか、ミュートされていないか、サスペンドされていないかを実際に確認する
|
||||||
|
*/
|
||||||
|
async #isValidNotifier(
|
||||||
|
notification: MiNotification | MiGroupedNotification,
|
||||||
|
meId: MiUser['id'],
|
||||||
|
): Promise<boolean> {
|
||||||
|
return (await this.#filterValidNotifier([notification], meId)).length === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* notifierが存在するか、ミュートされていないか、サスペンドされていないかを実際に複数確認する
|
||||||
|
*/
|
||||||
|
async #filterValidNotifier <T extends MiNotification | MiGroupedNotification> (
|
||||||
|
notifications: T[],
|
||||||
|
meId: MiUser['id'],
|
||||||
|
): Promise<T[]> {
|
||||||
|
const [
|
||||||
|
userIdsWhoMeMuting,
|
||||||
|
userMutedInstances,
|
||||||
|
] = await Promise.all([
|
||||||
|
this.cacheService.userMutingsCache.fetch(meId),
|
||||||
|
this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(isNotNull);
|
||||||
|
const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({
|
||||||
|
where: { id: In(notifierIds) },
|
||||||
|
}) : [];
|
||||||
|
|
||||||
|
const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => {
|
||||||
|
const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers);
|
||||||
|
return isValid ? notification : null;
|
||||||
|
}))) as [T | null] ).filter(isNotNull);
|
||||||
|
|
||||||
|
return filteredNotifications;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import * as fs from 'node:fs/promises';
|
||||||
|
import type { PathLike } from 'node:fs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `fs.createWriteStream()`相当のことを行う`WritableStream` (Web標準)
|
||||||
|
*/
|
||||||
|
export class FileWriterStream extends WritableStream<Uint8Array> {
|
||||||
|
constructor(path: PathLike) {
|
||||||
|
let file: fs.FileHandle | null = null;
|
||||||
|
|
||||||
|
super({
|
||||||
|
start: async () => {
|
||||||
|
file = await fs.open(path, 'a');
|
||||||
|
},
|
||||||
|
write: async (chunk, controller) => {
|
||||||
|
if (file === null) {
|
||||||
|
controller.error();
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
await file.write(chunk);
|
||||||
|
},
|
||||||
|
close: async () => {
|
||||||
|
await file?.close();
|
||||||
|
},
|
||||||
|
abort: async () => {
|
||||||
|
await file?.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { TransformStream } from 'node:stream/web';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ストリームに流れてきた各データについて`JSON.stringify()`した上で、それらを一つの配列にまとめる
|
||||||
|
*/
|
||||||
|
export class JsonArrayStream extends TransformStream<unknown, string> {
|
||||||
|
constructor() {
|
||||||
|
/** 最初の要素かどうかを変数に記録 */
|
||||||
|
let isFirst = true;
|
||||||
|
|
||||||
|
super({
|
||||||
|
start(controller) {
|
||||||
|
controller.enqueue('[');
|
||||||
|
},
|
||||||
|
flush(controller) {
|
||||||
|
controller.enqueue(']');
|
||||||
|
},
|
||||||
|
transform(chunk, controller) {
|
||||||
|
if (isFirst) {
|
||||||
|
isFirst = false;
|
||||||
|
} else {
|
||||||
|
// 妥当なJSON配列にするためには最初以外の要素の前に`,`を挿入しなければならない
|
||||||
|
controller.enqueue(',\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.enqueue(JSON.stringify(chunk));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,6 +44,7 @@ import {
|
||||||
packedRoleCondFormulaLogicsSchema,
|
packedRoleCondFormulaLogicsSchema,
|
||||||
packedRoleCondFormulaValueNot,
|
packedRoleCondFormulaValueNot,
|
||||||
packedRoleCondFormulaValueIsLocalOrRemoteSchema,
|
packedRoleCondFormulaValueIsLocalOrRemoteSchema,
|
||||||
|
packedRoleCondFormulaValueAssignedRoleSchema,
|
||||||
packedRoleCondFormulaValueCreatedSchema,
|
packedRoleCondFormulaValueCreatedSchema,
|
||||||
packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
|
packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
|
||||||
packedRoleCondFormulaValueSchema,
|
packedRoleCondFormulaValueSchema,
|
||||||
|
@ -96,6 +97,7 @@ export const refs = {
|
||||||
RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
|
RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
|
||||||
RoleCondFormulaValueNot: packedRoleCondFormulaValueNot,
|
RoleCondFormulaValueNot: packedRoleCondFormulaValueNot,
|
||||||
RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema,
|
RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema,
|
||||||
|
RoleCondFormulaValueAssignedRole: packedRoleCondFormulaValueAssignedRoleSchema,
|
||||||
RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema,
|
RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema,
|
||||||
RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
|
RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
|
||||||
RoleCondFormulaValue: packedRoleCondFormulaValueSchema,
|
RoleCondFormulaValue: packedRoleCondFormulaValueSchema,
|
||||||
|
|
|
@ -29,6 +29,11 @@ type CondFormulaValueIsRemote = {
|
||||||
type: 'isRemote';
|
type: 'isRemote';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type CondFormulaValueRoleAssignedTo = {
|
||||||
|
type: 'roleAssignedTo';
|
||||||
|
roleId: string;
|
||||||
|
};
|
||||||
|
|
||||||
type CondFormulaValueCreatedLessThan = {
|
type CondFormulaValueCreatedLessThan = {
|
||||||
type: 'createdLessThan';
|
type: 'createdLessThan';
|
||||||
sec: number;
|
sec: number;
|
||||||
|
@ -75,6 +80,7 @@ export type RoleCondFormulaValue = { id: string } & (
|
||||||
CondFormulaValueNot |
|
CondFormulaValueNot |
|
||||||
CondFormulaValueIsLocal |
|
CondFormulaValueIsLocal |
|
||||||
CondFormulaValueIsRemote |
|
CondFormulaValueIsRemote |
|
||||||
|
CondFormulaValueRoleAssignedTo |
|
||||||
CondFormulaValueCreatedLessThan |
|
CondFormulaValueCreatedLessThan |
|
||||||
CondFormulaValueCreatedMoreThan |
|
CondFormulaValueCreatedMoreThan |
|
||||||
CondFormulaValueFollowersLessThanOrEq |
|
CondFormulaValueFollowersLessThanOrEq |
|
||||||
|
|
|
@ -249,6 +249,8 @@ export class MiUserProfile {
|
||||||
type: 'follower';
|
type: 'follower';
|
||||||
} | {
|
} | {
|
||||||
type: 'mutualFollow';
|
type: 'mutualFollow';
|
||||||
|
} | {
|
||||||
|
type: 'followingOrFollower';
|
||||||
} | {
|
} | {
|
||||||
type: 'list';
|
type: 'list';
|
||||||
userListId: MiUserList['id'];
|
userListId: MiUserList['id'];
|
||||||
|
|
|
@ -57,6 +57,23 @@ export const packedRoleCondFormulaValueIsLocalOrRemoteSchema = {
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const packedRoleCondFormulaValueAssignedRoleSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
enum: ['roleAssignedTo'],
|
||||||
|
},
|
||||||
|
roleId: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
format: 'id',
|
||||||
|
example: 'xxxxxxxxxx',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const packedRoleCondFormulaValueCreatedSchema = {
|
export const packedRoleCondFormulaValueCreatedSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -115,6 +132,9 @@ export const packedRoleCondFormulaValueSchema = {
|
||||||
{
|
{
|
||||||
ref: 'RoleCondFormulaValueIsLocalOrRemote',
|
ref: 'RoleCondFormulaValueIsLocalOrRemote',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ref: 'RoleCondFormulaValueAssignedRole',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ref: 'RoleCondFormulaValueCreated',
|
ref: 'RoleCondFormulaValueCreated',
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const notificationRecieveConfig = {
|
||||||
type: {
|
type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: false,
|
nullable: false,
|
||||||
enum: ['all', 'following', 'follower', 'mutualFollow', 'never'],
|
enum: ['all', 'following', 'follower', 'mutualFollow', 'followingOrFollower', 'never'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ['type'],
|
required: ['type'],
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'node:fs';
|
import { ReadableStream, TextEncoderStream } from 'node:stream/web';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { MoreThan } from 'typeorm';
|
import { MoreThan } from 'typeorm';
|
||||||
import { format as dateFormat } from 'date-fns';
|
import { format as dateFormat } from 'date-fns';
|
||||||
|
@ -18,10 +18,82 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import { Packed } from '@/misc/json-schema.js';
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { JsonArrayStream } from '@/misc/JsonArrayStream.js';
|
||||||
|
import { FileWriterStream } from '@/misc/FileWriterStream.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
import type { DbJobDataWithUser } from '../types.js';
|
import type { DbJobDataWithUser } from '../types.js';
|
||||||
|
|
||||||
|
class NoteStream extends ReadableStream<Record<string, unknown>> {
|
||||||
|
constructor(
|
||||||
|
job: Bull.Job,
|
||||||
|
notesRepository: NotesRepository,
|
||||||
|
pollsRepository: PollsRepository,
|
||||||
|
driveFileEntityService: DriveFileEntityService,
|
||||||
|
idService: IdService,
|
||||||
|
userId: string,
|
||||||
|
) {
|
||||||
|
let exportedNotesCount = 0;
|
||||||
|
let cursor: MiNote['id'] | null = null;
|
||||||
|
|
||||||
|
const serialize = (
|
||||||
|
note: MiNote,
|
||||||
|
poll: MiPoll | null,
|
||||||
|
files: Packed<'DriveFile'>[],
|
||||||
|
): Record<string, unknown> => {
|
||||||
|
return {
|
||||||
|
id: note.id,
|
||||||
|
text: note.text,
|
||||||
|
createdAt: idService.parse(note.id).date.toISOString(),
|
||||||
|
fileIds: note.fileIds,
|
||||||
|
files: files,
|
||||||
|
replyId: note.replyId,
|
||||||
|
renoteId: note.renoteId,
|
||||||
|
poll: poll,
|
||||||
|
cw: note.cw,
|
||||||
|
visibility: note.visibility,
|
||||||
|
visibleUserIds: note.visibleUserIds,
|
||||||
|
localOnly: note.localOnly,
|
||||||
|
reactionAcceptance: note.reactionAcceptance,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
super({
|
||||||
|
async pull(controller): Promise<void> {
|
||||||
|
const notes = await notesRepository.find({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
...(cursor !== null ? { id: MoreThan(cursor) } : {}),
|
||||||
|
},
|
||||||
|
take: 100, // 100件ずつ取得
|
||||||
|
order: { id: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (notes.length === 0) {
|
||||||
|
job.updateProgress(100);
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = notes.at(-1)?.id ?? null;
|
||||||
|
|
||||||
|
for (const note of notes) {
|
||||||
|
const poll = note.hasPoll
|
||||||
|
? await pollsRepository.findOneByOrFail({ noteId: note.id }) // N+1
|
||||||
|
: null;
|
||||||
|
const files = await driveFileEntityService.packManyByIds(note.fileIds); // N+1
|
||||||
|
const content = serialize(note, poll, files);
|
||||||
|
|
||||||
|
controller.enqueue(content);
|
||||||
|
exportedNotesCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = await notesRepository.countBy({ userId });
|
||||||
|
job.updateProgress(exportedNotesCount / total);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExportNotesProcessorService {
|
export class ExportNotesProcessorService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
@ -59,67 +131,19 @@ export class ExportNotesProcessorService {
|
||||||
this.logger.info(`Temp file is ${path}`);
|
this.logger.info(`Temp file is ${path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
// メモリが足りなくならないようにストリームで処理する
|
||||||
|
await new NoteStream(
|
||||||
|
job,
|
||||||
|
this.notesRepository,
|
||||||
|
this.pollsRepository,
|
||||||
|
this.driveFileEntityService,
|
||||||
|
this.idService,
|
||||||
|
user.id,
|
||||||
|
)
|
||||||
|
.pipeThrough(new JsonArrayStream())
|
||||||
|
.pipeThrough(new TextEncoderStream())
|
||||||
|
.pipeTo(new FileWriterStream(path));
|
||||||
|
|
||||||
const write = (text: string): Promise<void> => {
|
|
||||||
return new Promise<void>((res, rej) => {
|
|
||||||
stream.write(text, err => {
|
|
||||||
if (err) {
|
|
||||||
this.logger.error(err);
|
|
||||||
rej(err);
|
|
||||||
} else {
|
|
||||||
res();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
await write('[');
|
|
||||||
|
|
||||||
let exportedNotesCount = 0;
|
|
||||||
let cursor: MiNote['id'] | null = null;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const notes = await this.notesRepository.find({
|
|
||||||
where: {
|
|
||||||
userId: user.id,
|
|
||||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
|
||||||
},
|
|
||||||
take: 100,
|
|
||||||
order: {
|
|
||||||
id: 1,
|
|
||||||
},
|
|
||||||
}) as MiNote[];
|
|
||||||
|
|
||||||
if (notes.length === 0) {
|
|
||||||
job.updateProgress(100);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor = notes.at(-1)?.id ?? null;
|
|
||||||
|
|
||||||
for (const note of notes) {
|
|
||||||
let poll: MiPoll | undefined;
|
|
||||||
if (note.hasPoll) {
|
|
||||||
poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id });
|
|
||||||
}
|
|
||||||
const files = await this.driveFileEntityService.packManyByIds(note.fileIds);
|
|
||||||
const content = JSON.stringify(this.serialize(note, poll, files));
|
|
||||||
const isFirst = exportedNotesCount === 0;
|
|
||||||
await write(isFirst ? content : ',\n' + content);
|
|
||||||
exportedNotesCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const total = await this.notesRepository.countBy({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
job.updateProgress(exportedNotesCount / total);
|
|
||||||
}
|
|
||||||
|
|
||||||
await write(']');
|
|
||||||
|
|
||||||
stream.end();
|
|
||||||
this.logger.succ(`Exported to: ${path}`);
|
this.logger.succ(`Exported to: ${path}`);
|
||||||
|
|
||||||
const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
|
const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
|
||||||
|
@ -130,22 +154,4 @@ export class ExportNotesProcessorService {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private serialize(note: MiNote, poll: MiPoll | null = null, files: Packed<'DriveFile'>[]): Record<string, unknown> {
|
|
||||||
return {
|
|
||||||
id: note.id,
|
|
||||||
text: note.text,
|
|
||||||
createdAt: this.idService.parse(note.id).date.toISOString(),
|
|
||||||
fileIds: note.fileIds,
|
|
||||||
files: files,
|
|
||||||
replyId: note.replyId,
|
|
||||||
renoteId: note.renoteId,
|
|
||||||
poll: poll,
|
|
||||||
cw: note.cw,
|
|
||||||
visibility: note.visibility,
|
|
||||||
visibleUserIds: note.visibleUserIds,
|
|
||||||
localOnly: note.localOnly,
|
|
||||||
reactionAcceptance: note.reactionAcceptance,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Brackets, In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotesRepository } from '@/models/_.js';
|
import type { NotesRepository } from '@/models/_.js';
|
||||||
import { obsoleteNotificationTypes, notificationTypes, FilterUnionByProperty } from '@/types.js';
|
import { obsoleteNotificationTypes, groupedNotificationTypes, FilterUnionByProperty } from '@/types.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteReadService } from '@/core/NoteReadService.js';
|
import { NoteReadService } from '@/core/NoteReadService.js';
|
||||||
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
||||||
|
@ -48,10 +48,10 @@ export const paramDef = {
|
||||||
markAsRead: { type: 'boolean', default: true },
|
markAsRead: { type: 'boolean', default: true },
|
||||||
// 後方互換のため、廃止された通知タイプも受け付ける
|
// 後方互換のため、廃止された通知タイプも受け付ける
|
||||||
includeTypes: { type: 'array', items: {
|
includeTypes: { type: 'array', items: {
|
||||||
type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
|
type: 'string', enum: [...groupedNotificationTypes, ...obsoleteNotificationTypes],
|
||||||
} },
|
} },
|
||||||
excludeTypes: { type: 'array', items: {
|
excludeTypes: { type: 'array', items: {
|
||||||
type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
|
type: 'string', enum: [...groupedNotificationTypes, ...obsoleteNotificationTypes],
|
||||||
} },
|
} },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
|
@ -79,12 +79,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
// excludeTypes に全指定されている場合はクエリしない
|
// excludeTypes に全指定されている場合はクエリしない
|
||||||
if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) {
|
if (groupedNotificationTypes.every(type => ps.excludeTypes?.includes(type))) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][];
|
||||||
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][];
|
||||||
|
|
||||||
const limit = (ps.limit + EXTRA_LIMIT) + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
const limit = (ps.limit + EXTRA_LIMIT) + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||||
const notificationsRes = await this.redisClient.xrevrange(
|
const notificationsRes = await this.redisClient.xrevrange(
|
||||||
|
@ -162,7 +162,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
groupedNotifications = groupedNotifications.slice(0, ps.limit);
|
groupedNotifications = groupedNotifications.slice(0, ps.limit);
|
||||||
|
|
||||||
const noteIds = groupedNotifications
|
const noteIds = groupedNotifications
|
||||||
.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type))
|
.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type))
|
||||||
.map(notification => notification.noteId!);
|
.map(notification => notification.noteId!);
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Brackets, In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotesRepository } from '@/models/_.js';
|
import type { NotesRepository } from '@/models/_.js';
|
||||||
|
|
|
@ -71,7 +71,15 @@ class HomeTimelineChannel extends Channel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
// 純粋なリノート(引用リノートでないリノート)の場合
|
||||||
|
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && note.poll == null) {
|
||||||
|
if (!this.withRenotes) return;
|
||||||
|
if (note.renote.reply) {
|
||||||
|
const reply = note.renote.reply;
|
||||||
|
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||||
|
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
* achievementEarned - 実績を獲得
|
* achievementEarned - 実績を獲得
|
||||||
* app - アプリ通知
|
* app - アプリ通知
|
||||||
* test - テスト通知(サーバー側)
|
* test - テスト通知(サーバー側)
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
export const notificationTypes = [
|
export const notificationTypes = [
|
||||||
'note',
|
'note',
|
||||||
|
@ -33,7 +34,15 @@ export const notificationTypes = [
|
||||||
'roleAssigned',
|
'roleAssigned',
|
||||||
'achievementEarned',
|
'achievementEarned',
|
||||||
'app',
|
'app',
|
||||||
'test'] as const;
|
'test',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const groupedNotificationTypes = [
|
||||||
|
...notificationTypes,
|
||||||
|
'reaction:grouped',
|
||||||
|
'renote:grouped',
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
||||||
|
|
||||||
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
||||||
|
|
|
@ -117,5 +117,184 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||||
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||||
});
|
});
|
||||||
|
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||||
|
const aliceNote = await post(alice, { text: 'hi' });
|
||||||
|
await post(bob, { text: '@alice hi', replyId: aliceNote.id });
|
||||||
|
await post(carol, { text: '@alice hi', replyId: aliceNote.id });
|
||||||
|
|
||||||
|
const res = await api('/i/notifications', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||||
|
await post(alice, { text: 'hi' });
|
||||||
|
await post(bob, { text: '@alice hi' });
|
||||||
|
await post(carol, { text: '@alice hi' });
|
||||||
|
|
||||||
|
const res = await api('/i/notifications', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
|
||||||
|
const aliceNote = await post(alice, { text: 'hi' });
|
||||||
|
await post(bob, { text: 'hi', renoteId: aliceNote.id });
|
||||||
|
await post(carol, { text: 'hi', renoteId: aliceNote.id });
|
||||||
|
|
||||||
|
const res = await api('/i/notifications', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
|
||||||
|
const aliceNote = await post(alice, { text: 'hi' });
|
||||||
|
await post(bob, { renoteId: aliceNote.id });
|
||||||
|
await post(carol, { renoteId: aliceNote.id });
|
||||||
|
|
||||||
|
const res = await api('/i/notifications', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
|
||||||
|
await api('/i/follow', { userId: alice.id }, bob);
|
||||||
|
await api('/i/follow', { userId: alice.id }, carol);
|
||||||
|
|
||||||
|
const res = await api('/i/notifications', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => {
|
||||||
|
await api('/i/update/', { isLocked: true }, alice);
|
||||||
|
await api('/following/create', { userId: alice.id }, bob);
|
||||||
|
await api('/following/create', { userId: alice.id }, carol);
|
||||||
|
|
||||||
|
const res = await api('/i/notifications', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Notification (Grouped)', () => {
|
||||||
|
test('通知にミュートしているユーザーの通知が含まれない(リアクション)', async () => {
|
||||||
|
const aliceNote = await post(alice, { text: 'hi' });
|
||||||
|
await react(bob, aliceNote, 'like');
|
||||||
|
await react(carol, aliceNote, 'like');
|
||||||
|
|
||||||
|
const res = await api('/i/notifications-grouped', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||||
|
});
|
||||||
|
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||||
|
const aliceNote = await post(alice, { text: 'hi' });
|
||||||
|
await post(bob, { text: '@alice hi', replyId: aliceNote.id });
|
||||||
|
await post(carol, { text: '@alice hi', replyId: aliceNote.id });
|
||||||
|
|
||||||
|
const res = await api('/i/notifications-grouped', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通知にミュートしているユーザーからのリプライが含まれない', async () => {
|
||||||
|
await post(alice, { text: 'hi' });
|
||||||
|
await post(bob, { text: '@alice hi' });
|
||||||
|
await post(carol, { text: '@alice hi' });
|
||||||
|
|
||||||
|
const res = await api('/i/notifications-grouped', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通知にミュートしているユーザーからの引用リノートが含まれない', async () => {
|
||||||
|
const aliceNote = await post(alice, { text: 'hi' });
|
||||||
|
await post(bob, { text: 'hi', renoteId: aliceNote.id });
|
||||||
|
await post(carol, { text: 'hi', renoteId: aliceNote.id });
|
||||||
|
|
||||||
|
const res = await api('/i/notifications-grouped', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通知にミュートしているユーザーからのリノートが含まれない', async () => {
|
||||||
|
const aliceNote = await post(alice, { text: 'hi' });
|
||||||
|
await post(bob, { renoteId: aliceNote.id });
|
||||||
|
await post(carol, { renoteId: aliceNote.id });
|
||||||
|
|
||||||
|
const res = await api('/i/notifications-grouped', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通知にミュートしているユーザーからのフォロー通知が含まれない', async () => {
|
||||||
|
await api('/i/follow', { userId: alice.id }, bob);
|
||||||
|
await api('/i/follow', { userId: alice.id }, carol);
|
||||||
|
|
||||||
|
const res = await api('/i/notifications-grouped', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('通知にミュートしているユーザーからのフォローリクエストが含まれない', async () => {
|
||||||
|
await api('/i/update/', { isLocked: true }, alice);
|
||||||
|
await api('/following/create', { userId: alice.id }, bob);
|
||||||
|
await api('/following/create', { userId: alice.id }, carol);
|
||||||
|
|
||||||
|
const res = await api('/i/notifications-grouped', {}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(Array.isArray(res.body), true);
|
||||||
|
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
|
||||||
|
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,9 +40,9 @@ describe('Streaming', () => {
|
||||||
let chinatsu: misskey.entities.SignupResponse;
|
let chinatsu: misskey.entities.SignupResponse;
|
||||||
let takumi: misskey.entities.SignupResponse;
|
let takumi: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
let kyokoNote: any;
|
let kyokoNote: misskey.entities.Note;
|
||||||
let kanakoNote: any;
|
let kanakoNote: misskey.entities.Note;
|
||||||
let takumiNote: any;
|
let takumiNote: misskey.entities.Note;
|
||||||
let list: any;
|
let list: any;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
@ -68,6 +68,9 @@ describe('Streaming', () => {
|
||||||
// Follow: ayano => akari
|
// Follow: ayano => akari
|
||||||
await follow(ayano, akari);
|
await follow(ayano, akari);
|
||||||
|
|
||||||
|
// Follow: kyoko => chitose
|
||||||
|
await api('following/create', { userId: chitose.id }, kyoko);
|
||||||
|
|
||||||
// Mute: chitose => kanako
|
// Mute: chitose => kanako
|
||||||
await api('mute/create', { userId: kanako.id }, chitose);
|
await api('mute/create', { userId: kanako.id }, chitose);
|
||||||
|
|
||||||
|
@ -170,7 +173,28 @@ describe('Streaming', () => {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信が流れない', async () => {
|
test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信が流れない', async () => {
|
||||||
// TODO
|
const chitoseNote = await post(chitose, { text: 'followers-only post', visibility: 'followers' });
|
||||||
|
|
||||||
|
const fired = await waitFire(
|
||||||
|
ayano, 'homeTimeline', // ayano:home
|
||||||
|
() => api('notes/create', { text: 'reply to chitose\'s followers-only post', replyId: chitoseNote.id }, kyoko), // kyoko's reply to chitose's followers-only post
|
||||||
|
msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(fired, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信のリノートが流れない', async () => {
|
||||||
|
const chitoseNote = await post(chitose, { text: 'followers-only post', visibility: 'followers' });
|
||||||
|
const kyokoReply = await post(kyoko, { text: 'reply to followers-only post', replyId: chitoseNote.id });
|
||||||
|
|
||||||
|
const fired = await waitFire(
|
||||||
|
ayano, 'homeTimeline', // ayano:home
|
||||||
|
() => api('notes/create', { renoteId: kyokoReply.id }, kyoko), // kyoko's renote of kyoko's reply to chitose's followers-only post
|
||||||
|
msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(fired, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('フォローしていないユーザーの投稿は流れない', async () => {
|
test('フォローしていないユーザーの投稿は流れない', async () => {
|
||||||
|
@ -202,6 +226,39 @@ describe('Streaming', () => {
|
||||||
|
|
||||||
assert.strictEqual(fired, false);
|
assert.strictEqual(fired, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('withRenotes: false のときリノートが流れない', async () => {
|
||||||
|
const fired = await waitFire(
|
||||||
|
ayano, 'homeTimeline', // ayano:home
|
||||||
|
() => api('notes/create', { renoteId: kyokoNote.id }, kyoko), // kyoko renote
|
||||||
|
msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
|
||||||
|
{ withRenotes: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(fired, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('withRenotes: false のとき引用リノートが流れる', async () => {
|
||||||
|
const fired = await waitFire(
|
||||||
|
ayano, 'homeTimeline', // ayano:home
|
||||||
|
() => api('notes/create', { text: 'quote', renoteId: kyokoNote.id }, kyoko), // kyoko quote
|
||||||
|
msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
|
||||||
|
{ withRenotes: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(fired, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('withRenotes: false のとき投票のみのリノートが流れる', async () => {
|
||||||
|
const fired = await waitFire(
|
||||||
|
ayano, 'homeTimeline', // ayano:home
|
||||||
|
() => api('notes/create', { poll: { choices: ['kinoko', 'takenoko'] }, renoteId: kyokoNote.id }, kyoko), // kyoko renote with poll
|
||||||
|
msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
|
||||||
|
{ withRenotes: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(fired, true);
|
||||||
|
});
|
||||||
}); // Home
|
}); // Home
|
||||||
|
|
||||||
describe('Local Timeline', () => {
|
describe('Local Timeline', () => {
|
||||||
|
|
|
@ -251,6 +251,34 @@ describe('RoleService', () => {
|
||||||
expect(user2Policies.canManageCustomEmojis).toBe(true);
|
expect(user2Policies.canManageCustomEmojis).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('コンディショナルロール: マニュアルロールにアサイン済み', async () => {
|
||||||
|
const [user1, user2, role1] = await Promise.all([
|
||||||
|
createUser(),
|
||||||
|
createUser(),
|
||||||
|
createRole({
|
||||||
|
name: 'manual role',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const role2 = await createRole({
|
||||||
|
name: 'conditional role',
|
||||||
|
target: 'conditional',
|
||||||
|
condFormula: {
|
||||||
|
// idはバックエンドのロジックに必要ない?
|
||||||
|
id: 'bdc612bd-9d54-4675-ae83-0499c82ea670',
|
||||||
|
type: 'roleAssignedTo',
|
||||||
|
roleId: role1.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await roleService.assign(user2.id, role1.id);
|
||||||
|
|
||||||
|
const [u1role, u2role] = await Promise.all([
|
||||||
|
roleService.getUserRoles(user1.id),
|
||||||
|
roleService.getUserRoles(user2.id),
|
||||||
|
]);
|
||||||
|
expect(u1role.some(r => r.id === role2.id)).toBe(false);
|
||||||
|
expect(u2role.some(r => r.id === role2.id)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test('expired role', async () => {
|
test('expired role', async () => {
|
||||||
const user = await createUser();
|
const user = await createUser();
|
||||||
const role = await createRole({
|
const role = await createRole({
|
||||||
|
|
|
@ -355,7 +355,7 @@ export const uploadUrl = async (user: UserToken, url: string): Promise<Packed<'D
|
||||||
return catcher;
|
return catcher;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function connectStream(user: UserToken, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
|
export function connectStream<C extends keyof misskey.Channels>(user: UserToken, channel: C, listener: (message: Record<string, any>) => any, params?: misskey.Channels[C]['params']): Promise<WebSocket> {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
const url = new URL(`ws://127.0.0.1:${port}/streaming`);
|
const url = new URL(`ws://127.0.0.1:${port}/streaming`);
|
||||||
const options: ClientOptions = {};
|
const options: ClientOptions = {};
|
||||||
|
@ -390,7 +390,7 @@ export function connectStream(user: UserToken, channel: string, listener: (messa
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const waitFire = async (user: UserToken, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean, params?: any) => {
|
export const waitFire = async <C extends keyof misskey.Channels>(user: UserToken, channel: C, trgr: () => any, cond: (msg: Record<string, any>) => boolean, params?: misskey.Channels[C]['params']) => {
|
||||||
return new Promise<boolean>(async (res, rej) => {
|
return new Promise<boolean>(async (res, rej) => {
|
||||||
let timer: NodeJS.Timeout | null = null;
|
let timer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
@ -435,7 +435,7 @@ export const waitFire = async (user: UserToken, channel: string, trgr: () => any
|
||||||
*/
|
*/
|
||||||
export function makeStreamCatcher<T>(
|
export function makeStreamCatcher<T>(
|
||||||
user: UserToken,
|
user: UserToken,
|
||||||
channel: string,
|
channel: keyof misskey.Channels,
|
||||||
cond: (message: Record<string, any>) => boolean,
|
cond: (message: Record<string, any>) => boolean,
|
||||||
extractor: (message: Record<string, any>) => T,
|
extractor: (message: Record<string, any>) => T,
|
||||||
timeout = 60 * 1000): Promise<T> {
|
timeout = 60 * 1000): Promise<T> {
|
||||||
|
|
|
@ -290,7 +290,7 @@ export async function openAccountMenu(opts: {
|
||||||
text: i18n.ts.profile,
|
text: i18n.ts.profile,
|
||||||
to: `/@${ $i.username }`,
|
to: `/@${ $i.username }`,
|
||||||
avatar: $i,
|
avatar: $i,
|
||||||
}, { type: 'divider' }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
|
}, { type: 'divider' as const }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
|
||||||
type: 'parent' as const,
|
type: 'parent' as const,
|
||||||
icon: 'ti ti-plus',
|
icon: 'ti ti-plus',
|
||||||
text: i18n.ts.addAccount,
|
text: i18n.ts.addAccount,
|
||||||
|
|
|
@ -38,11 +38,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template v-if="select.items">
|
<template v-if="select.items">
|
||||||
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
|
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
|
||||||
<optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label">
|
|
||||||
<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
|
|
||||||
</optgroup>
|
|
||||||
</template>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
|
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
|
||||||
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
|
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
|
||||||
|
@ -64,7 +59,7 @@ import MkSelect from '@/components/MkSelect.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
type Input = {
|
type Input = {
|
||||||
type: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
|
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
|
||||||
placeholder?: string | null;
|
placeholder?: string | null;
|
||||||
autocomplete?: string;
|
autocomplete?: string;
|
||||||
default: string | number | null;
|
default: string | number | null;
|
||||||
|
@ -74,22 +69,17 @@ type Input = {
|
||||||
|
|
||||||
type Select = {
|
type Select = {
|
||||||
items: {
|
items: {
|
||||||
value: string;
|
value: any;
|
||||||
text: string;
|
text: string;
|
||||||
}[];
|
}[];
|
||||||
groupedItems: {
|
|
||||||
label: string;
|
|
||||||
items: {
|
|
||||||
value: string;
|
|
||||||
text: string;
|
|
||||||
}[];
|
|
||||||
}[];
|
|
||||||
default: string | null;
|
default: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Result = string | number | true | null;
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting';
|
type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting';
|
||||||
title: string;
|
title?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
input?: Input;
|
input?: Input;
|
||||||
select?: Select;
|
select?: Select;
|
||||||
|
@ -113,7 +103,7 @@ const props = withDefaults(defineProps<{
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', v: { canceled: boolean; result: any }): void;
|
(ev: 'done', v: { canceled: true } | { canceled: false, result: Result }): void;
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -139,8 +129,11 @@ const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'character
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
function done(canceled: boolean, result?) {
|
// overload function を使いたいので lint エラーを無視する
|
||||||
emit('done', { canceled, result });
|
function done(canceled: true): void;
|
||||||
|
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
|
||||||
|
function done(canceled: boolean, result?: Result): void { // eslint-disable-line no-redeclare
|
||||||
|
emit('done', { canceled, result } as { canceled: true } | { canceled: false, result: Result });
|
||||||
modal.value?.close();
|
modal.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,13 +39,13 @@ withDefaults(defineProps<{
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', r?: Misskey.entities.DriveFile[]): void;
|
(ev: 'done', r?: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||||
|
|
||||||
const selected = ref<Misskey.entities.DriveFile[]>([]);
|
const selected = ref<Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]>([]);
|
||||||
|
|
||||||
function ok() {
|
function ok() {
|
||||||
emit('done', selected.value);
|
emit('done', selected.value);
|
||||||
|
@ -57,7 +57,7 @@ function cancel() {
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeSelection(files: Misskey.entities.DriveFile[]) {
|
function onChangeSelection(v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]) {
|
||||||
selected.value = files;
|
selected.value = v;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', v: any): void;
|
(ev: 'done', v: string): void;
|
||||||
(ev: 'close'): void;
|
(ev: 'close'): void;
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
@ -64,7 +64,7 @@ const emit = defineEmits<{
|
||||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
const picker = shallowRef<InstanceType<typeof MkEmojiPicker>>();
|
const picker = shallowRef<InstanceType<typeof MkEmojiPicker>>();
|
||||||
|
|
||||||
function chosen(emoji: any) {
|
function chosen(emoji: string) {
|
||||||
emit('done', emoji);
|
emit('done', emoji);
|
||||||
if (props.choseAndClose) {
|
if (props.choseAndClose) {
|
||||||
modal.value?.close();
|
modal.value?.close();
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
-->
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<MkWindow
|
|
||||||
ref="window"
|
|
||||||
:initialWidth="300"
|
|
||||||
:initialHeight="290"
|
|
||||||
:canResize="true"
|
|
||||||
:mini="true"
|
|
||||||
:front="true"
|
|
||||||
@closed="emit('closed')"
|
|
||||||
>
|
|
||||||
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" :targetNote="targetNote" asWindow :class="$style.picker" @chosen="chosen"/>
|
|
||||||
</MkWindow>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { } from 'vue';
|
|
||||||
import * as Misskey from 'misskey-js';
|
|
||||||
import MkWindow from '@/components/MkWindow.vue';
|
|
||||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
src?: HTMLElement;
|
|
||||||
showPinned?: boolean;
|
|
||||||
asReactionPicker?: boolean;
|
|
||||||
targetNote?: Misskey.entities.Note
|
|
||||||
}>(), {
|
|
||||||
showPinned: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(ev: 'chosen', v: any): void;
|
|
||||||
(ev: 'closed'): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
function chosen(emoji: any) {
|
|
||||||
emit('chosen', emoji);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
.picker {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -21,37 +21,37 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<MkSpacer :marginMin="20" :marginMax="32">
|
<MkSpacer :marginMin="20" :marginMax="32">
|
||||||
<div v-if="Object.keys(form).filter(item => !form[item].hidden).length > 0" class="_gaps_m">
|
<div v-if="Object.keys(form).filter(item => !form[item].hidden).length > 0" class="_gaps_m">
|
||||||
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
|
<template v-for="(v, k) in Object.fromEntries(Object.entries(form).filter(([_, v]) => !('hidden' in v) || 'hidden' in v && !v.hidden))">
|
||||||
<MkInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
|
<MkInput v-if="v.type === 'number'" v-model="values[k]" type="number" :step="v.step || 1">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
<template v-if="v.description" #caption>{{ v.description }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" :mfmAutocomplete="form[item].treatAsMfm">
|
<MkInput v-else-if="v.type === 'string' && !v.multiline" v-model="values[k]" type="text" :mfmAutocomplete="v.treatAsMfm">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
<template v-if="v.description" #caption>{{ v.description }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" :mfmAutocomplete="form[item].treatAsMfm" :mfmPreview="form[item].treatAsMfm">
|
<MkTextarea v-else-if="v.type === 'string' && v.multiline" v-model="values[k]" :mfmAutocomplete="v.treatAsMfm" :mfmPreview="v.treatAsMfm">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
<template v-if="v.description" #caption>{{ v.description }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
<MkSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
|
<MkSwitch v-else-if="v.type === 'boolean'" v-model="values[k]">
|
||||||
<span v-text="form[item].label || item"></span>
|
<span v-text="v.label || k"></span>
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
<template v-if="v.description" #caption>{{ v.description }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<MkSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
|
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
<option v-for="option in form[item].enum" :key="option.value" :value="option.value">{{ option.label }}</option>
|
<option v-for="option in v.enum" :key="option.value" :value="option.value">{{ option.label }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
|
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
<option v-for="option in form[item].options" :key="option.value" :value="option.value">{{ option.label }}</option>
|
<option v-for="option in v.options" :key="option.value" :value="option.value">{{ option.label }}</option>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
<MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :textConverter="form[item].textConverter">
|
<MkRange v-else-if="v.type === 'range'" v-model="values[k]" :min="v.min" :max="v.max" :step="v.step" :textConverter="v.textConverter">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
<template v-if="v.description" #caption>{{ v.description }}</template>
|
||||||
</MkRange>
|
</MkRange>
|
||||||
<MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
|
<MkButton v-else-if="v.type === 'button'" @click="v.action($event, values)">
|
||||||
<span v-text="form[item].content || item"></span>
|
<span v-text="v.content || k"></span>
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,19 +72,21 @@ import MkSelect from './MkSelect.vue';
|
||||||
import MkRange from './MkRange.vue';
|
import MkRange from './MkRange.vue';
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from './MkButton.vue';
|
||||||
import MkRadios from './MkRadios.vue';
|
import MkRadios from './MkRadios.vue';
|
||||||
|
import type { Form } from '@/scripts/form.js';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
form: any;
|
form: Form;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', v: {
|
(ev: 'done', v: {
|
||||||
canceled?: boolean;
|
canceled: true;
|
||||||
result?: any;
|
} | {
|
||||||
|
result: Record<string, any>;
|
||||||
}): void;
|
}): void;
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
|
|
||||||
import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
|
import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { ComponentProps } from 'vue-component-type-helpers';
|
import type { ComponentProps as CP } from 'vue-component-type-helpers';
|
||||||
|
import type { Form, GetFormResultType } from '@/scripts/form.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
|
import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
|
||||||
|
@ -19,7 +19,6 @@ import MkToast from '@/components/MkToast.vue';
|
||||||
import MkDialog from '@/components/MkDialog.vue';
|
import MkDialog from '@/components/MkDialog.vue';
|
||||||
import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
|
import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
|
||||||
import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
|
import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
|
||||||
import MkEmojiPickerWindow from '@/components/MkEmojiPickerWindow.vue';
|
|
||||||
import MkPopupMenu from '@/components/MkPopupMenu.vue';
|
import MkPopupMenu from '@/components/MkPopupMenu.vue';
|
||||||
import MkContextMenu from '@/components/MkContextMenu.vue';
|
import MkContextMenu from '@/components/MkContextMenu.vue';
|
||||||
import { MenuItem } from '@/types/menu.js';
|
import { MenuItem } from '@/types/menu.js';
|
||||||
|
@ -28,15 +27,15 @@ import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||||
|
|
||||||
export const openingWindowsCount = ref(0);
|
export const openingWindowsCount = ref(0);
|
||||||
|
|
||||||
export const apiWithDialog = ((
|
export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
|
||||||
endpoint: string,
|
endpoint: E,
|
||||||
data: Record<string, any> = {},
|
data: P = {} as any,
|
||||||
token?: string | null | undefined,
|
token?: string | null | undefined,
|
||||||
) => {
|
) => {
|
||||||
const promise = misskeyApi(endpoint, data, token);
|
const promise = misskeyApi(endpoint, data, token);
|
||||||
promiseDialog(promise, null, async (err) => {
|
promiseDialog(promise, null, async (err) => {
|
||||||
let title = null;
|
let title: string | undefined;
|
||||||
let text = err.message + '\n' + (err as any).id;
|
let text = err.message + '\n' + err.id;
|
||||||
if (err.code === 'INTERNAL_ERROR') {
|
if (err.code === 'INTERNAL_ERROR') {
|
||||||
title = i18n.ts.internalServerError;
|
title = i18n.ts.internalServerError;
|
||||||
text = i18n.ts.internalServerErrorDescription;
|
text = i18n.ts.internalServerErrorDescription;
|
||||||
|
@ -88,7 +87,7 @@ export const apiWithDialog = ((
|
||||||
export function promiseDialog<T extends Promise<any>>(
|
export function promiseDialog<T extends Promise<any>>(
|
||||||
promise: T,
|
promise: T,
|
||||||
onSuccess?: ((res: any) => void) | null,
|
onSuccess?: ((res: any) => void) | null,
|
||||||
onFailure?: ((err: Error) => void) | null,
|
onFailure?: ((err: Misskey.api.APIError) => void) | null,
|
||||||
text?: string,
|
text?: string,
|
||||||
): T {
|
): T {
|
||||||
const showing = ref(true);
|
const showing = ref(true);
|
||||||
|
@ -149,14 +148,30 @@ export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
|
||||||
// 使い物にならないので、代わりに ['$props'] から色々省くことで emit の型を生成する
|
// 使い物にならないので、代わりに ['$props'] から色々省くことで emit の型を生成する
|
||||||
// FIXME: 何故か *.ts ファイルからだと型がうまく取れない?ことがあるのをなんとかしたい
|
// FIXME: 何故か *.ts ファイルからだと型がうまく取れない?ことがあるのをなんとかしたい
|
||||||
type ComponentEmit<T> = T extends new () => { $props: infer Props }
|
type ComponentEmit<T> = T extends new () => { $props: infer Props }
|
||||||
? EmitsExtractor<Props>
|
? [keyof Pick<T, Extract<keyof T, `on${string}`>>] extends [never]
|
||||||
: never;
|
? Record<string, unknown> // *.ts ファイルから型がうまく取れないとき用(これがないと {} になって型エラーがうるさい)
|
||||||
|
: EmitsExtractor<Props>
|
||||||
|
: T extends (...args: any) => any
|
||||||
|
? ReturnType<T> extends { [x: string]: any; __ctx?: { [x: string]: any; props: infer Props } }
|
||||||
|
? [keyof Pick<T, Extract<keyof T, `on${string}`>>] extends [never]
|
||||||
|
? Record<string, unknown>
|
||||||
|
: EmitsExtractor<Props>
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
|
||||||
|
// props に ref を許可するようにする
|
||||||
|
type ComponentProps<T extends Component> = { [K in keyof CP<T>]: CP<T>[K] | Ref<CP<T>[K]> };
|
||||||
|
|
||||||
type EmitsExtractor<T> = {
|
type EmitsExtractor<T> = {
|
||||||
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
|
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function popup<T extends Component>(component: T, props: ComponentProps<T>, events: ComponentEmit<T> = {} as ComponentEmit<T>, disposeEvent?: keyof ComponentEmit<T>) {
|
export async function popup<T extends Component>(
|
||||||
|
component: T,
|
||||||
|
props: ComponentProps<T>,
|
||||||
|
events: ComponentEmit<T> = {} as ComponentEmit<T>,
|
||||||
|
disposeEvent?: keyof ComponentEmit<T>,
|
||||||
|
): Promise<{ dispose: () => void }> {
|
||||||
markRaw(component);
|
markRaw(component);
|
||||||
|
|
||||||
const id = ++popupIdCount;
|
const id = ++popupIdCount;
|
||||||
|
@ -197,12 +212,12 @@ export function toast(message: string) {
|
||||||
|
|
||||||
export function alert(props: {
|
export function alert(props: {
|
||||||
type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
||||||
title?: string | null;
|
title?: string;
|
||||||
text?: string | null;
|
text?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
popup(MkDialog, props, {
|
popup(MkDialog, props, {
|
||||||
done: result => {
|
done: () => {
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
|
@ -211,12 +226,12 @@ export function alert(props: {
|
||||||
|
|
||||||
export function confirm(props: {
|
export function confirm(props: {
|
||||||
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
||||||
title?: string | null;
|
title?: string;
|
||||||
text?: string | null;
|
text?: string;
|
||||||
okText?: string;
|
okText?: string;
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
}): Promise<{ canceled: boolean }> {
|
}): Promise<{ canceled: boolean }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
popup(MkDialog, {
|
popup(MkDialog, {
|
||||||
...props,
|
...props,
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
|
@ -237,13 +252,15 @@ export function actions<T extends {
|
||||||
danger?: boolean,
|
danger?: boolean,
|
||||||
}[]>(props: {
|
}[]>(props: {
|
||||||
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
||||||
title?: string | null;
|
title?: string;
|
||||||
text?: string | null;
|
text?: string;
|
||||||
actions: T;
|
actions: T;
|
||||||
}): Promise<{ canceled: true; result: undefined; } | {
|
}): Promise<{
|
||||||
|
canceled: true; result: undefined;
|
||||||
|
} | {
|
||||||
canceled: false; result: T[number]['value'];
|
canceled: false; result: T[number]['value'];
|
||||||
}> {
|
}> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
popup(MkDialog, {
|
popup(MkDialog, {
|
||||||
...props,
|
...props,
|
||||||
actions: props.actions.map(a => ({
|
actions: props.actions.map(a => ({
|
||||||
|
@ -262,19 +279,50 @@ export function actions<T extends {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// default が指定されていたら result は null になり得ないことを保証する overload function
|
||||||
export function inputText(props: {
|
export function inputText(props: {
|
||||||
type?: 'text' | 'email' | 'password' | 'url';
|
type?: 'text' | 'email' | 'password' | 'url';
|
||||||
title?: string | null;
|
title?: string;
|
||||||
text?: string | null;
|
text?: string;
|
||||||
|
placeholder?: string | null;
|
||||||
|
autocomplete?: string;
|
||||||
|
default: string;
|
||||||
|
minLength?: number;
|
||||||
|
maxLength?: number;
|
||||||
|
}): Promise<{
|
||||||
|
canceled: true; result: undefined;
|
||||||
|
} | {
|
||||||
|
canceled: false; result: string;
|
||||||
|
}>;
|
||||||
|
export function inputText(props: {
|
||||||
|
type?: 'text' | 'email' | 'password' | 'url';
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
placeholder?: string | null;
|
placeholder?: string | null;
|
||||||
autocomplete?: string;
|
autocomplete?: string;
|
||||||
default?: string | null;
|
default?: string | null;
|
||||||
minLength?: number;
|
minLength?: number;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
}): Promise<{ canceled: true; result: undefined; } | {
|
}): Promise<{
|
||||||
canceled: false; result: string;
|
canceled: true; result: undefined;
|
||||||
|
} | {
|
||||||
|
canceled: false; result: string | null;
|
||||||
|
}>;
|
||||||
|
export function inputText(props: {
|
||||||
|
type?: 'text' | 'email' | 'password' | 'url';
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
|
placeholder?: string | null;
|
||||||
|
autocomplete?: string;
|
||||||
|
default?: string | null;
|
||||||
|
minLength?: number;
|
||||||
|
maxLength?: number;
|
||||||
|
}): Promise<{
|
||||||
|
canceled: true; result: undefined;
|
||||||
|
} | {
|
||||||
|
canceled: false; result: string | null;
|
||||||
}> {
|
}> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
popup(MkDialog, {
|
popup(MkDialog, {
|
||||||
title: props.title,
|
title: props.title,
|
||||||
text: props.text,
|
text: props.text,
|
||||||
|
@ -282,7 +330,7 @@ export function inputText(props: {
|
||||||
type: props.type,
|
type: props.type,
|
||||||
placeholder: props.placeholder,
|
placeholder: props.placeholder,
|
||||||
autocomplete: props.autocomplete,
|
autocomplete: props.autocomplete,
|
||||||
default: props.default,
|
default: props.default ?? null,
|
||||||
minLength: props.minLength,
|
minLength: props.minLength,
|
||||||
maxLength: props.maxLength,
|
maxLength: props.maxLength,
|
||||||
},
|
},
|
||||||
|
@ -294,16 +342,41 @@ export function inputText(props: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// default が指定されていたら result は null になり得ないことを保証する overload function
|
||||||
export function inputNumber(props: {
|
export function inputNumber(props: {
|
||||||
title?: string | null;
|
title?: string;
|
||||||
text?: string | null;
|
text?: string;
|
||||||
|
placeholder?: string | null;
|
||||||
|
autocomplete?: string;
|
||||||
|
default: number;
|
||||||
|
}): Promise<{
|
||||||
|
canceled: true; result: undefined;
|
||||||
|
} | {
|
||||||
|
canceled: false; result: number;
|
||||||
|
}>;
|
||||||
|
export function inputNumber(props: {
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
placeholder?: string | null;
|
placeholder?: string | null;
|
||||||
autocomplete?: string;
|
autocomplete?: string;
|
||||||
default?: number | null;
|
default?: number | null;
|
||||||
}): Promise<{ canceled: true; result: undefined; } | {
|
}): Promise<{
|
||||||
canceled: false; result: number;
|
canceled: true; result: undefined;
|
||||||
|
} | {
|
||||||
|
canceled: false; result: number | null;
|
||||||
|
}>;
|
||||||
|
export function inputNumber(props: {
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
|
placeholder?: string | null;
|
||||||
|
autocomplete?: string;
|
||||||
|
default?: number | null;
|
||||||
|
}): Promise<{
|
||||||
|
canceled: true; result: undefined;
|
||||||
|
} | {
|
||||||
|
canceled: false; result: number | null;
|
||||||
}> {
|
}> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
popup(MkDialog, {
|
popup(MkDialog, {
|
||||||
title: props.title,
|
title: props.title,
|
||||||
text: props.text,
|
text: props.text,
|
||||||
|
@ -311,7 +384,7 @@ export function inputNumber(props: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
placeholder: props.placeholder,
|
placeholder: props.placeholder,
|
||||||
autocomplete: props.autocomplete,
|
autocomplete: props.autocomplete,
|
||||||
default: props.default,
|
default: props.default ?? null,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
done: result => {
|
done: result => {
|
||||||
|
@ -322,34 +395,38 @@ export function inputNumber(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inputDate(props: {
|
export function inputDate(props: {
|
||||||
title?: string | null;
|
title?: string;
|
||||||
text?: string | null;
|
text?: string;
|
||||||
placeholder?: string | null;
|
placeholder?: string | null;
|
||||||
default?: Date | null;
|
default?: string | null;
|
||||||
}): Promise<{ canceled: true; result: undefined; } | {
|
}): Promise<{
|
||||||
|
canceled: true; result: undefined;
|
||||||
|
} | {
|
||||||
canceled: false; result: Date;
|
canceled: false; result: Date;
|
||||||
}> {
|
}> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
popup(MkDialog, {
|
popup(MkDialog, {
|
||||||
title: props.title,
|
title: props.title,
|
||||||
text: props.text,
|
text: props.text,
|
||||||
input: {
|
input: {
|
||||||
type: 'date',
|
type: 'date',
|
||||||
placeholder: props.placeholder,
|
placeholder: props.placeholder,
|
||||||
default: props.default,
|
default: props.default ?? null,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
done: result => {
|
done: result => {
|
||||||
resolve(result ? { result: new Date(result.result), canceled: false } : { canceled: true });
|
resolve(result ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true });
|
||||||
},
|
},
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function authenticateDialog(): Promise<{ canceled: true; result: undefined; } | {
|
export function authenticateDialog(): Promise<{
|
||||||
|
canceled: true; result: undefined;
|
||||||
|
} | {
|
||||||
canceled: false; result: { password: string; token: string | null; };
|
canceled: false; result: { password: string; token: string | null; };
|
||||||
}> {
|
}> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
popup(MkPasswordDialog, {}, {
|
popup(MkPasswordDialog, {}, {
|
||||||
done: result => {
|
done: result => {
|
||||||
resolve(result ? { canceled: false, result } : { canceled: true, result: undefined });
|
resolve(result ? { canceled: false, result } : { canceled: true, result: undefined });
|
||||||
|
@ -358,34 +435,53 @@ export function authenticateDialog(): Promise<{ canceled: true; result: undefine
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// default が指定されていたら result は null になり得ないことを保証する overload function
|
||||||
export function select<C = any>(props: {
|
export function select<C = any>(props: {
|
||||||
title?: string | null;
|
title?: string;
|
||||||
text?: string | null;
|
text?: string;
|
||||||
default?: string | null;
|
default: string;
|
||||||
} & ({
|
|
||||||
items: {
|
items: {
|
||||||
value: C;
|
value: C;
|
||||||
text: string;
|
text: string;
|
||||||
}[];
|
}[];
|
||||||
|
}): Promise<{
|
||||||
|
canceled: true; result: undefined;
|
||||||
} | {
|
} | {
|
||||||
groupedItems: {
|
|
||||||
label: string;
|
|
||||||
items: {
|
|
||||||
value: C;
|
|
||||||
text: string;
|
|
||||||
}[];
|
|
||||||
}[];
|
|
||||||
})): Promise<{ canceled: true; result: undefined; } | {
|
|
||||||
canceled: false; result: C;
|
canceled: false; result: C;
|
||||||
|
}>;
|
||||||
|
export function select<C = any>(props: {
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
|
default?: string | null;
|
||||||
|
items: {
|
||||||
|
value: C;
|
||||||
|
text: string;
|
||||||
|
}[];
|
||||||
|
}): Promise<{
|
||||||
|
canceled: true; result: undefined;
|
||||||
|
} | {
|
||||||
|
canceled: false; result: C | null;
|
||||||
|
}>;
|
||||||
|
export function select<C = any>(props: {
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
|
default?: string | null;
|
||||||
|
items: {
|
||||||
|
value: C;
|
||||||
|
text: string;
|
||||||
|
}[];
|
||||||
|
}): Promise<{
|
||||||
|
canceled: true; result: undefined;
|
||||||
|
} | {
|
||||||
|
canceled: false; result: C | null;
|
||||||
}> {
|
}> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
popup(MkDialog, {
|
popup(MkDialog, {
|
||||||
title: props.title,
|
title: props.title,
|
||||||
text: props.text,
|
text: props.text,
|
||||||
select: {
|
select: {
|
||||||
items: props.items,
|
items: props.items,
|
||||||
groupedItems: props.groupedItems,
|
default: props.default ?? null,
|
||||||
default: props.default,
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
done: result => {
|
done: result => {
|
||||||
|
@ -396,7 +492,7 @@ export function select<C = any>(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function success(): Promise<void> {
|
export function success(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
const showing = ref(true);
|
const showing = ref(true);
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
showing.value = false;
|
showing.value = false;
|
||||||
|
@ -411,7 +507,7 @@ export function success(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function waiting(): Promise<void> {
|
export function waiting(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
const showing = ref(true);
|
const showing = ref(true);
|
||||||
popup(MkWaitingDialog, {
|
popup(MkWaitingDialog, {
|
||||||
success: false,
|
success: false,
|
||||||
|
@ -422,9 +518,9 @@ export function waiting(): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function form(title, form) {
|
export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true } | { result: GetFormResultType<F> }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form }, {
|
popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, {
|
||||||
done: result => {
|
done: result => {
|
||||||
resolve(result);
|
resolve(result);
|
||||||
},
|
},
|
||||||
|
@ -433,7 +529,7 @@ export function form(title, form) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; } = {}): Promise<Misskey.entities.UserDetailed> {
|
export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; } = {}): Promise<Misskey.entities.UserDetailed> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {
|
popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {
|
||||||
includeSelf: opts.includeSelf,
|
includeSelf: opts.includeSelf,
|
||||||
localOnly: opts.localOnly,
|
localOnly: opts.localOnly,
|
||||||
|
@ -446,7 +542,7 @@ export async function selectUser(opts: { includeSelf?: boolean; localOnly?: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function selectDriveFile(multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
|
export async function selectDriveFile(multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
|
popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
|
||||||
type: 'file',
|
type: 'file',
|
||||||
multiple,
|
multiple,
|
||||||
|
@ -460,23 +556,23 @@ export async function selectDriveFile(multiple: boolean): Promise<Misskey.entiti
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function selectDriveFolder(multiple: boolean) {
|
export async function selectDriveFolder(multiple: boolean): Promise<Misskey.entities.DriveFolder[]> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
|
popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
multiple,
|
multiple,
|
||||||
}, {
|
}, {
|
||||||
done: folders => {
|
done: folders => {
|
||||||
if (folders) {
|
if (folders) {
|
||||||
resolve(multiple ? folders : folders[0]);
|
resolve(folders);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pickEmoji(src: HTMLElement | null, opts) {
|
export async function pickEmoji(src: HTMLElement, opts: ComponentProps<typeof MkEmojiPickerDialog>): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
popup(MkEmojiPickerDialog, {
|
popup(MkEmojiPickerDialog, {
|
||||||
src,
|
src,
|
||||||
...opts,
|
...opts,
|
||||||
|
@ -492,7 +588,7 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
|
||||||
aspectRatio: number;
|
aspectRatio: number;
|
||||||
uploadFolder?: string | null;
|
uploadFolder?: string | null;
|
||||||
}): Promise<Misskey.entities.DriveFile> {
|
}): Promise<Misskey.entities.DriveFile> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
|
popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
|
||||||
file: image,
|
file: image,
|
||||||
aspectRatio: options.aspectRatio,
|
aspectRatio: options.aspectRatio,
|
||||||
|
@ -505,67 +601,13 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type AwaitType<T> =
|
export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | null, options?: {
|
||||||
T extends Promise<infer U> ? U :
|
|
||||||
T extends (...args: any[]) => Promise<infer V> ? V :
|
|
||||||
T;
|
|
||||||
let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
|
|
||||||
let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
|
|
||||||
export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: typeof activeTextarea) {
|
|
||||||
if (openingEmojiPicker) return;
|
|
||||||
|
|
||||||
activeTextarea = initialTextarea;
|
|
||||||
|
|
||||||
const textareas = document.querySelectorAll('textarea, input');
|
|
||||||
for (const textarea of Array.from(textareas)) {
|
|
||||||
textarea.addEventListener('focus', () => {
|
|
||||||
activeTextarea = textarea;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new MutationObserver(records => {
|
|
||||||
for (const record of records) {
|
|
||||||
for (const node of Array.from(record.addedNodes).filter(node => node instanceof HTMLElement) as HTMLElement[]) {
|
|
||||||
const textareas = node.querySelectorAll('textarea, input') as NodeListOf<NonNullable<typeof activeTextarea>>;
|
|
||||||
for (const textarea of Array.from(textareas).filter(textarea => textarea.dataset.preventEmojiInsert == null)) {
|
|
||||||
if (document.activeElement === textarea) activeTextarea = textarea;
|
|
||||||
textarea.addEventListener('focus', () => {
|
|
||||||
activeTextarea = textarea;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.body, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
attributes: false,
|
|
||||||
characterData: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
openingEmojiPicker = await popup(MkEmojiPickerWindow, {
|
|
||||||
src,
|
|
||||||
...opts,
|
|
||||||
}, {
|
|
||||||
chosen: emoji => {
|
|
||||||
insertTextAtCursor(activeTextarea, emoji);
|
|
||||||
},
|
|
||||||
closed: () => {
|
|
||||||
openingEmojiPicker!.dispose();
|
|
||||||
openingEmojiPicker = null;
|
|
||||||
observer.disconnect();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement | EventTarget | null, options?: {
|
|
||||||
align?: string;
|
align?: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
viaKeyboard?: boolean;
|
viaKeyboard?: boolean;
|
||||||
onClosing?: () => void;
|
onClosing?: () => void;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
let dispose;
|
let dispose;
|
||||||
popup(MkPopupMenu, {
|
popup(MkPopupMenu, {
|
||||||
items,
|
items,
|
||||||
|
@ -587,9 +629,9 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent): Promise<void> {
|
export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
let dispose;
|
let dispose;
|
||||||
popup(MkContextMenu, {
|
popup(MkContextMenu, {
|
||||||
items,
|
items,
|
||||||
|
@ -608,7 +650,7 @@ export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent)
|
||||||
export function post(props: Record<string, any> = {}): Promise<void> {
|
export function post(props: Record<string, any> = {}): Promise<void> {
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
|
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
|
||||||
// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
|
// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
|
||||||
// Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、
|
// Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、
|
||||||
|
|
|
@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSelect v-model="type" :class="$style.typeSelect">
|
<MkSelect v-model="type" :class="$style.typeSelect">
|
||||||
<option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option>
|
<option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option>
|
||||||
<option value="isRemote">{{ i18n.ts._role._condition.isRemote }}</option>
|
<option value="isRemote">{{ i18n.ts._role._condition.isRemote }}</option>
|
||||||
|
<option value="roleAssignedTo">{{ i18n.ts._role._condition.roleAssignedTo }}</option>
|
||||||
<option value="createdLessThan">{{ i18n.ts._role._condition.createdLessThan }}</option>
|
<option value="createdLessThan">{{ i18n.ts._role._condition.createdLessThan }}</option>
|
||||||
<option value="createdMoreThan">{{ i18n.ts._role._condition.createdMoreThan }}</option>
|
<option value="createdMoreThan">{{ i18n.ts._role._condition.createdMoreThan }}</option>
|
||||||
<option value="followersLessThanOrEq">{{ i18n.ts._role._condition.followersLessThanOrEq }}</option>
|
<option value="followersLessThanOrEq">{{ i18n.ts._role._condition.followersLessThanOrEq }}</option>
|
||||||
|
@ -51,6 +52,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq', 'notesLessThanOrEq', 'notesMoreThanOrEq'].includes(type)" v-model="v.value" type="number">
|
<MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq', 'notesLessThanOrEq', 'notesMoreThanOrEq'].includes(type)" v-model="v.value" type="number">
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
|
<MkSelect v-else-if="type === 'roleAssignedTo'" v-model="v.roleId">
|
||||||
|
<option v-for="role in roles.filter(r => r.target === 'manual')" :key="role.id" :value="role.id">{{ role.name }}</option>
|
||||||
|
</MkSelect>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -62,6 +67,7 @@ import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
|
import { rolesCache } from '@/cache.js';
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||||
|
|
||||||
|
@ -77,6 +83,8 @@ const props = defineProps<{
|
||||||
|
|
||||||
const v = ref(deepClone(props.modelValue));
|
const v = ref(deepClone(props.modelValue));
|
||||||
|
|
||||||
|
const roles = await rolesCache.fetch();
|
||||||
|
|
||||||
watch(() => props.modelValue, () => {
|
watch(() => props.modelValue, () => {
|
||||||
if (JSON.stringify(props.modelValue) === JSON.stringify(v.value)) return;
|
if (JSON.stringify(props.modelValue) === JSON.stringify(v.value)) return;
|
||||||
v.value = deepClone(props.modelValue);
|
v.value = deepClone(props.modelValue);
|
||||||
|
@ -92,6 +100,7 @@ const type = computed({
|
||||||
if (t === 'and') v.value.values = [];
|
if (t === 'and') v.value.values = [];
|
||||||
if (t === 'or') v.value.values = [];
|
if (t === 'or') v.value.values = [];
|
||||||
if (t === 'not') v.value.value = { id: uuid(), type: 'isRemote' };
|
if (t === 'not') v.value.value = { id: uuid(), type: 'isRemote' };
|
||||||
|
if (t === 'roleAssignedTo') v.value.roleId = '';
|
||||||
if (t === 'createdLessThan') v.value.sec = 86400;
|
if (t === 'createdLessThan') v.value.sec = 86400;
|
||||||
if (t === 'createdMoreThan') v.value.sec = 86400;
|
if (t === 'createdMoreThan') v.value.sec = 86400;
|
||||||
if (t === 'followersLessThanOrEq') v.value.value = 10;
|
if (t === 'followersLessThanOrEq') v.value.value = 10;
|
||||||
|
|
|
@ -135,7 +135,7 @@ async function addRole() {
|
||||||
const { canceled, result: role } = await os.select({
|
const { canceled, result: role } = await os.select({
|
||||||
items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
|
items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled || role == null) return;
|
||||||
|
|
||||||
rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
|
rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ const directNotesPagination = {
|
||||||
function setFilter(ev) {
|
function setFilter(ev) {
|
||||||
const typeItems = notificationTypes.map(t => ({
|
const typeItems = notificationTypes.map(t => ({
|
||||||
text: i18n.ts._notification._types[t],
|
text: i18n.ts._notification._types[t],
|
||||||
active: includeTypes.value && includeTypes.value.includes(t),
|
active: (includeTypes.value && includeTypes.value.includes(t)) ?? false,
|
||||||
action: () => {
|
action: () => {
|
||||||
includeTypes.value = [t];
|
includeTypes.value = [t];
|
||||||
},
|
},
|
||||||
|
@ -63,7 +63,7 @@ function setFilter(ev) {
|
||||||
action: () => {
|
action: () => {
|
||||||
includeTypes.value = null;
|
includeTypes.value = null;
|
||||||
},
|
},
|
||||||
}, { type: 'divider' }, ...typeItems] : typeItems;
|
}, { type: 'divider' as const }, ...typeItems] : typeItems;
|
||||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,7 +113,7 @@ if (defaultStore.state.uploadFolder) {
|
||||||
|
|
||||||
function chooseUploadFolder() {
|
function chooseUploadFolder() {
|
||||||
os.selectDriveFolder(false).then(async folder => {
|
os.selectDriveFolder(false).then(async folder => {
|
||||||
defaultStore.set('uploadFolder', folder ? folder.id : null);
|
defaultStore.set('uploadFolder', folder[0] ? folder[0].id : null);
|
||||||
os.success();
|
os.success();
|
||||||
if (defaultStore.state.uploadFolder) {
|
if (defaultStore.state.uploadFolder) {
|
||||||
uploadFolder.value = await misskeyApi('drive/folders/show', {
|
uploadFolder.value = await misskeyApi('drive/folders/show', {
|
||||||
|
|
|
@ -213,7 +213,7 @@ async function pickEmoji(itemsRef: Ref<string[]>, ev: MouseEvent) {
|
||||||
os.pickEmoji(getHTMLElement(ev), {
|
os.pickEmoji(getHTMLElement(ev), {
|
||||||
showPinned: false,
|
showPinned: false,
|
||||||
}).then(it => {
|
}).then(it => {
|
||||||
const emoji = it as string;
|
const emoji = it;
|
||||||
if (!itemsRef.value.includes(emoji)) {
|
if (!itemsRef.value.includes(emoji)) {
|
||||||
itemsRef.value.push(emoji);
|
itemsRef.value.push(emoji);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<option value="following">{{ i18n.ts.following }}</option>
|
<option value="following">{{ i18n.ts.following }}</option>
|
||||||
<option value="follower">{{ i18n.ts.followers }}</option>
|
<option value="follower">{{ i18n.ts.followers }}</option>
|
||||||
<option value="mutualFollow">{{ i18n.ts.mutualFollow }}</option>
|
<option value="mutualFollow">{{ i18n.ts.mutualFollow }}</option>
|
||||||
|
<option value="followingOrFollower">{{ i18n.ts.followingOrFollower }}</option>
|
||||||
<option value="list">{{ i18n.ts.userList }}</option>
|
<option value="list">{{ i18n.ts.userList }}</option>
|
||||||
<option value="never">{{ i18n.ts.none }}</option>
|
<option value="never">{{ i18n.ts.none }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
|
@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
$i.notificationRecieveConfig[type]?.type === 'following' ? i18n.ts.following :
|
$i.notificationRecieveConfig[type]?.type === 'following' ? i18n.ts.following :
|
||||||
$i.notificationRecieveConfig[type]?.type === 'follower' ? i18n.ts.followers :
|
$i.notificationRecieveConfig[type]?.type === 'follower' ? i18n.ts.followers :
|
||||||
$i.notificationRecieveConfig[type]?.type === 'mutualFollow' ? i18n.ts.mutualFollow :
|
$i.notificationRecieveConfig[type]?.type === 'mutualFollow' ? i18n.ts.mutualFollow :
|
||||||
|
$i.notificationRecieveConfig[type]?.type === 'followingOrFollower' ? i18n.ts.followingOrFollower :
|
||||||
$i.notificationRecieveConfig[type]?.type === 'list' ? i18n.ts.userList :
|
$i.notificationRecieveConfig[type]?.type === 'list' ? i18n.ts.userList :
|
||||||
i18n.ts.all
|
i18n.ts.all
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -203,6 +203,7 @@ async function saveNew(): Promise<void> {
|
||||||
|
|
||||||
const { canceled, result: name } = await os.inputText({
|
const { canceled, result: name } = await os.inputText({
|
||||||
title: ts._preferencesBackups.inputName,
|
title: ts._preferencesBackups.inputName,
|
||||||
|
default: '',
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
|
@ -371,6 +372,7 @@ async function rename(id: string): Promise<void> {
|
||||||
|
|
||||||
const { canceled: cancel1, result: name } = await os.inputText({
|
const { canceled: cancel1, result: name } = await os.inputText({
|
||||||
title: ts._preferencesBackups.inputName,
|
title: ts._preferencesBackups.inputName,
|
||||||
|
default: '',
|
||||||
});
|
});
|
||||||
if (cancel1 || profiles.value[id].name === name) return;
|
if (cancel1 || profiles.value[id].name === name) return;
|
||||||
|
|
||||||
|
|
|
@ -12,29 +12,37 @@ export type FormItem = {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'string';
|
type: 'string';
|
||||||
default: string | null;
|
default: string | null;
|
||||||
|
description?: string;
|
||||||
|
required?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
|
treatAsMfm?: boolean;
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'number';
|
type: 'number';
|
||||||
default: number | null;
|
default: number | null;
|
||||||
|
description?: string;
|
||||||
|
required?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
step?: number;
|
step?: number;
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'boolean';
|
type: 'boolean';
|
||||||
default: boolean | null;
|
default: boolean | null;
|
||||||
|
description?: string;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'enum';
|
type: 'enum';
|
||||||
default: string | null;
|
default: string | null;
|
||||||
|
required?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
enum: EnumItem[];
|
enum: EnumItem[];
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'radio';
|
type: 'radio';
|
||||||
default: unknown | null;
|
default: unknown | null;
|
||||||
|
required?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
options: {
|
options: {
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -44,9 +52,12 @@ export type FormItem = {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'range';
|
type: 'range';
|
||||||
default: number | null;
|
default: number | null;
|
||||||
step: number;
|
description?: string;
|
||||||
|
required?: boolean;
|
||||||
|
step?: number;
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
|
textConverter?: (value: number) => string;
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'object';
|
type: 'object';
|
||||||
|
@ -57,6 +68,10 @@ export type FormItem = {
|
||||||
type: 'array';
|
type: 'array';
|
||||||
default: unknown[] | null;
|
default: unknown[] | null;
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
|
} | {
|
||||||
|
type: 'button';
|
||||||
|
content?: string;
|
||||||
|
action: (ev: MouseEvent, v: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Form = Record<string, FormItem>;
|
export type Form = Record<string, FormItem>;
|
||||||
|
|
|
@ -117,6 +117,7 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue';
|
||||||
import XDirectColumn from '@/ui/deck/direct-column.vue';
|
import XDirectColumn from '@/ui/deck/direct-column.vue';
|
||||||
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
|
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
|
||||||
import { mainRouter } from '@/router/main.js';
|
import { mainRouter } from '@/router/main.js';
|
||||||
|
import { MenuItem } from '@/types/menu.js';
|
||||||
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
||||||
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
|
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
|
||||||
|
|
||||||
|
@ -221,21 +222,19 @@ document.documentElement.style.scrollBehavior = 'auto';
|
||||||
loadDeck();
|
loadDeck();
|
||||||
|
|
||||||
function changeProfile(ev: MouseEvent) {
|
function changeProfile(ev: MouseEvent) {
|
||||||
const items = ref([{
|
let items: MenuItem[] = [{
|
||||||
text: deckStore.state.profile,
|
text: deckStore.state.profile,
|
||||||
active: true.valueOf,
|
active: true,
|
||||||
}]);
|
action: () => {},
|
||||||
|
}];
|
||||||
getProfiles().then(profiles => {
|
getProfiles().then(profiles => {
|
||||||
items.value = [{
|
items.push(...(profiles.filter(k => k !== deckStore.state.profile).map(k => ({
|
||||||
text: deckStore.state.profile,
|
|
||||||
active: true.valueOf,
|
|
||||||
}, ...(profiles.filter(k => k !== deckStore.state.profile).map(k => ({
|
|
||||||
text: k,
|
text: k,
|
||||||
action: () => {
|
action: () => {
|
||||||
deckStore.set('profile', k);
|
deckStore.set('profile', k);
|
||||||
unisonReload();
|
unisonReload();
|
||||||
},
|
},
|
||||||
}))), { type: 'divider' }, {
|
}))), { type: 'divider' as const }, {
|
||||||
text: i18n.ts._deck.newProfile,
|
text: i18n.ts._deck.newProfile,
|
||||||
icon: 'ti ti-plus',
|
icon: 'ti ti-plus',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
|
@ -248,9 +247,10 @@ function changeProfile(ev: MouseEvent) {
|
||||||
deckStore.set('profile', name);
|
deckStore.set('profile', name);
|
||||||
unisonReload();
|
unisonReload();
|
||||||
},
|
},
|
||||||
}];
|
});
|
||||||
|
}).then(() => {
|
||||||
|
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||||
});
|
});
|
||||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteProfile() {
|
async function deleteProfile() {
|
||||||
|
|
|
@ -93,10 +93,10 @@ const fetch = () => {
|
||||||
|
|
||||||
const choose = () => {
|
const choose = () => {
|
||||||
os.selectDriveFolder(false).then(folder => {
|
os.selectDriveFolder(false).then(folder => {
|
||||||
if (folder == null) {
|
if (folder[0] == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
widgetProps.folderId = folder.id;
|
widgetProps.folderId = folder[0].id;
|
||||||
save();
|
save();
|
||||||
fetch();
|
fetch();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1712,6 +1712,7 @@ declare namespace entities {
|
||||||
RoleCondFormulaLogics,
|
RoleCondFormulaLogics,
|
||||||
RoleCondFormulaValueNot,
|
RoleCondFormulaValueNot,
|
||||||
RoleCondFormulaValueIsLocalOrRemote,
|
RoleCondFormulaValueIsLocalOrRemote,
|
||||||
|
RoleCondFormulaValueAssignedRole,
|
||||||
RoleCondFormulaValueCreated,
|
RoleCondFormulaValueCreated,
|
||||||
RoleCondFormulaFollowersOrFollowingOrNotes,
|
RoleCondFormulaFollowersOrFollowingOrNotes,
|
||||||
RoleCondFormulaValue,
|
RoleCondFormulaValue,
|
||||||
|
@ -2731,6 +2732,9 @@ type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics'];
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue'];
|
type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type RoleCondFormulaValueAssignedRole = components['schemas']['RoleCondFormulaValueAssignedRole'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated'];
|
type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated'];
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ export type Signin = components['schemas']['Signin'];
|
||||||
export type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics'];
|
export type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics'];
|
||||||
export type RoleCondFormulaValueNot = components['schemas']['RoleCondFormulaValueNot'];
|
export type RoleCondFormulaValueNot = components['schemas']['RoleCondFormulaValueNot'];
|
||||||
export type RoleCondFormulaValueIsLocalOrRemote = components['schemas']['RoleCondFormulaValueIsLocalOrRemote'];
|
export type RoleCondFormulaValueIsLocalOrRemote = components['schemas']['RoleCondFormulaValueIsLocalOrRemote'];
|
||||||
|
export type RoleCondFormulaValueAssignedRole = components['schemas']['RoleCondFormulaValueAssignedRole'];
|
||||||
export type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated'];
|
export type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated'];
|
||||||
export type RoleCondFormulaFollowersOrFollowingOrNotes = components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes'];
|
export type RoleCondFormulaFollowersOrFollowingOrNotes = components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes'];
|
||||||
export type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue'];
|
export type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue'];
|
||||||
|
|
|
@ -3700,7 +3700,7 @@ export type components = {
|
||||||
notificationRecieveConfig: {
|
notificationRecieveConfig: {
|
||||||
note?: OneOf<[{
|
note?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -3709,7 +3709,7 @@ export type components = {
|
||||||
}]>;
|
}]>;
|
||||||
follow?: OneOf<[{
|
follow?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -3718,7 +3718,7 @@ export type components = {
|
||||||
}]>;
|
}]>;
|
||||||
mention?: OneOf<[{
|
mention?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -3727,7 +3727,7 @@ export type components = {
|
||||||
}]>;
|
}]>;
|
||||||
reply?: OneOf<[{
|
reply?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -3736,7 +3736,7 @@ export type components = {
|
||||||
}]>;
|
}]>;
|
||||||
renote?: OneOf<[{
|
renote?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -3745,7 +3745,7 @@ export type components = {
|
||||||
}]>;
|
}]>;
|
||||||
quote?: OneOf<[{
|
quote?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -3754,7 +3754,7 @@ export type components = {
|
||||||
}]>;
|
}]>;
|
||||||
reaction?: OneOf<[{
|
reaction?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -3763,7 +3763,7 @@ export type components = {
|
||||||
}]>;
|
}]>;
|
||||||
pollEnded?: OneOf<[{
|
pollEnded?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -3772,7 +3772,7 @@ export type components = {
|
||||||
}]>;
|
}]>;
|
||||||
receiveFollowRequest?: OneOf<[{
|
receiveFollowRequest?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -3781,7 +3781,7 @@ export type components = {
|
||||||
}]>;
|
}]>;
|
||||||
followRequestAccepted?: OneOf<[{
|
followRequestAccepted?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -3790,7 +3790,7 @@ export type components = {
|
||||||
}]>;
|
}]>;
|
||||||
roleAssigned?: OneOf<[{
|
roleAssigned?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -3799,7 +3799,7 @@ export type components = {
|
||||||
}]>;
|
}]>;
|
||||||
achievementEarned?: OneOf<[{
|
achievementEarned?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -3808,7 +3808,7 @@ export type components = {
|
||||||
}]>;
|
}]>;
|
||||||
app?: OneOf<[{
|
app?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -3817,7 +3817,7 @@ export type components = {
|
||||||
}]>;
|
}]>;
|
||||||
test?: OneOf<[{
|
test?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -4573,6 +4573,15 @@ export type components = {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'isLocal' | 'isRemote';
|
type: 'isLocal' | 'isRemote';
|
||||||
};
|
};
|
||||||
|
RoleCondFormulaValueAssignedRole: {
|
||||||
|
/** @enum {string} */
|
||||||
|
type: 'roleAssignedTo';
|
||||||
|
/**
|
||||||
|
* Format: id
|
||||||
|
* @example xxxxxxxxxx
|
||||||
|
*/
|
||||||
|
roleId: string;
|
||||||
|
};
|
||||||
RoleCondFormulaValueCreated: {
|
RoleCondFormulaValueCreated: {
|
||||||
id: string;
|
id: string;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
|
@ -4585,7 +4594,7 @@ export type components = {
|
||||||
type: 'followersLessThanOrEq' | 'followersMoreThanOrEq' | 'followingLessThanOrEq' | 'followingMoreThanOrEq' | 'notesLessThanOrEq' | 'notesMoreThanOrEq';
|
type: 'followersLessThanOrEq' | 'followersMoreThanOrEq' | 'followingLessThanOrEq' | 'followingMoreThanOrEq' | 'notesLessThanOrEq' | 'notesMoreThanOrEq';
|
||||||
value: number;
|
value: number;
|
||||||
};
|
};
|
||||||
RoleCondFormulaValue: components['schemas']['RoleCondFormulaLogics'] | components['schemas']['RoleCondFormulaValueNot'] | components['schemas']['RoleCondFormulaValueIsLocalOrRemote'] | components['schemas']['RoleCondFormulaValueCreated'] | components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes'];
|
RoleCondFormulaValue: components['schemas']['RoleCondFormulaLogics'] | components['schemas']['RoleCondFormulaValueNot'] | components['schemas']['RoleCondFormulaValueIsLocalOrRemote'] | components['schemas']['RoleCondFormulaValueAssignedRole'] | components['schemas']['RoleCondFormulaValueCreated'] | components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes'];
|
||||||
RoleLite: {
|
RoleLite: {
|
||||||
/**
|
/**
|
||||||
* Format: id
|
* Format: id
|
||||||
|
@ -8427,7 +8436,7 @@ export type operations = {
|
||||||
notificationRecieveConfig: {
|
notificationRecieveConfig: {
|
||||||
note?: OneOf<[{
|
note?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -8436,7 +8445,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
follow?: OneOf<[{
|
follow?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -8445,7 +8454,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
mention?: OneOf<[{
|
mention?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -8454,7 +8463,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
reply?: OneOf<[{
|
reply?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -8463,7 +8472,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
renote?: OneOf<[{
|
renote?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -8472,7 +8481,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
quote?: OneOf<[{
|
quote?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -8481,7 +8490,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
reaction?: OneOf<[{
|
reaction?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -8490,7 +8499,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
pollEnded?: OneOf<[{
|
pollEnded?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -8499,7 +8508,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
receiveFollowRequest?: OneOf<[{
|
receiveFollowRequest?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -8508,7 +8517,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
followRequestAccepted?: OneOf<[{
|
followRequestAccepted?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -8517,7 +8526,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
roleAssigned?: OneOf<[{
|
roleAssigned?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -8526,7 +8535,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
achievementEarned?: OneOf<[{
|
achievementEarned?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -8535,7 +8544,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
app?: OneOf<[{
|
app?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -8544,7 +8553,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
test?: OneOf<[{
|
test?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -17678,8 +17687,8 @@ export type operations = {
|
||||||
untilId?: string;
|
untilId?: string;
|
||||||
/** @default true */
|
/** @default true */
|
||||||
markAsRead?: boolean;
|
markAsRead?: boolean;
|
||||||
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
|
||||||
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
|
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -18778,7 +18787,7 @@ export type operations = {
|
||||||
notificationRecieveConfig?: {
|
notificationRecieveConfig?: {
|
||||||
note?: OneOf<[{
|
note?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -18787,7 +18796,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
follow?: OneOf<[{
|
follow?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -18796,7 +18805,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
mention?: OneOf<[{
|
mention?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -18805,7 +18814,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
reply?: OneOf<[{
|
reply?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -18814,7 +18823,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
renote?: OneOf<[{
|
renote?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -18823,7 +18832,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
quote?: OneOf<[{
|
quote?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -18832,7 +18841,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
reaction?: OneOf<[{
|
reaction?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -18841,7 +18850,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
pollEnded?: OneOf<[{
|
pollEnded?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -18850,7 +18859,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
receiveFollowRequest?: OneOf<[{
|
receiveFollowRequest?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -18859,7 +18868,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
followRequestAccepted?: OneOf<[{
|
followRequestAccepted?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -18868,7 +18877,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
roleAssigned?: OneOf<[{
|
roleAssigned?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -18877,7 +18886,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
achievementEarned?: OneOf<[{
|
achievementEarned?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -18886,7 +18895,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
app?: OneOf<[{
|
app?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
@ -18895,7 +18904,7 @@ export type operations = {
|
||||||
}]>;
|
}]>;
|
||||||
test?: OneOf<[{
|
test?: OneOf<[{
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never';
|
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
|
||||||
}, {
|
}, {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'list';
|
type: 'list';
|
||||||
|
|
Loading…
Reference in New Issue