いいかんじ
This commit is contained in:
parent
7dbbfb657c
commit
582a6948e5
|
@ -1136,9 +1136,6 @@ export interface Locale {
|
||||||
"authenticationRequiredToContinue": string;
|
"authenticationRequiredToContinue": string;
|
||||||
"dateAndTime": string;
|
"dateAndTime": string;
|
||||||
"showRenotes": string;
|
"showRenotes": string;
|
||||||
"edited": string;
|
|
||||||
"notificationRecieveConfig": string;
|
|
||||||
"mutualFollow": string;
|
|
||||||
"_announcement": {
|
"_announcement": {
|
||||||
"forExistingUsers": string;
|
"forExistingUsers": string;
|
||||||
"forExistingUsersDescription": string;
|
"forExistingUsersDescription": string;
|
||||||
|
|
|
@ -1133,10 +1133,6 @@ authentication: "認証"
|
||||||
authenticationRequiredToContinue: "続けるには認証を行ってください"
|
authenticationRequiredToContinue: "続けるには認証を行ってください"
|
||||||
dateAndTime: "日時"
|
dateAndTime: "日時"
|
||||||
showRenotes: "リノートを表示"
|
showRenotes: "リノートを表示"
|
||||||
edited: "編集済み"
|
|
||||||
notificationRecieveConfig: "通知の受信設定"
|
|
||||||
mutualFollow: "相互フォロー"
|
|
||||||
fileAttachedOnly: "ファイル付きのみ"
|
|
||||||
|
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "既存ユーザーのみ"
|
forExistingUsers: "既存ユーザーのみ"
|
||||||
|
@ -2190,7 +2186,6 @@ _moderationLogTypes:
|
||||||
updateRole: "ロールを更新"
|
updateRole: "ロールを更新"
|
||||||
assignRole: "ロールへアサイン"
|
assignRole: "ロールへアサイン"
|
||||||
unassignRole: "ロールのアサイン解除"
|
unassignRole: "ロールのアサイン解除"
|
||||||
updateRole: "ロール設定更新"
|
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
unsuspend: "凍結解除"
|
unsuspend: "凍結解除"
|
||||||
addCustomEmoji: "カスタム絵文字追加"
|
addCustomEmoji: "カスタム絵文字追加"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2023.9.2",
|
"version": "2023.9.2-prismisskey.1",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
export class NoteUpdatedAt1695901659683 {
|
||||||
|
name = 'NoteUpdatedAt1695901659683'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class NotificationRecieveConfig1695944637565 {
|
||||||
|
name = 'NotificationRecieveConfig1695944637565'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutingNotificationTypes"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ADD "notificationRecieveConfig" jsonb NOT NULL DEFAULT '{}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "notificationRecieveConfig"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutingNotificationTypes" "public"."user_profile_notificationrecieveconfig_enum" array NOT NULL DEFAULT '{}'`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import type { AntennasRepository, UserListJoiningsRepository } from '@/models/_.js';
|
import type { AntennasRepository, UserListJoiningsRepository } from '@/models/_.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -50,7 +50,7 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'antennaCreated':
|
case 'antennaCreated':
|
||||||
this.antennas.push({
|
this.antennas.push({
|
||||||
|
|
|
@ -11,7 +11,7 @@ import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -160,7 +160,7 @@ export class CacheService implements OnApplicationShutdown {
|
||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'userChangeSuspendedState':
|
case 'userChangeSuspendedState':
|
||||||
case 'remoteUserUpdated': {
|
case 'remoteUserUpdated': {
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { query } from '@/misc/prelude/url.js';
|
import { query } from '@/misc/prelude/url.js';
|
||||||
import type { Serialized } from '@/server/api/stream/types.js';
|
import type { Serialized } from '@/types.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
|
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
|
||||||
|
|
|
@ -5,27 +5,254 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
|
import type { MiChannel } from '@/models/Channel.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
|
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import type { MiUserList } from '@/models/UserList.js';
|
|
||||||
import type { MiAntenna } from '@/models/Antenna.js';
|
import type { MiAntenna } from '@/models/Antenna.js';
|
||||||
import type {
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
StreamChannels,
|
import type { MiDriveFolder } from '@/models/DriveFolder.js';
|
||||||
AdminStreamTypes,
|
import type { MiUserList } from '@/models/UserList.js';
|
||||||
AntennaStreamTypes,
|
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||||
BroadcastTypes,
|
import type { MiSignin } from '@/models/Signin.js';
|
||||||
DriveStreamTypes,
|
import type { MiPage } from '@/models/Page.js';
|
||||||
InternalStreamTypes,
|
import type { MiWebhook } from '@/models/Webhook.js';
|
||||||
MainStreamTypes,
|
import type { MiMeta } from '@/models/Meta.js';
|
||||||
NoteStreamTypes,
|
import { MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||||
UserListStreamTypes,
|
|
||||||
RoleTimelineStreamTypes,
|
|
||||||
} from '@/server/api/stream/types.js';
|
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MiRole } from '@/models/_.js';
|
import { Serialized } from '@/types.js';
|
||||||
|
import type Emitter from 'strict-event-emitter-types';
|
||||||
|
import type { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
//#region Stream type-body definitions
|
||||||
|
export interface BroadcastTypes {
|
||||||
|
emojiAdded: {
|
||||||
|
emoji: Packed<'EmojiDetailed'>;
|
||||||
|
};
|
||||||
|
emojiUpdated: {
|
||||||
|
emojis: Packed<'EmojiDetailed'>[];
|
||||||
|
};
|
||||||
|
emojiDeleted: {
|
||||||
|
emojis: {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
[other: string]: any;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
announcementCreated: {
|
||||||
|
announcement: Packed<'Announcement'>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainEventTypes {
|
||||||
|
notification: Packed<'Notification'>;
|
||||||
|
mention: Packed<'Note'>;
|
||||||
|
reply: Packed<'Note'>;
|
||||||
|
renote: Packed<'Note'>;
|
||||||
|
follow: Packed<'UserDetailedNotMe'>;
|
||||||
|
followed: Packed<'User'>;
|
||||||
|
unfollow: Packed<'User'>;
|
||||||
|
meUpdated: Packed<'User'>;
|
||||||
|
pageEvent: {
|
||||||
|
pageId: MiPage['id'];
|
||||||
|
event: string;
|
||||||
|
var: any;
|
||||||
|
userId: MiUser['id'];
|
||||||
|
user: Packed<'User'>;
|
||||||
|
};
|
||||||
|
urlUploadFinished: {
|
||||||
|
marker?: string | null;
|
||||||
|
file: Packed<'DriveFile'>;
|
||||||
|
};
|
||||||
|
readAllNotifications: undefined;
|
||||||
|
unreadNotification: Packed<'Notification'>;
|
||||||
|
unreadMention: MiNote['id'];
|
||||||
|
readAllUnreadMentions: undefined;
|
||||||
|
unreadSpecifiedNote: MiNote['id'];
|
||||||
|
readAllUnreadSpecifiedNotes: undefined;
|
||||||
|
readAllAntennas: undefined;
|
||||||
|
unreadAntenna: MiAntenna;
|
||||||
|
readAllAnnouncements: undefined;
|
||||||
|
myTokenRegenerated: undefined;
|
||||||
|
signin: MiSignin;
|
||||||
|
registryUpdated: {
|
||||||
|
scope?: string[];
|
||||||
|
key: string;
|
||||||
|
value: any | null;
|
||||||
|
};
|
||||||
|
driveFileCreated: Packed<'DriveFile'>;
|
||||||
|
readAntenna: MiAntenna;
|
||||||
|
receiveFollowRequest: Packed<'User'>;
|
||||||
|
announcementCreated: {
|
||||||
|
announcement: Packed<'Announcement'>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DriveEventTypes {
|
||||||
|
fileCreated: Packed<'DriveFile'>;
|
||||||
|
fileDeleted: MiDriveFile['id'];
|
||||||
|
fileUpdated: Packed<'DriveFile'>;
|
||||||
|
folderCreated: Packed<'DriveFolder'>;
|
||||||
|
folderDeleted: MiDriveFolder['id'];
|
||||||
|
folderUpdated: Packed<'DriveFolder'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoteEventTypes {
|
||||||
|
pollVoted: {
|
||||||
|
choice: number;
|
||||||
|
userId: MiUser['id'];
|
||||||
|
};
|
||||||
|
deleted: {
|
||||||
|
deletedAt: Date;
|
||||||
|
};
|
||||||
|
updated: {
|
||||||
|
cw: string | null;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
reacted: {
|
||||||
|
reaction: string;
|
||||||
|
emoji?: {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
} | null;
|
||||||
|
userId: MiUser['id'];
|
||||||
|
};
|
||||||
|
unreacted: {
|
||||||
|
reaction: string;
|
||||||
|
userId: MiUser['id'];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
type NoteStreamEventTypes = {
|
||||||
|
[key in keyof NoteEventTypes]: {
|
||||||
|
id: MiNote['id'];
|
||||||
|
body: NoteEventTypes[key];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface UserListEventTypes {
|
||||||
|
userAdded: Packed<'User'>;
|
||||||
|
userRemoved: Packed<'User'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AntennaEventTypes {
|
||||||
|
note: MiNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleTimelineEventTypes {
|
||||||
|
note: Packed<'Note'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminEventTypes {
|
||||||
|
newAbuseUserReport: {
|
||||||
|
id: MiAbuseUserReport['id'];
|
||||||
|
targetUserId: MiUser['id'],
|
||||||
|
reporterId: MiUser['id'],
|
||||||
|
comment: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
// 辞書(interface or type)から{ type, body }ユニオンを定義
|
||||||
|
// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type
|
||||||
|
// VS Codeの展開を防止するためにEvents型を定義
|
||||||
|
type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
|
||||||
|
type EventUnionFromDictionary<
|
||||||
|
T extends object,
|
||||||
|
U = Events<T>
|
||||||
|
> = U[keyof U];
|
||||||
|
|
||||||
|
type SerializedAll<T> = {
|
||||||
|
[K in keyof T]: Serialized<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface InternalEventTypes {
|
||||||
|
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
|
||||||
|
userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; };
|
||||||
|
remoteUserUpdated: { id: MiUser['id']; };
|
||||||
|
follow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
|
||||||
|
unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
|
||||||
|
blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||||
|
blockingDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||||
|
policiesUpdated: MiRole['policies'];
|
||||||
|
roleCreated: MiRole;
|
||||||
|
roleDeleted: MiRole;
|
||||||
|
roleUpdated: MiRole;
|
||||||
|
userRoleAssigned: MiRoleAssignment;
|
||||||
|
userRoleUnassigned: MiRoleAssignment;
|
||||||
|
webhookCreated: MiWebhook;
|
||||||
|
webhookDeleted: MiWebhook;
|
||||||
|
webhookUpdated: MiWebhook;
|
||||||
|
antennaCreated: MiAntenna;
|
||||||
|
antennaDeleted: MiAntenna;
|
||||||
|
antennaUpdated: MiAntenna;
|
||||||
|
metaUpdated: MiMeta;
|
||||||
|
followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
||||||
|
unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
||||||
|
updateUserProfile: MiUserProfile;
|
||||||
|
mute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
|
||||||
|
unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
|
||||||
|
userListMemberAdded: { userListId: MiUserList['id']; memberId: MiUser['id']; };
|
||||||
|
userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
|
||||||
|
}
|
||||||
|
|
||||||
|
// name/messages(spec) pairs dictionary
|
||||||
|
export type GlobalEvents = {
|
||||||
|
internal: {
|
||||||
|
name: 'internal';
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>;
|
||||||
|
};
|
||||||
|
broadcast: {
|
||||||
|
name: 'broadcast';
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
|
||||||
|
};
|
||||||
|
main: {
|
||||||
|
name: `mainStream:${MiUser['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>;
|
||||||
|
};
|
||||||
|
drive: {
|
||||||
|
name: `driveStream:${MiUser['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>;
|
||||||
|
};
|
||||||
|
note: {
|
||||||
|
name: `noteStream:${MiNote['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
|
||||||
|
};
|
||||||
|
userList: {
|
||||||
|
name: `userListStream:${MiUserList['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>;
|
||||||
|
};
|
||||||
|
roleTimeline: {
|
||||||
|
name: `roleTimelineStream:${MiRole['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>;
|
||||||
|
};
|
||||||
|
antenna: {
|
||||||
|
name: `antennaStream:${MiAntenna['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>;
|
||||||
|
};
|
||||||
|
admin: {
|
||||||
|
name: `adminStream:${MiUser['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>;
|
||||||
|
};
|
||||||
|
notes: {
|
||||||
|
name: 'notesStream';
|
||||||
|
payload: Serialized<Packed<'Note'>>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// API event definitions
|
||||||
|
// ストリームごとのEmitterの辞書を用意
|
||||||
|
type EventEmitterDictionary = { [x in keyof GlobalEvents]: Emitter.default<EventEmitter, { [y in GlobalEvents[x]['name']]: (e: GlobalEvents[x]['payload']) => void }> };
|
||||||
|
// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
|
||||||
|
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
||||||
|
// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする
|
||||||
|
export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof GlobalEvents]>;
|
||||||
|
// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる
|
||||||
|
|
||||||
|
// provide stream channels union
|
||||||
|
export type StreamChannels = GlobalEvents[keyof GlobalEvents]['name'];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GlobalEventService {
|
export class GlobalEventService {
|
||||||
|
@ -51,7 +278,7 @@ export class GlobalEventService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishInternalEvent<K extends keyof InternalStreamTypes>(type: K, value?: InternalStreamTypes[K]): void {
|
public publishInternalEvent<K extends keyof InternalEventTypes>(type: K, value?: InternalEventTypes[K]): void {
|
||||||
this.publish('internal', type, typeof value === 'undefined' ? null : value);
|
this.publish('internal', type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,17 +288,17 @@ export class GlobalEventService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishMainStream<K extends keyof MainStreamTypes>(userId: MiUser['id'], type: K, value?: MainStreamTypes[K]): void {
|
public publishMainStream<K extends keyof MainEventTypes>(userId: MiUser['id'], type: K, value?: MainEventTypes[K]): void {
|
||||||
this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishDriveStream<K extends keyof DriveStreamTypes>(userId: MiUser['id'], type: K, value?: DriveStreamTypes[K]): void {
|
public publishDriveStream<K extends keyof DriveEventTypes>(userId: MiUser['id'], type: K, value?: DriveEventTypes[K]): void {
|
||||||
this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishNoteStream<K extends keyof NoteStreamTypes>(noteId: MiNote['id'], type: K, value?: NoteStreamTypes[K]): void {
|
public publishNoteStream<K extends keyof NoteEventTypes>(noteId: MiNote['id'], type: K, value?: NoteEventTypes[K]): void {
|
||||||
this.publish(`noteStream:${noteId}`, type, {
|
this.publish(`noteStream:${noteId}`, type, {
|
||||||
id: noteId,
|
id: noteId,
|
||||||
body: value,
|
body: value,
|
||||||
|
@ -79,17 +306,17 @@ export class GlobalEventService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishUserListStream<K extends keyof UserListStreamTypes>(listId: MiUserList['id'], type: K, value?: UserListStreamTypes[K]): void {
|
public publishUserListStream<K extends keyof UserListEventTypes>(listId: MiUserList['id'], type: K, value?: UserListEventTypes[K]): void {
|
||||||
this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishAntennaStream<K extends keyof AntennaStreamTypes>(antennaId: MiAntenna['id'], type: K, value?: AntennaStreamTypes[K]): void {
|
public publishAntennaStream<K extends keyof AntennaEventTypes>(antennaId: MiAntenna['id'], type: K, value?: AntennaEventTypes[K]): void {
|
||||||
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishRoleTimelineStream<K extends keyof RoleTimelineStreamTypes>(roleId: MiRole['id'], type: K, value?: RoleTimelineStreamTypes[K]): void {
|
public publishRoleTimelineStream<K extends keyof RoleTimelineEventTypes>(roleId: MiRole['id'], type: K, value?: RoleTimelineEventTypes[K]): void {
|
||||||
this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +326,7 @@ export class GlobalEventService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishAdminStream<K extends keyof AdminStreamTypes>(userId: MiUser['id'], type: K, value?: AdminStreamTypes[K]): void {
|
public publishAdminStream<K extends keyof AdminEventTypes>(userId: MiUser['id'], type: K, value?: AdminEventTypes[K]): void {
|
||||||
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { MiMeta } from '@/models/Meta.js';
|
import { MiMeta } from '@/models/Meta.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -46,7 +46,7 @@ export class MetaService implements OnApplicationShutdown {
|
||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'metaUpdated': {
|
case 'metaUpdated': {
|
||||||
this.cache = body;
|
this.cache = body;
|
||||||
|
|
|
@ -110,9 +110,8 @@ class NotificationManager {
|
||||||
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
|
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
|
||||||
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
|
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
|
||||||
this.notificationService.createNotification(x.target, x.reason, {
|
this.notificationService.createNotification(x.target, x.reason, {
|
||||||
notifierId: this.notifier.id,
|
|
||||||
noteId: this.note.id,
|
noteId: this.note.id,
|
||||||
});
|
}, this.notifier.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -515,9 +514,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}).then(followings => {
|
}).then(followings => {
|
||||||
for (const following of followings) {
|
for (const following of followings) {
|
||||||
this.notificationService.createNotification(following.followerId, 'note', {
|
this.notificationService.createNotification(following.followerId, 'note', {
|
||||||
notifierId: user.id,
|
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
});
|
}, user.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { NotificationEntityService } from '@/core/entities/NotificationEntitySer
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
import { UserListService } from '@/core/UserListService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationService implements OnApplicationShutdown {
|
export class NotificationService implements OnApplicationShutdown {
|
||||||
|
@ -38,6 +39,7 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private pushNotificationService: PushNotificationService,
|
private pushNotificationService: PushNotificationService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
|
private userListService: UserListService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,27 +76,56 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
public async createNotification(
|
public async createNotification(
|
||||||
notifieeId: MiUser['id'],
|
notifieeId: MiUser['id'],
|
||||||
type: MiNotification['type'],
|
type: MiNotification['type'],
|
||||||
data: Partial<MiNotification>,
|
data: Omit<Partial<MiNotification>, 'notifierId'>,
|
||||||
|
notifierId?: MiUser['id'] | null,
|
||||||
): Promise<MiNotification | null> {
|
): Promise<MiNotification | null> {
|
||||||
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
|
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
|
||||||
const isMuted = profile.mutingNotificationTypes.includes(type);
|
const recieveConfig = profile.notificationRecieveConfig[type];
|
||||||
if (isMuted) return null;
|
if (recieveConfig?.type === 'never') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.notifierId) {
|
if (notifierId) {
|
||||||
if (notifieeId === data.notifierId) {
|
if (notifieeId === notifierId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
|
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
|
||||||
if (mutings.has(data.notifierId)) {
|
if (mutings.has(notifierId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recieveConfig?.type === 'following') {
|
||||||
|
const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId));
|
||||||
|
if (!isFollowing) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (recieveConfig?.type === 'follower') {
|
||||||
|
const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId));
|
||||||
|
if (!isFollower) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (recieveConfig?.type === 'mutualFollow') {
|
||||||
|
const [isFollowing, isFollower] = await Promise.all([
|
||||||
|
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId)),
|
||||||
|
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId)),
|
||||||
|
]);
|
||||||
|
if (!isFollowing && !isFollower) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (recieveConfig?.type === 'list') {
|
||||||
|
const isMember = await this.userListService.membersCache.fetch(recieveConfig.userListId).then(members => members.has(notifierId));
|
||||||
|
if (!isMember) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const notification = {
|
const notification = {
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
type: type,
|
type: type,
|
||||||
|
notifierId: notifierId,
|
||||||
...data,
|
...data,
|
||||||
} as MiNotification;
|
} as MiNotification;
|
||||||
|
|
||||||
|
@ -117,8 +148,8 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
||||||
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
||||||
|
|
||||||
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
|
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! }));
|
||||||
if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
|
if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! }));
|
||||||
}, () => { /* aborted, ignore it */ });
|
}, () => { /* aborted, ignore it */ });
|
||||||
|
|
||||||
return notification;
|
return notification;
|
||||||
|
|
|
@ -219,10 +219,9 @@ export class ReactionService {
|
||||||
// リアクションされたユーザーがローカルユーザーなら通知を作成
|
// リアクションされたユーザーがローカルユーザーなら通知を作成
|
||||||
if (note.userHost === null) {
|
if (note.userHost === null) {
|
||||||
this.notificationService.createNotification(note.userId, 'reaction', {
|
this.notificationService.createNotification(note.userId, 'reaction', {
|
||||||
notifierId: user.id,
|
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
});
|
}, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region 配信
|
//#region 配信
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { MetaService } from '@/core/MetaService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { RoleCondFormulaValue } from '@/models/Role.js';
|
import type { RoleCondFormulaValue } from '@/models/Role.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
@ -116,7 +116,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'roleCreated': {
|
case 'roleCreated': {
|
||||||
const cached = this.rolesCache.get();
|
const cached = this.rolesCache.get();
|
||||||
|
|
|
@ -230,8 +230,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
|
|
||||||
// 通知を作成
|
// 通知を作成
|
||||||
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||||
notifierId: followee.id,
|
}, followee.id);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alreadyFollowed) return;
|
if (alreadyFollowed) return;
|
||||||
|
@ -304,8 +303,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
|
|
||||||
// 通知を作成
|
// 通知を作成
|
||||||
this.notificationService.createNotification(followee.id, 'follow', {
|
this.notificationService.createNotification(followee.id, 'follow', {
|
||||||
notifierId: follower.id,
|
}, follower.id);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -488,9 +486,8 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
|
|
||||||
// 通知を作成
|
// 通知を作成
|
||||||
this.notificationService.createNotification(followee.id, 'receiveFollowRequest', {
|
this.notificationService.createNotification(followee.id, 'receiveFollowRequest', {
|
||||||
notifierId: follower.id,
|
|
||||||
followRequestId: followRequest.id,
|
followRequestId: followRequest.id,
|
||||||
});
|
}, follower.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import type { UserListJoiningsRepository } from '@/models/_.js';
|
import type { UserListJoiningsRepository } from '@/models/_.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { MiUserList } from '@/models/UserList.js';
|
import type { MiUserList } from '@/models/UserList.js';
|
||||||
|
@ -16,12 +17,22 @@ import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { RedisKVCache } from '@/misc/cache.js';
|
||||||
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserListService {
|
export class UserListService implements OnApplicationShutdown {
|
||||||
public static TooManyUsersError = class extends Error {};
|
public static TooManyUsersError = class extends Error {};
|
||||||
|
|
||||||
|
public membersCache: RedisKVCache<Set<string>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.redisForSub)
|
||||||
|
private redisForSub: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListJoiningsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||||
|
|
||||||
|
@ -32,10 +43,48 @@ export class UserListService {
|
||||||
private proxyAccountService: ProxyAccountService,
|
private proxyAccountService: ProxyAccountService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
) {
|
) {
|
||||||
|
this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.userListJoiningsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.redisForSub.on('message', this.onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async push(target: MiUser, list: MiUserList, me: MiUser) {
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
|
if (obj.channel === 'internal') {
|
||||||
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
|
switch (type) {
|
||||||
|
case 'userListMemberAdded': {
|
||||||
|
const { userListId, memberId } = body;
|
||||||
|
const members = await this.membersCache.get(userListId);
|
||||||
|
if (members) {
|
||||||
|
members.add(memberId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'userListMemberRemoved': {
|
||||||
|
const { userListId, memberId } = body;
|
||||||
|
const members = await this.membersCache.get(userListId);
|
||||||
|
if (members) {
|
||||||
|
members.delete(memberId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async addMember(target: MiUser, list: MiUserList, me: MiUser) {
|
||||||
const currentCount = await this.userListJoiningsRepository.countBy({
|
const currentCount = await this.userListJoiningsRepository.countBy({
|
||||||
userListId: list.id,
|
userListId: list.id,
|
||||||
});
|
});
|
||||||
|
@ -50,6 +99,7 @@ export class UserListService {
|
||||||
userListId: list.id,
|
userListId: list.id,
|
||||||
} as MiUserListJoining);
|
} as MiUserListJoining);
|
||||||
|
|
||||||
|
this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id });
|
||||||
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
|
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
|
||||||
|
|
||||||
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
|
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
|
||||||
|
@ -60,4 +110,26 @@ export class UserListService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async removeMember(target: MiUser, list: MiUserList) {
|
||||||
|
await this.userListJoiningsRepository.delete({
|
||||||
|
userId: target.id,
|
||||||
|
userListId: list.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.globalEventService.publishInternalEvent('userListMemberRemoved', { userListId: list.id, memberId: target.id });
|
||||||
|
this.globalEventService.publishUserListStream(list.id, 'userRemoved', await this.userEntityService.pack(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public dispose(): void {
|
||||||
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
this.membersCache.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type { WebhooksRepository } from '@/models/_.js';
|
||||||
import type { MiWebhook } from '@/models/Webhook.js';
|
import type { MiWebhook } from '@/models/Webhook.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -45,7 +45,7 @@ export class WebhookService implements OnApplicationShutdown {
|
||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'webhookCreated':
|
case 'webhookCreated':
|
||||||
if (body.active) {
|
if (body.active) {
|
||||||
|
|
|
@ -308,6 +308,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
const packed: Packed<'Note'> = await awaitAll({
|
const packed: Packed<'Note'> = await awaitAll({
|
||||||
id: note.id,
|
id: note.id,
|
||||||
createdAt: note.createdAt.toISOString(),
|
createdAt: note.createdAt.toISOString(),
|
||||||
|
updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
|
||||||
userId: note.userId,
|
userId: note.userId,
|
||||||
user: this.userEntityService.pack(note.user ?? note.userId, me, {
|
user: this.userEntityService.pack(note.user ?? note.userId, me, {
|
||||||
detail: false,
|
detail: false,
|
||||||
|
|
|
@ -452,7 +452,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||||
mutedWords: profile!.mutedWords,
|
mutedWords: profile!.mutedWords,
|
||||||
mutedInstances: profile!.mutedInstances,
|
mutedInstances: profile!.mutedInstances,
|
||||||
mutingNotificationTypes: profile!.mutingNotificationTypes,
|
notificationRecieveConfig: profile!.notificationRecieveConfig,
|
||||||
emailNotificationTypes: profile!.emailNotificationTypes,
|
emailNotificationTypes: profile!.emailNotificationTypes,
|
||||||
achievements: profile!.achievements,
|
achievements: profile!.achievements,
|
||||||
loggedInDays: profile!.loggedInDates.length,
|
loggedInDays: profile!.loggedInDates.length,
|
||||||
|
|
|
@ -24,6 +24,11 @@ export class MiNote {
|
||||||
})
|
})
|
||||||
public createdAt: Date;
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
default: null,
|
||||||
|
})
|
||||||
|
public updatedAt: Date | null;
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column({
|
@Column({
|
||||||
...id(),
|
...id(),
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { obsoleteNotificationTypes, ffVisibility, notificationTypes } from '@/ty
|
||||||
import { id } from './util/id.js';
|
import { id } from './util/id.js';
|
||||||
import { MiUser } from './User.js';
|
import { MiUser } from './User.js';
|
||||||
import { MiPage } from './Page.js';
|
import { MiPage } from './Page.js';
|
||||||
|
import { MiUserList } from './UserList.js';
|
||||||
|
|
||||||
// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
|
// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
|
||||||
// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
|
// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
|
||||||
|
@ -222,16 +223,25 @@ export class MiUserProfile {
|
||||||
})
|
})
|
||||||
public mutedInstances: string[];
|
public mutedInstances: string[];
|
||||||
|
|
||||||
@Column('enum', {
|
@Column('jsonb', {
|
||||||
enum: [
|
default: {},
|
||||||
...notificationTypes,
|
|
||||||
// マイグレーションで削除が困難なので古いenumは残しておく
|
|
||||||
...obsoleteNotificationTypes,
|
|
||||||
],
|
|
||||||
array: true,
|
|
||||||
default: [],
|
|
||||||
})
|
})
|
||||||
public mutingNotificationTypes: typeof notificationTypes[number][];
|
public notificationRecieveConfig: {
|
||||||
|
[notificationType in typeof notificationTypes[number]]?: {
|
||||||
|
type: 'all';
|
||||||
|
} | {
|
||||||
|
type: 'never';
|
||||||
|
} | {
|
||||||
|
type: 'following';
|
||||||
|
} | {
|
||||||
|
type: 'follower';
|
||||||
|
} | {
|
||||||
|
type: 'mutualFollow';
|
||||||
|
} | {
|
||||||
|
type: 'list';
|
||||||
|
userListId: MiUserList['id'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 32, array: true, default: '{}',
|
length: 32, array: true, default: '{}',
|
||||||
|
|
|
@ -17,6 +17,11 @@ export const packedNoteSchema = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
deletedAt: {
|
deletedAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
|
@ -142,7 +147,7 @@ export const packedNoteSchema = {
|
||||||
isSensitive: {
|
isSensitive: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -387,14 +387,10 @@ export const packedMeDetailedOnlySchema = {
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mutingNotificationTypes: {
|
notificationRecieveConfig: {
|
||||||
type: 'array',
|
type: 'object',
|
||||||
nullable: true, optional: false,
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
emailNotificationTypes: {
|
emailNotificationTypes: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
|
|
|
@ -101,7 +101,7 @@ export class ImportUserListsProcessorService {
|
||||||
|
|
||||||
if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
|
if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
|
||||||
|
|
||||||
this.userListService.push(target, list!, user);
|
this.userListService.addMember(target, list!, user);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.warn(`Error in line:${linenum} ${e}`);
|
this.logger.warn(`Error in line:${linenum} ${e}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { AdsRepository } from '@/models/_.js';
|
import type { AdsRepository } from '@/models/_.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -39,9 +40,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private adsRepository: AdsRepository,
|
private adsRepository: AdsRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.adsRepository.insert({
|
const ad = await this.adsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
expiresAt: new Date(ps.expiresAt),
|
expiresAt: new Date(ps.expiresAt),
|
||||||
|
@ -53,7 +55,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
ratio: ps.ratio,
|
ratio: ps.ratio,
|
||||||
place: ps.place,
|
place: ps.place,
|
||||||
memo: ps.memo,
|
memo: ps.memo,
|
||||||
|
}).then(r => this.adsRepository.findOneByOrFail({ id: r.identifiers[0].id }));
|
||||||
|
|
||||||
|
this.moderationLogService.log(me, 'createAd', {
|
||||||
|
adId: ad.id,
|
||||||
|
ad: ad,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return ad;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { AdsRepository } from '@/models/_.js';
|
import type { AdsRepository } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -37,6 +38,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.adsRepository)
|
@Inject(DI.adsRepository)
|
||||||
private adsRepository: AdsRepository,
|
private adsRepository: AdsRepository,
|
||||||
|
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const ad = await this.adsRepository.findOneBy({ id: ps.id });
|
const ad = await this.adsRepository.findOneBy({ id: ps.id });
|
||||||
|
@ -44,6 +47,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (ad == null) throw new ApiError(meta.errors.noSuchAd);
|
if (ad == null) throw new ApiError(meta.errors.noSuchAd);
|
||||||
|
|
||||||
await this.adsRepository.delete(ad.id);
|
await this.adsRepository.delete(ad.id);
|
||||||
|
|
||||||
|
this.moderationLogService.log(me, 'deleteAd', {
|
||||||
|
adId: ad.id,
|
||||||
|
ad: ad,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ export const paramDef = {
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
sinceId: { type: 'string', format: 'misskey:id' },
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
|
publishing: { type: 'boolean', default: false },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -36,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId);
|
const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId);
|
||||||
|
if (ps.publishing) {
|
||||||
|
query.andWhere('ad.expiresAt > :now', { now: new Date() }).andWhere('ad.startsAt <= :now', { now: new Date() });
|
||||||
|
}
|
||||||
const ads = await query.limit(ps.limit).getMany();
|
const ads = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
return ads;
|
return ads;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { AdsRepository } from '@/models/_.js';
|
import type { AdsRepository } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -46,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.adsRepository)
|
@Inject(DI.adsRepository)
|
||||||
private adsRepository: AdsRepository,
|
private adsRepository: AdsRepository,
|
||||||
|
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const ad = await this.adsRepository.findOneBy({ id: ps.id });
|
const ad = await this.adsRepository.findOneBy({ id: ps.id });
|
||||||
|
@ -63,6 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
startsAt: new Date(ps.startsAt),
|
startsAt: new Date(ps.startsAt),
|
||||||
dayOfWeek: ps.dayOfWeek,
|
dayOfWeek: ps.dayOfWeek,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });
|
||||||
|
|
||||||
|
this.moderationLogService.log(me, 'updateAd', {
|
||||||
|
adId: ad.id,
|
||||||
|
before: ad,
|
||||||
|
after: updatedAd,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
||||||
mutedWords: profile.mutedWords,
|
mutedWords: profile.mutedWords,
|
||||||
mutedInstances: profile.mutedInstances,
|
mutedInstances: profile.mutedInstances,
|
||||||
mutingNotificationTypes: profile.mutingNotificationTypes,
|
notificationRecieveConfig: profile.notificationRecieveConfig,
|
||||||
isModerator: isModerator,
|
isModerator: isModerator,
|
||||||
isSilenced: isSilenced,
|
isSilenced: isSilenced,
|
||||||
isSuspended: user.isSuspended,
|
isSuspended: user.isSuspended,
|
||||||
|
|
|
@ -165,9 +165,7 @@ export const paramDef = {
|
||||||
mutedInstances: { type: 'array', items: {
|
mutedInstances: { type: 'array', items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
mutingNotificationTypes: { type: 'array', items: {
|
notificationRecieveConfig: { type: 'object' },
|
||||||
type: 'string', enum: notificationTypes,
|
|
||||||
} },
|
|
||||||
emailNotificationTypes: { type: 'array', items: {
|
emailNotificationTypes: { type: 'array', items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
|
@ -248,7 +246,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
||||||
}
|
}
|
||||||
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
|
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
|
||||||
if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][];
|
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
|
||||||
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
|
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
|
||||||
if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable;
|
if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable;
|
||||||
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
|
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
|
||||||
|
|
|
@ -75,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.notesRepository.update({ id: note.id }, {
|
await this.notesRepository.update({ id: note.id }, {
|
||||||
|
updatedAt: new Date(),
|
||||||
cw: ps.cw,
|
cw: ps.cw,
|
||||||
text: ps.text,
|
text: ps.text,
|
||||||
});
|
});
|
||||||
|
|
|
@ -144,7 +144,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.userListService.push(currentUser, userList, me);
|
await this.userListService.addMember(currentUser, userList, me);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof UserListService.TooManyUsersError) {
|
if (err instanceof UserListService.TooManyUsersError) {
|
||||||
throw new ApiError(meta.errors.tooManyUsers);
|
throw new ApiError(meta.errors.tooManyUsers);
|
||||||
|
|
|
@ -4,12 +4,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UserListsRepository, UserListJoiningsRepository } from '@/models/_.js';
|
import type { UserListsRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { UserListService } from '@/core/UserListService.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -53,12 +52,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
@Inject(DI.userListsRepository)
|
@Inject(DI.userListsRepository)
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
private userListService: UserListService,
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
|
||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
// Fetch the list
|
// Fetch the list
|
||||||
|
@ -77,10 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pull the user
|
await this.userListService.removeMember(user, userList);
|
||||||
await this.userListJoiningsRepository.delete({ userListId: userList.id, userId: user.id });
|
|
||||||
|
|
||||||
this.globalEventService.publishUserListStream(userList.id, 'userRemoved', await this.userEntityService.pack(user));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.userListService.push(user, userList, me);
|
await this.userListService.addMember(user, userList, me);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof UserListService.TooManyUsersError) {
|
if (err instanceof UserListService.TooManyUsersError) {
|
||||||
throw new ApiError(meta.errors.tooManyUsers);
|
throw new ApiError(meta.errors.tooManyUsers);
|
||||||
|
|
|
@ -12,10 +12,10 @@ import type { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { MiUserProfile } from '@/models/_.js';
|
import { MiUserProfile } from '@/models/_.js';
|
||||||
|
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { ChannelsService } from './ChannelsService.js';
|
import type { ChannelsService } from './ChannelsService.js';
|
||||||
import type { EventEmitter } from 'events';
|
import type { EventEmitter } from 'events';
|
||||||
import type Channel from './channel.js';
|
import type Channel from './channel.js';
|
||||||
import type { StreamEventEmitter, StreamMessages } from './types.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main stream connection
|
* Main stream connection
|
||||||
|
@ -122,7 +122,7 @@ export default class Connection {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private onBroadcastMessage(data: StreamMessages['broadcast']['payload']) {
|
private onBroadcastMessage(data: GlobalEvents['broadcast']['payload']) {
|
||||||
this.sendMessageToWs(data.type, data.body);
|
this.sendMessageToWs(data.type, data.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,7 +196,7 @@ export default class Connection {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async onNoteStreamMessage(data: StreamMessages['note']['payload']) {
|
private async onNoteStreamMessage(data: GlobalEvents['note']['payload']) {
|
||||||
this.sendMessageToWs('noteUpdated', {
|
this.sendMessageToWs('noteUpdated', {
|
||||||
id: data.body.id,
|
id: data.body.id,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
|
|
|
@ -7,8 +7,8 @@ import { Injectable } from '@nestjs/common';
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import Channel from '../channel.js';
|
import Channel from '../channel.js';
|
||||||
import type { StreamMessages } from '../types.js';
|
|
||||||
|
|
||||||
class AntennaChannel extends Channel {
|
class AntennaChannel extends Channel {
|
||||||
public readonly chName = 'antenna';
|
public readonly chName = 'antenna';
|
||||||
|
@ -35,7 +35,7 @@ class AntennaChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async onEvent(data: StreamMessages['antenna']['payload']) {
|
private async onEvent(data: GlobalEvents['antenna']['payload']) {
|
||||||
if (data.type === 'note') {
|
if (data.type === 'note') {
|
||||||
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
|
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import Channel from '../channel.js';
|
import Channel from '../channel.js';
|
||||||
import { StreamMessages } from '../types.js';
|
|
||||||
|
|
||||||
class RoleTimelineChannel extends Channel {
|
class RoleTimelineChannel extends Channel {
|
||||||
public readonly chName = 'roleTimeline';
|
public readonly chName = 'roleTimeline';
|
||||||
|
@ -37,7 +37,7 @@ class RoleTimelineChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async onEvent(data: StreamMessages['roleTimeline']['payload']) {
|
private async onEvent(data: GlobalEvents['roleTimeline']['payload']) {
|
||||||
if (data.type === 'note') {
|
if (data.type === 'note') {
|
||||||
const note = data.body;
|
const note = data.body;
|
||||||
|
|
||||||
|
|
|
@ -1,259 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { MiChannel } from '@/models/Channel.js';
|
|
||||||
import type { MiUser } from '@/models/User.js';
|
|
||||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
|
||||||
import type { MiNote } from '@/models/Note.js';
|
|
||||||
import type { MiAntenna } from '@/models/Antenna.js';
|
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
|
||||||
import type { MiDriveFolder } from '@/models/DriveFolder.js';
|
|
||||||
import type { MiUserList } from '@/models/UserList.js';
|
|
||||||
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
|
||||||
import type { MiSignin } from '@/models/Signin.js';
|
|
||||||
import type { MiPage } from '@/models/Page.js';
|
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
|
||||||
import type { MiWebhook } from '@/models/Webhook.js';
|
|
||||||
import type { MiMeta } from '@/models/Meta.js';
|
|
||||||
import { MiRole, MiRoleAssignment } from '@/models/_.js';
|
|
||||||
import type Emitter from 'strict-event-emitter-types';
|
|
||||||
import type { EventEmitter } from 'events';
|
|
||||||
|
|
||||||
//#region Stream type-body definitions
|
|
||||||
export interface InternalStreamTypes {
|
|
||||||
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
|
|
||||||
userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; };
|
|
||||||
remoteUserUpdated: { id: MiUser['id']; };
|
|
||||||
follow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
|
|
||||||
unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
|
|
||||||
blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
|
||||||
blockingDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
|
||||||
policiesUpdated: MiRole['policies'];
|
|
||||||
roleCreated: MiRole;
|
|
||||||
roleDeleted: MiRole;
|
|
||||||
roleUpdated: MiRole;
|
|
||||||
userRoleAssigned: MiRoleAssignment;
|
|
||||||
userRoleUnassigned: MiRoleAssignment;
|
|
||||||
webhookCreated: MiWebhook;
|
|
||||||
webhookDeleted: MiWebhook;
|
|
||||||
webhookUpdated: MiWebhook;
|
|
||||||
antennaCreated: MiAntenna;
|
|
||||||
antennaDeleted: MiAntenna;
|
|
||||||
antennaUpdated: MiAntenna;
|
|
||||||
metaUpdated: MiMeta;
|
|
||||||
followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
|
||||||
unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
|
||||||
updateUserProfile: MiUserProfile;
|
|
||||||
mute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
|
|
||||||
unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BroadcastTypes {
|
|
||||||
emojiAdded: {
|
|
||||||
emoji: Packed<'EmojiDetailed'>;
|
|
||||||
};
|
|
||||||
emojiUpdated: {
|
|
||||||
emojis: Packed<'EmojiDetailed'>[];
|
|
||||||
};
|
|
||||||
emojiDeleted: {
|
|
||||||
emojis: {
|
|
||||||
id?: string;
|
|
||||||
name: string;
|
|
||||||
[other: string]: any;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
announcementCreated: {
|
|
||||||
announcement: Packed<'Announcement'>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MainStreamTypes {
|
|
||||||
notification: Packed<'Notification'>;
|
|
||||||
mention: Packed<'Note'>;
|
|
||||||
reply: Packed<'Note'>;
|
|
||||||
renote: Packed<'Note'>;
|
|
||||||
follow: Packed<'UserDetailedNotMe'>;
|
|
||||||
followed: Packed<'User'>;
|
|
||||||
unfollow: Packed<'User'>;
|
|
||||||
meUpdated: Packed<'User'>;
|
|
||||||
pageEvent: {
|
|
||||||
pageId: MiPage['id'];
|
|
||||||
event: string;
|
|
||||||
var: any;
|
|
||||||
userId: MiUser['id'];
|
|
||||||
user: Packed<'User'>;
|
|
||||||
};
|
|
||||||
urlUploadFinished: {
|
|
||||||
marker?: string | null;
|
|
||||||
file: Packed<'DriveFile'>;
|
|
||||||
};
|
|
||||||
readAllNotifications: undefined;
|
|
||||||
unreadNotification: Packed<'Notification'>;
|
|
||||||
unreadMention: MiNote['id'];
|
|
||||||
readAllUnreadMentions: undefined;
|
|
||||||
unreadSpecifiedNote: MiNote['id'];
|
|
||||||
readAllUnreadSpecifiedNotes: undefined;
|
|
||||||
readAllAntennas: undefined;
|
|
||||||
unreadAntenna: MiAntenna;
|
|
||||||
readAllAnnouncements: undefined;
|
|
||||||
myTokenRegenerated: undefined;
|
|
||||||
signin: MiSignin;
|
|
||||||
registryUpdated: {
|
|
||||||
scope?: string[];
|
|
||||||
key: string;
|
|
||||||
value: any | null;
|
|
||||||
};
|
|
||||||
driveFileCreated: Packed<'DriveFile'>;
|
|
||||||
readAntenna: MiAntenna;
|
|
||||||
receiveFollowRequest: Packed<'User'>;
|
|
||||||
announcementCreated: {
|
|
||||||
announcement: Packed<'Announcement'>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DriveStreamTypes {
|
|
||||||
fileCreated: Packed<'DriveFile'>;
|
|
||||||
fileDeleted: MiDriveFile['id'];
|
|
||||||
fileUpdated: Packed<'DriveFile'>;
|
|
||||||
folderCreated: Packed<'DriveFolder'>;
|
|
||||||
folderDeleted: MiDriveFolder['id'];
|
|
||||||
folderUpdated: Packed<'DriveFolder'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NoteStreamTypes {
|
|
||||||
pollVoted: {
|
|
||||||
choice: number;
|
|
||||||
userId: MiUser['id'];
|
|
||||||
};
|
|
||||||
deleted: {
|
|
||||||
deletedAt: Date;
|
|
||||||
};
|
|
||||||
updated: {
|
|
||||||
cw: string | null;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
reacted: {
|
|
||||||
reaction: string;
|
|
||||||
emoji?: {
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
} | null;
|
|
||||||
userId: MiUser['id'];
|
|
||||||
};
|
|
||||||
unreacted: {
|
|
||||||
reaction: string;
|
|
||||||
userId: MiUser['id'];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
type NoteStreamEventTypes = {
|
|
||||||
[key in keyof NoteStreamTypes]: {
|
|
||||||
id: MiNote['id'];
|
|
||||||
body: NoteStreamTypes[key];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface UserListStreamTypes {
|
|
||||||
userAdded: Packed<'User'>;
|
|
||||||
userRemoved: Packed<'User'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AntennaStreamTypes {
|
|
||||||
note: MiNote;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RoleTimelineStreamTypes {
|
|
||||||
note: Packed<'Note'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminStreamTypes {
|
|
||||||
newAbuseUserReport: {
|
|
||||||
id: MiAbuseUserReport['id'];
|
|
||||||
targetUserId: MiUser['id'],
|
|
||||||
reporterId: MiUser['id'],
|
|
||||||
comment: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
// 辞書(interface or type)から{ type, body }ユニオンを定義
|
|
||||||
// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type
|
|
||||||
// VS Codeの展開を防止するためにEvents型を定義
|
|
||||||
type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
|
|
||||||
type EventUnionFromDictionary<
|
|
||||||
T extends object,
|
|
||||||
U = Events<T>
|
|
||||||
> = U[keyof U];
|
|
||||||
|
|
||||||
// redis通すとDateのインスタンスはstringに変換されるので
|
|
||||||
export type Serialized<T> = {
|
|
||||||
[K in keyof T]:
|
|
||||||
T[K] extends Date
|
|
||||||
? string
|
|
||||||
: T[K] extends (Date | null)
|
|
||||||
? (string | null)
|
|
||||||
: T[K] extends Record<string, any>
|
|
||||||
? Serialized<T[K]>
|
|
||||||
: T[K];
|
|
||||||
};
|
|
||||||
|
|
||||||
type SerializedAll<T> = {
|
|
||||||
[K in keyof T]: Serialized<T[K]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// name/messages(spec) pairs dictionary
|
|
||||||
export type StreamMessages = {
|
|
||||||
internal: {
|
|
||||||
name: 'internal';
|
|
||||||
payload: EventUnionFromDictionary<SerializedAll<InternalStreamTypes>>;
|
|
||||||
};
|
|
||||||
broadcast: {
|
|
||||||
name: 'broadcast';
|
|
||||||
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
|
|
||||||
};
|
|
||||||
main: {
|
|
||||||
name: `mainStream:${MiUser['id']}`;
|
|
||||||
payload: EventUnionFromDictionary<SerializedAll<MainStreamTypes>>;
|
|
||||||
};
|
|
||||||
drive: {
|
|
||||||
name: `driveStream:${MiUser['id']}`;
|
|
||||||
payload: EventUnionFromDictionary<SerializedAll<DriveStreamTypes>>;
|
|
||||||
};
|
|
||||||
note: {
|
|
||||||
name: `noteStream:${MiNote['id']}`;
|
|
||||||
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
|
|
||||||
};
|
|
||||||
userList: {
|
|
||||||
name: `userListStream:${MiUserList['id']}`;
|
|
||||||
payload: EventUnionFromDictionary<SerializedAll<UserListStreamTypes>>;
|
|
||||||
};
|
|
||||||
roleTimeline: {
|
|
||||||
name: `roleTimelineStream:${MiRole['id']}`;
|
|
||||||
payload: EventUnionFromDictionary<SerializedAll<RoleTimelineStreamTypes>>;
|
|
||||||
};
|
|
||||||
antenna: {
|
|
||||||
name: `antennaStream:${MiAntenna['id']}`;
|
|
||||||
payload: EventUnionFromDictionary<SerializedAll<AntennaStreamTypes>>;
|
|
||||||
};
|
|
||||||
admin: {
|
|
||||||
name: `adminStream:${MiUser['id']}`;
|
|
||||||
payload: EventUnionFromDictionary<SerializedAll<AdminStreamTypes>>;
|
|
||||||
};
|
|
||||||
notes: {
|
|
||||||
name: 'notesStream';
|
|
||||||
payload: Serialized<Packed<'Note'>>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// API event definitions
|
|
||||||
// ストリームごとのEmitterの辞書を用意
|
|
||||||
type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter.default<EventEmitter, { [y in StreamMessages[x]['name']]: (e: StreamMessages[x]['payload']) => void }> };
|
|
||||||
// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
|
|
||||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
|
||||||
// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする
|
|
||||||
export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof StreamMessages]>;
|
|
||||||
// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる
|
|
||||||
|
|
||||||
// provide stream channels union
|
|
||||||
export type StreamChannels = StreamMessages[keyof StreamMessages]['name'];
|
|
|
@ -57,6 +57,9 @@ export const moderationLogTypes = [
|
||||||
'unmarkSensitiveDriveFile',
|
'unmarkSensitiveDriveFile',
|
||||||
'resolveAbuseReport',
|
'resolveAbuseReport',
|
||||||
'createInvitation',
|
'createInvitation',
|
||||||
|
'createAd',
|
||||||
|
'updateAd',
|
||||||
|
'deleteAd',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ModerationLogPayloads = {
|
export type ModerationLogPayloads = {
|
||||||
|
@ -202,4 +205,28 @@ export type ModerationLogPayloads = {
|
||||||
createInvitation: {
|
createInvitation: {
|
||||||
invitations: any[];
|
invitations: any[];
|
||||||
};
|
};
|
||||||
|
createAd: {
|
||||||
|
adId: string;
|
||||||
|
ad: any;
|
||||||
|
};
|
||||||
|
updateAd: {
|
||||||
|
adId: string;
|
||||||
|
before: any;
|
||||||
|
after: any;
|
||||||
|
};
|
||||||
|
deleteAd: {
|
||||||
|
adId: string;
|
||||||
|
ad: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Serialized<T> = {
|
||||||
|
[K in keyof T]:
|
||||||
|
T[K] extends Date
|
||||||
|
? string
|
||||||
|
: T[K] extends (Date | null)
|
||||||
|
? (string | null)
|
||||||
|
: T[K] extends Record<string, any>
|
||||||
|
? Serialized<T[K]>
|
||||||
|
: T[K];
|
||||||
};
|
};
|
||||||
|
|
|
@ -166,7 +166,7 @@ describe('ユーザー', () => {
|
||||||
unreadAnnouncements: user.unreadAnnouncements,
|
unreadAnnouncements: user.unreadAnnouncements,
|
||||||
mutedWords: user.mutedWords,
|
mutedWords: user.mutedWords,
|
||||||
mutedInstances: user.mutedInstances,
|
mutedInstances: user.mutedInstances,
|
||||||
mutingNotificationTypes: user.mutingNotificationTypes,
|
notificationRecieveConfig: user.notificationRecieveConfig,
|
||||||
emailNotificationTypes: user.emailNotificationTypes,
|
emailNotificationTypes: user.emailNotificationTypes,
|
||||||
achievements: user.achievements,
|
achievements: user.achievements,
|
||||||
loggedInDays: user.loggedInDays,
|
loggedInDays: user.loggedInDays,
|
||||||
|
@ -414,7 +414,7 @@ describe('ユーザー', () => {
|
||||||
assert.deepStrictEqual(response.unreadAnnouncements, []);
|
assert.deepStrictEqual(response.unreadAnnouncements, []);
|
||||||
assert.deepStrictEqual(response.mutedWords, []);
|
assert.deepStrictEqual(response.mutedWords, []);
|
||||||
assert.deepStrictEqual(response.mutedInstances, []);
|
assert.deepStrictEqual(response.mutedInstances, []);
|
||||||
assert.deepStrictEqual(response.mutingNotificationTypes, []);
|
assert.deepStrictEqual(response.notificationRecieveConfig, {});
|
||||||
assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']);
|
assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']);
|
||||||
assert.deepStrictEqual(response.achievements, []);
|
assert.deepStrictEqual(response.achievements, []);
|
||||||
assert.deepStrictEqual(response.loggedInDays, 0);
|
assert.deepStrictEqual(response.loggedInDays, 0);
|
||||||
|
@ -495,8 +495,8 @@ describe('ユーザー', () => {
|
||||||
{ parameters: (): object => ({ mutedWords: [] }) },
|
{ parameters: (): object => ({ mutedWords: [] }) },
|
||||||
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
|
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
|
||||||
{ parameters: (): object => ({ mutedInstances: [] }) },
|
{ parameters: (): object => ({ mutedInstances: [] }) },
|
||||||
{ parameters: (): object => ({ mutingNotificationTypes: ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) },
|
{ parameters: (): object => ({ notificationRecieveConfig: { mention: { type: 'following' } } }) },
|
||||||
{ parameters: (): object => ({ mutingNotificationTypes: [] }) },
|
{ parameters: (): object => ({ notificationRecieveConfig: {} }) },
|
||||||
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
|
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
|
||||||
{ parameters: (): object => ({ emailNotificationTypes: [] }) },
|
{ parameters: (): object => ({ emailNotificationTypes: [] }) },
|
||||||
] as const)('を書き換えることができる($#)', async ({ parameters }) => {
|
] as const)('を書き換えることができる($#)', async ({ parameters }) => {
|
||||||
|
|
|
@ -93,6 +93,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<div :class="$style.noteFooterInfo">
|
<div :class="$style.noteFooterInfo">
|
||||||
|
<div v-if="appearNote.updatedAt">
|
||||||
|
{{ i18n.ts.edited }}: <MkTime :time="appearNote.updatedAt" mode="detail"/>
|
||||||
|
</div>
|
||||||
<MkA :to="notePage(appearNote)">
|
<MkA :to="notePage(appearNote)">
|
||||||
<MkTime :time="appearNote.createdAt" mode="detail"/>
|
<MkTime :time="appearNote.createdAt" mode="detail"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
|
|
|
@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
|
<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.info">
|
<div :class="$style.info">
|
||||||
|
<span v-if="note.updatedAt" style="margin-right: 0.5em;" :title="i18n.ts.edited"><i class="ti ti-pencil"></i></span>
|
||||||
<MkA :to="notePage(note)">
|
<MkA :to="notePage(note)">
|
||||||
<MkTime :time="note.createdAt"/>
|
<MkTime :time="note.createdAt"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkModalWindow
|
||||||
|
ref="dialog"
|
||||||
|
:width="400"
|
||||||
|
:height="450"
|
||||||
|
:withOkButton="true"
|
||||||
|
:okButtonDisabled="false"
|
||||||
|
@ok="ok()"
|
||||||
|
@close="dialog?.close()"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
|
<template #header>{{ i18n.ts.notificationSetting }}</template>
|
||||||
|
|
||||||
|
<MkSpacer :marginMin="20" :marginMax="28">
|
||||||
|
<div class="_gaps_m">
|
||||||
|
<MkInfo>{{ i18n.ts.notificationSettingDesc }}</MkInfo>
|
||||||
|
<div class="_buttons">
|
||||||
|
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
||||||
|
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
||||||
|
</div>
|
||||||
|
<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
|
||||||
|
</div>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkModalWindow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, Ref } from 'vue';
|
||||||
|
import MkSwitch from './MkSwitch.vue';
|
||||||
|
import MkInfo from './MkInfo.vue';
|
||||||
|
import MkButton from './MkButton.vue';
|
||||||
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
import { notificationTypes } from '@/const.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'done', v: { excludeTypes: string[] }): void,
|
||||||
|
(ev: 'closed'): void,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
excludeTypes?: typeof notificationTypes[number][];
|
||||||
|
}>(), {
|
||||||
|
excludeTypes: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||||
|
|
||||||
|
const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any);
|
||||||
|
|
||||||
|
function ok() {
|
||||||
|
emit('done', {
|
||||||
|
excludeTypes: (Object.keys(typesMap) as typeof notificationTypes[number][])
|
||||||
|
.filter(type => !typesMap[type].value),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dialog) dialog.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableAll() {
|
||||||
|
for (const type of notificationTypes) {
|
||||||
|
typesMap[type].value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableAll() {
|
||||||
|
for (const type of notificationTypes) {
|
||||||
|
typesMap[type].value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,95 +0,0 @@
|
||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
-->
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<MkModalWindow
|
|
||||||
ref="dialog"
|
|
||||||
:width="400"
|
|
||||||
:height="450"
|
|
||||||
:withOkButton="true"
|
|
||||||
:okButtonDisabled="false"
|
|
||||||
@ok="ok()"
|
|
||||||
@close="dialog?.close()"
|
|
||||||
@closed="emit('closed')"
|
|
||||||
>
|
|
||||||
<template #header>{{ i18n.ts.notificationSetting }}</template>
|
|
||||||
|
|
||||||
<MkSpacer :marginMin="20" :marginMax="28">
|
|
||||||
<div class="_gaps_m">
|
|
||||||
<template v-if="showGlobalToggle">
|
|
||||||
<MkSwitch v-model="useGlobalSetting">
|
|
||||||
{{ i18n.ts.useGlobalSetting }}
|
|
||||||
<template #caption>{{ i18n.ts.useGlobalSettingDesc }}</template>
|
|
||||||
</MkSwitch>
|
|
||||||
</template>
|
|
||||||
<template v-if="!useGlobalSetting">
|
|
||||||
<MkInfo>{{ i18n.ts.notificationSettingDesc }}</MkInfo>
|
|
||||||
<div class="_buttons">
|
|
||||||
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
|
||||||
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
|
||||||
</div>
|
|
||||||
<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</MkSpacer>
|
|
||||||
</MkModalWindow>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, Ref } from 'vue';
|
|
||||||
import MkSwitch from './MkSwitch.vue';
|
|
||||||
import MkInfo from './MkInfo.vue';
|
|
||||||
import MkButton from './MkButton.vue';
|
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
|
||||||
import { notificationTypes } from '@/const';
|
|
||||||
import { i18n } from '@/i18n.js';
|
|
||||||
|
|
||||||
type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(ev: 'done', v: { includingTypes: string[] | null }): void,
|
|
||||||
(ev: 'closed'): void,
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
includingTypes?: typeof notificationTypes[number][] | null;
|
|
||||||
showGlobalToggle?: boolean;
|
|
||||||
}>(), {
|
|
||||||
includingTypes: () => [],
|
|
||||||
showGlobalToggle: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let includingTypes = $computed(() => props.includingTypes?.filter(x => notificationTypes.includes(x)) ?? []);
|
|
||||||
|
|
||||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
|
||||||
|
|
||||||
const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(includingTypes.includes(t)) }), {} as any);
|
|
||||||
let useGlobalSetting = $ref((includingTypes === null || includingTypes.length === 0) && props.showGlobalToggle);
|
|
||||||
|
|
||||||
function ok() {
|
|
||||||
if (useGlobalSetting) {
|
|
||||||
emit('done', { includingTypes: null });
|
|
||||||
} else {
|
|
||||||
emit('done', {
|
|
||||||
includingTypes: (Object.keys(typesMap) as typeof notificationTypes[number][])
|
|
||||||
.filter(type => typesMap[type].value),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dialog) dialog.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
function disableAll() {
|
|
||||||
for (const type of notificationTypes) {
|
|
||||||
typesMap[type].value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableAll() {
|
|
||||||
for (const type of notificationTypes) {
|
|
||||||
typesMap[type].value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -30,11 +30,11 @@ import MkNote from '@/components/MkNote.vue';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { notificationTypes } from '@/const';
|
import { notificationTypes } from '@/const.js';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
includeTypes?: typeof notificationTypes[number][];
|
excludeTypes?: typeof notificationTypes[number][];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||||
|
@ -43,13 +43,12 @@ const pagination: Paging = {
|
||||||
endpoint: 'i/notifications' as const,
|
endpoint: 'i/notifications' as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: computed(() => ({
|
params: computed(() => ({
|
||||||
includeTypes: props.includeTypes ?? undefined,
|
excludeTypes: props.excludeTypes ?? undefined,
|
||||||
excludeTypes: props.includeTypes ? undefined : $i.mutingNotificationTypes,
|
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
const onNotification = (notification) => {
|
const onNotification = (notification) => {
|
||||||
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
|
const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false;
|
||||||
if (isMuted || document.visibilityState === 'visible') {
|
if (isMuted || document.visibilityState === 'visible') {
|
||||||
useStream().send('readNotification');
|
useStream().send('readNotification');
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,367 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="_button"
|
||||||
|
:class="[$style.root,{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light'
|
||||||
|
,}]" v-if="isFollowing"
|
||||||
|
@click="onClick"
|
||||||
|
>
|
||||||
|
<span v-if="props.user.notify === 'none'" :class="[{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }] "><i class="ti ti-bell"></i></span>
|
||||||
|
<span v-else-if="props.user.notify === 'normal'" :class="[{[$style.gamingDark]: gaming === 'dark',[$style.gamingLight]: gaming === 'light' }]"><i class="ti ti-bell-off"></i></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import {useStream} from '@/stream.js';
|
||||||
|
import {defaultStore} from "@/store.js";
|
||||||
|
|
||||||
|
let gaming = ref('');
|
||||||
|
|
||||||
|
const gamingMode = computed(defaultStore.makeGetterSetter('gamingMode'));
|
||||||
|
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||||
|
if (darkMode.value && gamingMode.value == true) {
|
||||||
|
gaming.value = 'dark';
|
||||||
|
} else if (!darkMode.value && gamingMode.value == true) {
|
||||||
|
gaming.value = 'light';
|
||||||
|
} else {
|
||||||
|
gaming.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(darkMode, () => {
|
||||||
|
if (darkMode.value && gamingMode.value == true) {
|
||||||
|
gaming.value = 'dark';
|
||||||
|
} else if (!darkMode.value && gamingMode.value == true) {
|
||||||
|
gaming.value = 'light';
|
||||||
|
} else {
|
||||||
|
gaming.value = '';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(gamingMode, () => {
|
||||||
|
if (darkMode.value && gamingMode.value == true) {
|
||||||
|
gaming.value = 'dark';
|
||||||
|
} else if (!darkMode.value && gamingMode.value == true) {
|
||||||
|
gaming.value = 'light';
|
||||||
|
} else {
|
||||||
|
gaming.value = '';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
user: Misskey.entities.UserDetailed,
|
||||||
|
full?: boolean,
|
||||||
|
large?: boolean,
|
||||||
|
}>(), {
|
||||||
|
full: false,
|
||||||
|
large: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let isFollowing = $ref(props.user.isFollowing);
|
||||||
|
let notify = $ref(props.user.notify);
|
||||||
|
const connection = useStream().useChannel('main');
|
||||||
|
|
||||||
|
if (props.user.isFollowing == null) {
|
||||||
|
os.api('users/show', {
|
||||||
|
userId: props.user.id,
|
||||||
|
}).then(onFollowChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.user.notify == null) {
|
||||||
|
os.api('users/show', {
|
||||||
|
userId: props.user.id,
|
||||||
|
}).then(onNotifyChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFollowChange(user: Misskey.entities.UserDetailed) {
|
||||||
|
if (user.id === props.user.id) {
|
||||||
|
isFollowing = user.isFollowing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onNotifyChange(user: Misskey.entities.UserDetailed) {
|
||||||
|
if (user.id === props.user.id) {
|
||||||
|
notify = user.notify;
|
||||||
|
console.log(props.user.notify)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function onClick() {
|
||||||
|
try {
|
||||||
|
await os.apiWithDialog('following/update', {
|
||||||
|
userId: props.user.id,
|
||||||
|
notify: props.user.notify === 'normal' ? 'none' : 'normal',
|
||||||
|
}).then(() => {
|
||||||
|
props.user.notify = props.user.notify === 'normal' ? 'none' : 'normal';
|
||||||
|
});
|
||||||
|
}finally {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
connection.on('follow', onFollowChange);
|
||||||
|
connection.on('unfollow', onFollowChange);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
connection.dispose();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--fgOnWhite);
|
||||||
|
border: solid 1px var(--accent);
|
||||||
|
padding: 0;
|
||||||
|
height: 31px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 32px;
|
||||||
|
background: #fff;
|
||||||
|
vertical-align: bottom;
|
||||||
|
|
||||||
|
&.gamingDark {
|
||||||
|
color: black !important;
|
||||||
|
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||||
|
background-size: 1800% 1800%;
|
||||||
|
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
border: solid 1px black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.gamingLight {
|
||||||
|
color: white !important;
|
||||||
|
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||||
|
background-size: 1800% 1800% !important;
|
||||||
|
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
border: solid 1px white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.full {
|
||||||
|
padding: 0 8px 0 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
&.gamingDark {
|
||||||
|
color: black;
|
||||||
|
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||||
|
background-size: 1800% 1800%;
|
||||||
|
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&.gamingLight {
|
||||||
|
color: white;
|
||||||
|
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||||
|
background-size: 1800% 1800% !important;
|
||||||
|
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
font-size: 16px;
|
||||||
|
height: 38px;
|
||||||
|
padding: 0 12px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.full) {
|
||||||
|
width: 31px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
right: -5px;
|
||||||
|
bottom: -5px;
|
||||||
|
left: -5px;
|
||||||
|
border: 2px solid var(--focus);
|
||||||
|
border-radius: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
//background: mix($primary, #fff, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
//background: mix($primary, #fff, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--fgOnAccent);
|
||||||
|
background: var(--accent);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--accentLighten);
|
||||||
|
border-color: var(--accentLighten);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: var(--accentDarken);
|
||||||
|
border-color: var(--accentDarken);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.gamingDark:hover {
|
||||||
|
color: black;
|
||||||
|
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||||
|
background-size: 1800% 1800%;
|
||||||
|
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
border: solid 1px white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.gamingDark:active {
|
||||||
|
color: black;
|
||||||
|
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||||
|
background-size: 1800% 1800%;
|
||||||
|
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
border: solid 1px white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.gamingLight:hover {
|
||||||
|
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||||
|
background-size: 1800% 1800% !important;
|
||||||
|
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
border: solid 1px white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.gamingLight:active {
|
||||||
|
color: white;
|
||||||
|
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||||
|
background-size: 1800% 1800% !important;
|
||||||
|
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
border: solid 1px white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.gamingDark {
|
||||||
|
-webkit-text-fill-color: unset !important;
|
||||||
|
color: black;
|
||||||
|
border: solid 1px white;
|
||||||
|
background: linear-gradient(270deg, #e7a2a2, #e3cfa2, #ebefa1, #b3e7a6, #a6ebe7, #aec5e3, #cabded, #e0b9e3, #f4bddd);
|
||||||
|
background-size: 1800% 1800%;
|
||||||
|
-webkit-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
-moz-animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
animation: AnimationDark var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.gamingLight {
|
||||||
|
-webkit-text-fill-color: unset !important;
|
||||||
|
color: white;
|
||||||
|
border: solid 1px white;
|
||||||
|
background: linear-gradient(270deg, #c06161, #c0a567, #b6ba69, #81bc72, #63c3be, #8bacd6, #9f8bd6, #d18bd6, #d883b4);
|
||||||
|
background-size: 1800% 1800% !important;
|
||||||
|
-webkit-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
-moz-animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
animation: AnimationLight var(--gamingspeed) cubic-bezier(0, 0.2, 0.90, 1) infinite !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.gamingDark {
|
||||||
|
color: black;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.gamingLight {
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@-webkit-keyframes AnimationLight {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-moz-keyframes AnimationLight {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes AnimationLight {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes AnimationDark {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-moz-keyframes AnimationDark {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes AnimationDark {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -5,8 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header>
|
||||||
|
<XHeader :actions="headerActions" :tabs="headerTabs" />
|
||||||
|
</template>
|
||||||
<MkSpacer :contentMax="900">
|
<MkSpacer :contentMax="900">
|
||||||
|
<MkSwitch :modelValue="publishing" @update:modelValue="onChangePublishing">
|
||||||
|
{{ i18n.ts.publishing }}
|
||||||
|
</MkSwitch>
|
||||||
<div>
|
<div>
|
||||||
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
|
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
|
||||||
<MkAd v-if="ad.url" :specify="ad" />
|
<MkAd v-if="ad.url" :specify="ad" />
|
||||||
|
@ -46,7 +51,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span>
|
<span>
|
||||||
{{ i18n.ts._ad.timezoneinfo }}
|
{{ i18n.ts._ad.timezoneinfo }}
|
||||||
<div v-for="(day, index) in daysOfWeek" :key="index">
|
<div v-for="(day, index) in daysOfWeek" :key="index">
|
||||||
<input :id="`ad${ad.id}-${index}`" type="checkbox" :checked="(ad.dayOfWeek & (1 << index)) !== 0" @change="toggleDayOfWeek(ad, index)">
|
<input :id="`ad${ad.id}-${index}`" type="checkbox" :checked="(ad.dayOfWeek & (1 << index)) !== 0"
|
||||||
|
@change="toggleDayOfWeek(ad, index)">
|
||||||
<label :for="`ad${ad.id}-${index}`">{{ day }}</label>
|
<label :for="`ad${ad.id}-${index}`">{{ day }}</label>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@ -55,8 +61,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.memo }}</template>
|
<template #label>{{ i18n.ts.memo }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i
|
||||||
<MkButton class="button" inline danger @click="remove(ad)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
|
class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
|
<MkButton class="button" inline danger @click="remove(ad)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}
|
||||||
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MkButton class="button" @click="more()">
|
<MkButton class="button" @click="more()">
|
||||||
|
@ -75,6 +83,7 @@ import MkInput from '@/components/MkInput.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import MkRadios from '@/components/MkRadios.vue';
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import FormSplit from '@/components/form/split.vue';
|
import FormSplit from '@/components/form/split.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -86,8 +95,9 @@ let ads: any[] = $ref([]);
|
||||||
const localTime = new Date();
|
const localTime = new Date();
|
||||||
const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000;
|
const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000;
|
||||||
const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday];
|
const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday];
|
||||||
|
let publishing = false;
|
||||||
|
|
||||||
os.api('admin/ad/list').then(adsResponse => {
|
os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
|
||||||
ads = adsResponse.map(r => {
|
ads = adsResponse.map(r => {
|
||||||
const exdate = new Date(r.expiresAt);
|
const exdate = new Date(r.expiresAt);
|
||||||
const stdate = new Date(r.startsAt);
|
const stdate = new Date(r.startsAt);
|
||||||
|
@ -101,6 +111,10 @@ os.api('admin/ad/list').then(adsResponse => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onChangePublishing = (v) => {
|
||||||
|
publishing = v;
|
||||||
|
refresh();
|
||||||
|
};
|
||||||
// 選択された曜日(index)のビットフラグを操作する
|
// 選択された曜日(index)のビットフラグを操作する
|
||||||
function toggleDayOfWeek(ad, index) {
|
function toggleDayOfWeek(ad, index) {
|
||||||
ad.dayOfWeek ^= 1 << index;
|
ad.dayOfWeek ^= 1 << index;
|
||||||
|
@ -131,6 +145,8 @@ function remove(ad) {
|
||||||
if (ad.id == null) return;
|
if (ad.id == null) return;
|
||||||
os.apiWithDialog('admin/ad/delete', {
|
os.apiWithDialog('admin/ad/delete', {
|
||||||
id: ad.id,
|
id: ad.id,
|
||||||
|
}).then(() => {
|
||||||
|
refresh();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -172,7 +188,7 @@ function save(ad) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function more() {
|
function more() {
|
||||||
os.api('admin/ad/list', { untilId: ads.reduce((acc, ad) => ad.id != null ? ad : acc).id }).then(adsResponse => {
|
os.api('admin/ad/list', { untilId: ads.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => {
|
||||||
ads = ads.concat(adsResponse.map(r => {
|
ads = ads.concat(adsResponse.map(r => {
|
||||||
const exdate = new Date(r.expiresAt);
|
const exdate = new Date(r.expiresAt);
|
||||||
const stdate = new Date(r.startsAt);
|
const stdate = new Date(r.startsAt);
|
||||||
|
@ -188,7 +204,7 @@ function more() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
os.api('admin/ad/list').then(adsResponse => {
|
os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
|
||||||
ads = adsResponse.map(r => {
|
ads = adsResponse.map(r => {
|
||||||
const exdate = new Date(r.expiresAt);
|
const exdate = new Date(r.expiresAt);
|
||||||
const stdate = new Date(r.startsAt);
|
const stdate = new Date(r.startsAt);
|
||||||
|
|
|
@ -6,7 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<MkFolder>
|
<MkFolder>
|
||||||
<template #label>
|
<template #label>
|
||||||
<b>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
|
<b
|
||||||
|
:class="{
|
||||||
|
[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation'].includes(log.type),
|
||||||
|
[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword'].includes(log.type),
|
||||||
|
[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd'].includes(log.type)
|
||||||
|
}"
|
||||||
|
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
|
||||||
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
|
@ -18,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-else-if="log.type === 'deleteRole'">: {{ log.info.role.name }}</span>
|
<span v-else-if="log.type === 'deleteRole'">: {{ log.info.role.name }}</span>
|
||||||
<span v-else-if="log.type === 'addCustomEmoji'">: {{ log.info.emoji.name }}</span>
|
<span v-else-if="log.type === 'addCustomEmoji'">: {{ log.info.emoji.name }}</span>
|
||||||
<span v-else-if="log.type === 'updateCustomEmoji'">: {{ log.info.before.name }}</span>
|
<span v-else-if="log.type === 'updateCustomEmoji'">: {{ log.info.before.name }}</span>
|
||||||
|
<span v-else-if="log.type === 'deleteCustomEmoji'">: {{ log.info.emoji.name }}</span>
|
||||||
<span v-else-if="log.type === 'markSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
|
<span v-else-if="log.type === 'markSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
|
<span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span>
|
<span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span>
|
||||||
|
@ -76,6 +83,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="log.type === 'updateAd'">
|
||||||
|
<div :class="$style.diff">
|
||||||
|
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>raw</summary>
|
<summary>raw</summary>
|
||||||
|
@ -114,4 +126,16 @@ const props = defineProps<{
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logYellow {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logRed {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logGreen {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :contentMax="800">
|
<MkSpacer :contentMax="800">
|
||||||
<div v-if="tab === 'all'">
|
<div v-if="tab === 'all'">
|
||||||
<XNotifications class="notifications" :includeTypes="includeTypes"/>
|
<XNotifications class="notifications" :excludeTypes="excludeTypes"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'mentions'">
|
<div v-else-if="tab === 'mentions'">
|
||||||
<MkNotes :pagination="mentionsPagination"/>
|
<MkNotes :pagination="mentionsPagination"/>
|
||||||
|
@ -27,10 +27,11 @@ import MkNotes from '@/components/MkNotes.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { notificationTypes } from '@/const';
|
import { notificationTypes } from '@/const.js';
|
||||||
|
|
||||||
let tab = $ref('all');
|
let tab = $ref('all');
|
||||||
let includeTypes = $ref<string[] | null>(null);
|
let includeTypes = $ref<string[] | null>(null);
|
||||||
|
const excludeTypes = $computed(() => includeTypes ? notificationTypes.filter(t => !includeTypes.includes(t)) : null);
|
||||||
|
|
||||||
const mentionsPagination = {
|
const mentionsPagination = {
|
||||||
endpoint: 'notes/mentions' as const,
|
endpoint: 'notes/mentions' as const,
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="_gaps_m">
|
||||||
|
<MkSelect v-model="type">
|
||||||
|
<option value="all">{{ i18n.ts.all }}</option>
|
||||||
|
<option value="following">{{ i18n.ts.following }}</option>
|
||||||
|
<option value="follower">{{ i18n.ts.followers }}</option>
|
||||||
|
<option value="mutualFollow">{{ i18n.ts.mutualFollow }}</option>
|
||||||
|
<option value="list">{{ i18n.ts.userList }}</option>
|
||||||
|
<option value="never">{{ i18n.ts.none }}</option>
|
||||||
|
</MkSelect>
|
||||||
|
|
||||||
|
<MkSelect v-if="type === 'list'" v-model="userListId">
|
||||||
|
<template #label>{{ i18n.ts.userList }}</template>
|
||||||
|
<option v-for="list in props.userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
|
||||||
|
</MkSelect>
|
||||||
|
|
||||||
|
<div class="_buttons">
|
||||||
|
<MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
value: any;
|
||||||
|
userLists: Misskey.entities.UserList[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'update', result: any): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let type = $ref(props.value.type);
|
||||||
|
let userListId = $ref(props.value.userListId);
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
emit('update', { type, userListId });
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -5,7 +5,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<FormLink @click="configure"><template #icon><i class="ti ti-settings"></i></template>{{ i18n.ts.notificationSetting }}</FormLink>
|
<FormSection first>
|
||||||
|
<template #label>{{ i18n.ts.notificationRecieveConfig }}</template>
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<MkFolder v-for="type in notificationTypes" :key="type">
|
||||||
|
<template #label>{{ i18n.t('_notification._types.' + type) }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
{{
|
||||||
|
$i.notificationRecieveConfig[type]?.type === 'never' ? i18n.ts.none :
|
||||||
|
$i.notificationRecieveConfig[type]?.type === 'following' ? i18n.ts.following :
|
||||||
|
$i.notificationRecieveConfig[type]?.type === 'follower' ? i18n.ts.followers :
|
||||||
|
$i.notificationRecieveConfig[type]?.type === 'mutualFollow' ? i18n.ts.mutualFollow :
|
||||||
|
$i.notificationRecieveConfig[type]?.type === 'list' ? i18n.ts.userList :
|
||||||
|
i18n.ts.all
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<XNotificationConfig :userLists="userLists" :value="$i.notificationRecieveConfig[type] ?? { type: 'all' }" @update="(res) => updateReceiveConfig(type, res)"/>
|
||||||
|
</MkFolder>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<FormLink @click="readAllNotifications">{{ i18n.ts.markAsReadAllNotifications }}</FormLink>
|
<FormLink @click="readAllNotifications">{{ i18n.ts.markAsReadAllNotifications }}</FormLink>
|
||||||
|
@ -37,19 +56,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import XNotificationConfig from './notifications.notification-config.vue';
|
||||||
import FormLink from '@/components/form/link.vue';
|
import FormLink from '@/components/form/link.vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
import FormSection from '@/components/form/section.vue';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
||||||
import { notificationTypes } from '@/const';
|
import { notificationTypes } from '@/const.js';
|
||||||
|
|
||||||
let allowButton = $shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
|
let allowButton = $shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
|
||||||
let pushRegistrationInServer = $computed(() => allowButton?.pushRegistrationInServer);
|
let pushRegistrationInServer = $computed(() => allowButton?.pushRegistrationInServer);
|
||||||
let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage || false);
|
let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage || false);
|
||||||
|
const userLists = await os.api('users/lists/list');
|
||||||
|
|
||||||
async function readAllUnreadNotes() {
|
async function readAllUnreadNotes() {
|
||||||
await os.api('i/read-all-unread-notes');
|
await os.api('i/read-all-unread-notes');
|
||||||
|
@ -59,21 +81,15 @@ async function readAllNotifications() {
|
||||||
await os.api('notifications/mark-all-as-read');
|
await os.api('notifications/mark-all-as-read');
|
||||||
}
|
}
|
||||||
|
|
||||||
function configure() {
|
async function updateReceiveConfig(type, value) {
|
||||||
const includingTypes = notificationTypes.filter(x => !$i!.mutingNotificationTypes.includes(x));
|
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSettingWindow.vue')), {
|
|
||||||
includingTypes,
|
|
||||||
showGlobalToggle: false,
|
|
||||||
}, {
|
|
||||||
done: async (res) => {
|
|
||||||
const { includingTypes: value } = res;
|
|
||||||
await os.apiWithDialog('i/update', {
|
await os.apiWithDialog('i/update', {
|
||||||
mutingNotificationTypes: notificationTypes.filter(x => !value.includes(x)),
|
notificationRecieveConfig: {
|
||||||
}).then(i => {
|
...$i!.notificationRecieveConfig,
|
||||||
$i!.mutingNotificationTypes = i.mutingNotificationTypes;
|
[type]: value,
|
||||||
});
|
|
||||||
},
|
},
|
||||||
}, 'closed');
|
}).then(i => {
|
||||||
|
$i!.notificationRecieveConfig = i.notificationRecieveConfig;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeSendReadMessage(v: boolean) {
|
function onChangeSendReadMessage(v: boolean) {
|
||||||
|
|
|
@ -183,7 +183,7 @@ const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLis
|
||||||
}] : []), {
|
}] : []), {
|
||||||
key: 'social',
|
key: 'social',
|
||||||
title: i18n.ts._timelines.social,
|
title: i18n.ts._timelines.social,
|
||||||
icon: 'ti ti-universe',
|
icon: 'ti ti-rocket',
|
||||||
iconOnly: true,
|
iconOnly: true,
|
||||||
}] : []), ...(isGlobalTimelineAvailable ? [{
|
}] : []), ...(isGlobalTimelineAvailable ? [{
|
||||||
key: 'global',
|
key: 'global',
|
||||||
|
|
|
@ -34,6 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
|
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
|
||||||
<div v-if="$i" class="actions">
|
<div v-if="$i" class="actions">
|
||||||
<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
|
<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
|
||||||
|
<MkNotifyButton v-if="$i.id != user.id " :user="user"></MkNotifyButton>
|
||||||
<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -166,6 +167,7 @@ import { confetti } from '@/scripts/confetti.js';
|
||||||
import MkNotes from '@/components/MkNotes.vue';
|
import MkNotes from '@/components/MkNotes.vue';
|
||||||
import { api } from '@/os.js';
|
import { api } from '@/os.js';
|
||||||
import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
||||||
|
import MkNotifyButton from "@/components/MkNotifyButton.vue";
|
||||||
|
|
||||||
function calcAge(birthdate: string): number {
|
function calcAge(birthdate: string): number {
|
||||||
const date = new Date(birthdate);
|
const date = new Date(birthdate);
|
||||||
|
|
|
@ -72,6 +72,7 @@ export function useNoteCapture(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'updated': {
|
case 'updated': {
|
||||||
|
note.value.updatedAt = new Date().toISOString();
|
||||||
note.value.cw = body.cw;
|
note.value.cw = body.cw;
|
||||||
note.value.text = body.text;
|
note.value.text = body.text;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -65,9 +65,7 @@ const dev = _DEV_;
|
||||||
|
|
||||||
let notifications = $ref<Misskey.entities.Notification[]>([]);
|
let notifications = $ref<Misskey.entities.Notification[]>([]);
|
||||||
|
|
||||||
function onNotification(notification: Misskey.entities.Notification, isClient: boolean = false) {
|
function onNotification(notification: Misskey.entities.Notification, isClient = false) {
|
||||||
if ($i.mutingNotificationTypes.includes(notification.type)) return;
|
|
||||||
|
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === 'visible') {
|
||||||
if (!isClient) {
|
if (!isClient) {
|
||||||
useStream().send('readNotification');
|
useStream().send('readNotification');
|
||||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<XColumn :column="column" :isStacked="isStacked" :menu="menu">
|
<XColumn :column="column" :isStacked="isStacked" :menu="menu">
|
||||||
<template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
<template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||||
|
|
||||||
<XNotifications :includeTypes="column.includingTypes"/>
|
<XNotifications :excludeTypes="props.column.excludeTypes"/>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -25,13 +25,13 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function func() {
|
function func() {
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSettingWindow.vue')), {
|
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), {
|
||||||
includingTypes: props.column.includingTypes,
|
excludeTypes: props.column.excludeTypes,
|
||||||
}, {
|
}, {
|
||||||
done: async (res) => {
|
done: async (res) => {
|
||||||
const { includingTypes } = res;
|
const { excludeTypes } = res;
|
||||||
updateColumn(props.column.id, {
|
updateColumn(props.column.id, {
|
||||||
includingTypes: includingTypes,
|
excludeTypes: excludeTypes,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
|
|
|
@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import XCalendar from './WidgetActivity.calendar.vue';
|
import XCalendar from './WidgetActivity.calendar.vue';
|
||||||
import XChart from './WidgetActivity.chart.vue';
|
import XChart from './WidgetActivity.chart.vue';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, shallowRef } from 'vue';
|
import { onMounted, onUnmounted, shallowRef } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
|
|
||||||
const name = 'ai';
|
const name = 'ai';
|
||||||
|
|
|
@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, Ref, ref, watch } from 'vue';
|
import { onMounted, Ref, ref, watch } from 'vue';
|
||||||
import { Interpreter, Parser } from '@syuilo/aiscript';
|
import { Interpreter, Parser } from '@syuilo/aiscript';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
|
import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
|
||||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Interpreter, Parser } from '@syuilo/aiscript';
|
import { Interpreter, Parser } from '@syuilo/aiscript';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
|
import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
|
||||||
|
|
|
@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { useInterval } from '@/scripts/use-interval.js';
|
import { useInterval } from '@/scripts/use-interval.js';
|
||||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkClickerGame from '@/components/MkClickerGame.vue';
|
import MkClickerGame from '@/components/MkClickerGame.vue';
|
||||||
|
|
|
@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkAnalogClock from '@/components/MkAnalogClock.vue';
|
import MkAnalogClock from '@/components/MkAnalogClock.vue';
|
||||||
|
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import { timezones } from '@/scripts/timezones.js';
|
import { timezones } from '@/scripts/timezones.js';
|
||||||
import MkDigitalClock from '@/components/MkDigitalClock.vue';
|
import MkDigitalClock from '@/components/MkDigitalClock.vue';
|
||||||
|
|
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkMiniChart from '@/components/MkMiniChart.vue';
|
import MkMiniChart from '@/components/MkMiniChart.vue';
|
||||||
|
|
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkTagCloud from '@/components/MkTagCloud.vue';
|
import MkTagCloud from '@/components/MkTagCloud.vue';
|
||||||
|
|
|
@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onUnmounted, reactive } from 'vue';
|
import { onUnmounted, reactive } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import number from '@/filters/number.js';
|
import number from '@/filters/number.js';
|
||||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
|
@ -10,14 +10,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configureNotification()"><i class="ti ti-settings"></i></button></template>
|
<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configureNotification()"><i class="ti ti-settings"></i></button></template>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<XNotifications :includeTypes="widgetProps.includingTypes"/>
|
<XNotifications :excludeTypes="widgetProps.excludeTypes"/>
|
||||||
</div>
|
</div>
|
||||||
</MkContainer>
|
</MkContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import XNotifications from '@/components/MkNotifications.vue';
|
import XNotifications from '@/components/MkNotifications.vue';
|
||||||
|
@ -35,10 +35,10 @@ const widgetPropsDef = {
|
||||||
type: 'number' as const,
|
type: 'number' as const,
|
||||||
default: 300,
|
default: 300,
|
||||||
},
|
},
|
||||||
includingTypes: {
|
excludeTypes: {
|
||||||
type: 'array' as const,
|
type: 'array' as const,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
default: null,
|
default: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -54,12 +54,12 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name,
|
||||||
);
|
);
|
||||||
|
|
||||||
const configureNotification = () => {
|
const configureNotification = () => {
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSettingWindow.vue')), {
|
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), {
|
||||||
includingTypes: widgetProps.includingTypes,
|
excludeTypes: widgetProps.excludeTypes,
|
||||||
}, {
|
}, {
|
||||||
done: async (res) => {
|
done: async (res) => {
|
||||||
const { includingTypes } = res;
|
const { excludeTypes } = res;
|
||||||
widgetProps.includingTypes = includingTypes;
|
widgetProps.excludeTypes = excludeTypes;
|
||||||
save();
|
save();
|
||||||
},
|
},
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { useInterval } from '@/scripts/use-interval.js';
|
import { useInterval } from '@/scripts/use-interval.js';
|
||||||
|
|
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onUnmounted, ref } from 'vue';
|
import { onUnmounted, ref } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import MkPostForm from '@/components/MkPostForm.vue';
|
import MkPostForm from '@/components/MkPostForm.vue';
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
|
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch, computed } from 'vue';
|
import { ref, watch, computed } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import { url as base } from '@/config.js';
|
import { url as base } from '@/config.js';
|
||||||
|
|
|
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch, computed } from 'vue';
|
import { ref, watch, computed } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import MarqueeText from '@/components/MkMarquee.vue';
|
import MarqueeText from '@/components/MkMarquee.vue';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
|
|
|
@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref, shallowRef } from 'vue';
|
import { onMounted, ref, shallowRef } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { useInterval } from '@/scripts/use-interval.js';
|
import { useInterval } from '@/scripts/use-interval.js';
|
||||||
|
|
|
@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
|
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkMiniChart from '@/components/MkMiniChart.vue';
|
import MkMiniChart from '@/components/MkMiniChart.vue';
|
||||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onUnmounted, ref, watch } from 'vue';
|
import { onUnmounted, ref, watch } from 'vue';
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
|
|
||||||
const name = 'unixClock';
|
const name = 'unixClock';
|
||||||
|
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import { GetFormResultType } from '@/scripts/form.js';
|
import { GetFormResultType } from '@/scripts/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
|
@ -1545,7 +1545,7 @@ export type Endpoints = {
|
||||||
receiveAnnouncementEmail?: boolean;
|
receiveAnnouncementEmail?: boolean;
|
||||||
alwaysMarkNsfw?: boolean;
|
alwaysMarkNsfw?: boolean;
|
||||||
mutedWords?: string[][];
|
mutedWords?: string[][];
|
||||||
mutingNotificationTypes?: Notification_2['type'][];
|
notificationRecieveConfig?: any;
|
||||||
emailNotificationTypes?: string[];
|
emailNotificationTypes?: string[];
|
||||||
alsoKnownAs?: string[];
|
alsoKnownAs?: string[];
|
||||||
};
|
};
|
||||||
|
@ -2475,7 +2475,22 @@ type MeDetailed = UserDetailed & {
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
isExplorable: boolean;
|
isExplorable: boolean;
|
||||||
mutedWords: string[][];
|
mutedWords: string[][];
|
||||||
mutingNotificationTypes: string[];
|
notificationRecieveConfig: {
|
||||||
|
[notificationType in typeof notificationTypes_2[number]]?: {
|
||||||
|
type: 'all';
|
||||||
|
} | {
|
||||||
|
type: 'never';
|
||||||
|
} | {
|
||||||
|
type: 'following';
|
||||||
|
} | {
|
||||||
|
type: 'follower';
|
||||||
|
} | {
|
||||||
|
type: 'mutualFollow';
|
||||||
|
} | {
|
||||||
|
type: 'list';
|
||||||
|
userListId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
noCrawle: boolean;
|
noCrawle: boolean;
|
||||||
receiveAnnouncementEmail: boolean;
|
receiveAnnouncementEmail: boolean;
|
||||||
usePasswordLessLogin: boolean;
|
usePasswordLessLogin: boolean;
|
||||||
|
@ -2619,7 +2634,7 @@ type ModerationLog = {
|
||||||
});
|
});
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation"];
|
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd"];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"];
|
export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"];
|
||||||
|
@ -2628,6 +2643,7 @@ export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"];
|
||||||
type Note = {
|
type Note = {
|
||||||
id: ID;
|
id: ID;
|
||||||
createdAt: DateString;
|
createdAt: DateString;
|
||||||
|
updatedAt?: DateString | null;
|
||||||
text: string | null;
|
text: string | null;
|
||||||
cw: string | null;
|
cw: string | null;
|
||||||
user: User;
|
user: User;
|
||||||
|
@ -2966,7 +2982,8 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
|
||||||
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:631:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:631:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
||||||
// src/entities.ts:579:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
// src/entities.ts:107:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
|
||||||
|
// src/entities.ts:595:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||||
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
||||||
|
|
||||||
// (No @packageDocumentation comment for this package)
|
// (No @packageDocumentation comment for this package)
|
||||||
|
|
|
@ -430,7 +430,7 @@ export type Endpoints = {
|
||||||
receiveAnnouncementEmail?: boolean;
|
receiveAnnouncementEmail?: boolean;
|
||||||
alwaysMarkNsfw?: boolean;
|
alwaysMarkNsfw?: boolean;
|
||||||
mutedWords?: string[][];
|
mutedWords?: string[][];
|
||||||
mutingNotificationTypes?: Notification['type'][];
|
notificationRecieveConfig?: any;
|
||||||
emailNotificationTypes?: string[];
|
emailNotificationTypes?: string[];
|
||||||
alsoKnownAs?: string[];
|
alsoKnownAs?: string[];
|
||||||
}; res: MeDetailed; };
|
}; res: MeDetailed; };
|
||||||
|
|
|
@ -75,6 +75,9 @@ export const moderationLogTypes = [
|
||||||
'unmarkSensitiveDriveFile',
|
'unmarkSensitiveDriveFile',
|
||||||
'resolveAbuseReport',
|
'resolveAbuseReport',
|
||||||
'createInvitation',
|
'createInvitation',
|
||||||
|
'createAd',
|
||||||
|
'updateAd',
|
||||||
|
'deleteAd',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ModerationLogPayloads = {
|
export type ModerationLogPayloads = {
|
||||||
|
@ -220,4 +223,17 @@ export type ModerationLogPayloads = {
|
||||||
createInvitation: {
|
createInvitation: {
|
||||||
invitations: any[];
|
invitations: any[];
|
||||||
};
|
};
|
||||||
|
createAd: {
|
||||||
|
adId: string;
|
||||||
|
ad: any;
|
||||||
|
};
|
||||||
|
updateAd: {
|
||||||
|
adId: string;
|
||||||
|
before: any;
|
||||||
|
after: any;
|
||||||
|
};
|
||||||
|
deleteAd: {
|
||||||
|
adId: string;
|
||||||
|
ad: any;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ModerationLogPayloads } from './consts.js';
|
import { ModerationLogPayloads, notificationTypes } from './consts.js';
|
||||||
|
|
||||||
export type ID = string;
|
export type ID = string;
|
||||||
export type DateString = string;
|
export type DateString = string;
|
||||||
|
@ -104,7 +104,22 @@ export type MeDetailed = UserDetailed & {
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
isExplorable: boolean;
|
isExplorable: boolean;
|
||||||
mutedWords: string[][];
|
mutedWords: string[][];
|
||||||
mutingNotificationTypes: string[];
|
notificationRecieveConfig: {
|
||||||
|
[notificationType in typeof notificationTypes[number]]?: {
|
||||||
|
type: 'all';
|
||||||
|
} | {
|
||||||
|
type: 'never';
|
||||||
|
} | {
|
||||||
|
type: 'following';
|
||||||
|
} | {
|
||||||
|
type: 'follower';
|
||||||
|
} | {
|
||||||
|
type: 'mutualFollow';
|
||||||
|
} | {
|
||||||
|
type: 'list';
|
||||||
|
userListId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
noCrawle: boolean;
|
noCrawle: boolean;
|
||||||
receiveAnnouncementEmail: boolean;
|
receiveAnnouncementEmail: boolean;
|
||||||
usePasswordLessLogin: boolean;
|
usePasswordLessLogin: boolean;
|
||||||
|
@ -162,6 +177,7 @@ export type GalleryPost = {
|
||||||
export type Note = {
|
export type Note = {
|
||||||
id: ID;
|
id: ID;
|
||||||
createdAt: DateString;
|
createdAt: DateString;
|
||||||
|
updatedAt?: DateString | null;
|
||||||
text: string | null;
|
text: string | null;
|
||||||
cw: string | null;
|
cw: string | null;
|
||||||
user: User;
|
user: User;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
/*
|
/*
|
||||||
* Notification manager for SW
|
* Notification manager for SW
|
||||||
*/
|
*/
|
||||||
import type { BadgeNames, PushNotificationDataMap } from '@/types';
|
import type { BadgeNames, PushNotificationDataMap } from '@/types.js';
|
||||||
import { char2fileName } from '@/scripts/twemoji-base.js';
|
import { char2fileName } from '@/scripts/twemoji-base.js';
|
||||||
import { cli } from '@/scripts/operations.js';
|
import { cli } from '@/scripts/operations.js';
|
||||||
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
* 各種操作
|
* 各種操作
|
||||||
*/
|
*/
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { SwMessage, SwMessageOrderType } from '@/types';
|
import type { SwMessage, SwMessageOrderType } from '@/types.js';
|
||||||
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
||||||
import { getUrlWithLoginId } from '@/scripts/login-id.js';
|
import { getUrlWithLoginId } from '@/scripts/login-id.js';
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { get } from 'idb-keyval';
|
import { get } from 'idb-keyval';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { PushNotificationDataMap } from '@/types';
|
import type { PushNotificationDataMap } from '@/types.js';
|
||||||
import { createEmptyNotification, createNotification } from '@/scripts/create-notification.js';
|
import { createEmptyNotification, createNotification } from '@/scripts/create-notification.js';
|
||||||
import { swLang } from '@/scripts/lang.js';
|
import { swLang } from '@/scripts/lang.js';
|
||||||
import * as swos from '@/scripts/operations.js';
|
import * as swos from '@/scripts/operations.js';
|
||||||
|
|
Loading…
Reference in New Issue