Merge remote-branch 'misskey/develop'

This commit is contained in:
NoriDev 2023-12-22 00:11:05 +09:00
commit 0a3e7413a3
141 changed files with 1119 additions and 266 deletions

View file

@ -14,7 +14,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View file

@ -37,7 +37,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -19,7 +19,7 @@ jobs:
with: with:
version: 8 version: 8
run_install: false run_install: false
- uses: actions/setup-node@v4.0.0 - uses: actions/setup-node@v4.0.1
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -46,7 +46,7 @@ jobs:
with: with:
version: 7 version: 7
run_install: false run_install: false
- uses: actions/setup-node@v4.0.0 - uses: actions/setup-node@v4.0.1
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -72,7 +72,7 @@ jobs:
with: with:
version: 7 version: 7
run_install: false run_install: false
- uses: actions/setup-node@v4.0.0 - uses: actions/setup-node@v4.0.1
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View file

@ -17,7 +17,7 @@ jobs:
services: services:
postgres: postgres:
image: postgres:13 image: postgres:15
ports: ports:
- 54312:5432 - 54312:5432
env: env:
@ -38,7 +38,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -26,7 +26,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }} - name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -25,7 +25,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
@ -56,7 +56,7 @@ jobs:
services: services:
postgres: postgres:
image: postgres:13 image: postgres:15
ports: ports:
- 54312:5432 - 54312:5432
env: env:
@ -83,7 +83,7 @@ jobs:
version: 7 version: 7
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -28,7 +28,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -17,6 +17,7 @@
### Note ### Note
- Node.js 20.10.0が最小要件になりました - Node.js 20.10.0が最小要件になりました
- 絵文字の追加辞書を既にインストールしている場合は、お手数ですが再インストールのほどお願いします
- 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。 - 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。
**影響:** **影響:**
@ -31,9 +32,12 @@
- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed) - Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)
- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
- Feat: TL上からートが見えなくなるワードミュートであるハードミュートを追加 - Feat: TL上からートが見えなくなるワードミュートであるハードミュートを追加
- Enhance: 公開ロールにアサインされたときに通知が作成されるように
- Enhance: アイコンデコレーションを複数設定できるように - Enhance: アイコンデコレーションを複数設定できるように
- Enhance: アイコンデコレーションの位置を微調整できるように - Enhance: アイコンデコレーションの位置を微調整できるように
- Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072 - Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072
- Enhance: ローカリゼーションの更新
- Enhance: 依存関係の更新
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 - Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
### Client ### Client
@ -79,6 +83,7 @@
- Fix: AiScriptの`readline`が不正な値を返すことがある問題を修正 - Fix: AiScriptの`readline`が不正な値を返すことがある問題を修正
- Fix: 投票のみ/画像のみの引用RNが、通知欄でただのRNとして判定されるバグを修正 - Fix: 投票のみ/画像のみの引用RNが、通知欄でただのRNとして判定されるバグを修正
- Fix: CWをつけて引用RNしても、普通のRNとして扱われてしまうバグを修正しました。 - Fix: CWをつけて引用RNしても、普通のRNとして扱われてしまうバグを修正しました。
- Fix: 「画像が1枚のみのメディアリストの高さ」を「デフォルト」以外に設定していると、CWの中などに添付された画像が見られないバグを修正
### Server ### Server
- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように
@ -97,6 +102,7 @@
- Fix: 「みつける」が年越し時に壊れる問題を修正 - Fix: 「みつける」が年越し時に壊れる問題を修正
- Fix: アカウントをブロックした際に、自身のユーザーのページでノートが相手に表示される問題を修正 - Fix: アカウントをブロックした際に、自身のユーザーのページでノートが相手に表示される問題を修正
- Fix: モデレーションログがモデレーターは閲覧できないように修正 - Fix: モデレーションログがモデレーターは閲覧できないように修正
- Fix: ハッシュタグのトレンド除外設定が即時に効果を持つように修正
- Fix: HTTP Digestヘッダのアルゴリズム部分に大文字の"SHA-256"しか使えない - Fix: HTTP Digestヘッダのアルゴリズム部分に大文字の"SHA-256"しか使えない
- Fix: 管理者用APIのアクセス権限が適切に設定されていない問題を修正 - Fix: 管理者用APIのアクセス権限が適切に設定されていない問題を修正

View file

@ -27,7 +27,7 @@ spec:
ports: ports:
- containerPort: 3000 - containerPort: 3000
- name: postgres - name: postgres
image: postgres:14-alpine image: postgres:15-alpine
env: env:
- name: POSTGRES_USER - name: POSTGRES_USER
value: "example-cherrypick-user" value: "example-cherrypick-user"
@ -38,7 +38,7 @@ spec:
ports: ports:
- containerPort: 5432 - containerPort: 5432
- name: redis - name: redis
image: redis:alpine image: redis:7-alpine
ports: ports:
- containerPort: 6379 - containerPort: 6379
volumes: volumes:

1
locales/index.d.ts vendored
View file

@ -2642,6 +2642,7 @@ export interface Locale {
"pollEnded": string; "pollEnded": string;
"newNote": string; "newNote": string;
"unreadAntennaNote": string; "unreadAntennaNote": string;
"roleAssigned": string;
"emptyPushNotificationMessage": string; "emptyPushNotificationMessage": string;
"achievementEarned": string; "achievementEarned": string;
"testNotification": string; "testNotification": string;

View file

@ -2540,6 +2540,7 @@ _notification:
pollEnded: "アンケートの結果が出ました" pollEnded: "アンケートの結果が出ました"
newNote: "新しい投稿" newNote: "新しい投稿"
unreadAntennaNote: "アンテナ {name}" unreadAntennaNote: "アンテナ {name}"
roleAssigned: "ロールが付与されました"
emptyPushNotificationMessage: "プッシュ通知の更新をしました" emptyPushNotificationMessage: "プッシュ通知の更新をしました"
achievementEarned: "実績を獲得" achievementEarned: "実績を獲得"
testNotification: "通知テスト" testNotification: "通知テスト"

View file

@ -974,8 +974,8 @@ makeReactionsPublicDescription: "나의 리액션을 누구나 볼 수 있게
classic: "클래식" classic: "클래식"
muteThread: "이 글타래 뮤트" muteThread: "이 글타래 뮤트"
unmuteThread: "글타래 뮤트 해제" unmuteThread: "글타래 뮤트 해제"
ffVisibility: "내 인맥의 공개 범위" followingVisibility: "팔로우의 공개 범위"
ffVisibilityDescription: "나의 팔로우와 팔로워 정보에 대한 공개 범위를 설정할 수 있어요." followersVisibility: "팔로워의 공개 범위"
continueThread: "이 글타래 이어서 보기" continueThread: "이 글타래 이어서 보기"
deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 돼요. 그래도 계속할까요? " deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 돼요. 그래도 계속할까요? "
incorrectPassword: "비밀번호가 다른 것 같아요!" incorrectPassword: "비밀번호가 다른 것 같아요!"
@ -1281,7 +1281,8 @@ code: "코드"
reloadRequiredToApplySettings: "설정을 반영하려면 페이지를 다시 불러와야 해요." reloadRequiredToApplySettings: "설정을 반영하려면 페이지를 다시 불러와야 해요."
remainingN: "남음: {n}" remainingN: "남음: {n}"
overwriteContentConfirm: "현재 내용을 덮어쓰기 하게 돼요. 그래도 계속 진행할까요?" overwriteContentConfirm: "현재 내용을 덮어쓰기 하게 돼요. 그래도 계속 진행할까요?"
seasonalScreenEffect: "계절 따른 화면 연출" seasonalScreenEffect: "계절에 따른 화면 연출"
decorate: "장식하기"
showUnreadNotificationsCount: "읽지 않은 알림 수 표시" showUnreadNotificationsCount: "읽지 않은 알림 수 표시"
showCatOnly: "고양이만 보기" showCatOnly: "고양이만 보기"
additionalPermissionsForFlash: "Play에 대한 추가 권한" additionalPermissionsForFlash: "Play에 대한 추가 권한"
@ -2134,7 +2135,7 @@ _sfx:
channel: "채널 알림" channel: "채널 알림"
reaction: "리액션 선택" reaction: "리액션 선택"
_soundSettings: _soundSettings:
driveFile: "드라이브에 있는 오디오 사용" driveFile: "드라이브에 있는 오디오 파일 사용"
driveFileWarn: "드라이브에 있는 파일을 선택해 주세요." driveFileWarn: "드라이브에 있는 파일을 선택해 주세요."
driveFileTypeWarn: "이 파일은 지원되지 않는 형식이에요." driveFileTypeWarn: "이 파일은 지원되지 않는 형식이에요."
driveFileTypeWarnDescription: "오디오 파일을 선택해 주세요." driveFileTypeWarnDescription: "오디오 파일을 선택해 주세요."

View file

@ -1,7 +1,7 @@
{ {
"name": "cherrypick", "name": "cherrypick",
"version": "4.6.0-beta.5", "version": "4.6.0-beta.5",
"basedMisskeyVersion": "2023.12.0-beta.5", "basedMisskeyVersion": "2023.12.0-beta.6",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -77,6 +77,17 @@ export class FeaturedService {
return Array.from(ranking.keys()); return Array.from(ranking.keys());
} }
@bindThis
private async removeFromRanking(name: string, windowRange: number, element: string): Promise<void> {
const currentWindow = this.getCurrentWindow(windowRange);
const previousWindow = currentWindow - 1;
const redisPipeline = this.redisClient.pipeline();
redisPipeline.zrem(`${name}:${currentWindow}`, element);
redisPipeline.zrem(`${name}:${previousWindow}`, element);
await redisPipeline.exec();
}
@bindThis @bindThis
public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> { public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> {
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score); return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
@ -126,4 +137,9 @@ export class FeaturedService {
public getHashtagsRanking(threshold: number): Promise<string[]> { public getHashtagsRanking(threshold: number): Promise<string[]> {
return this.getRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, threshold); return this.getRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, threshold);
} }
@bindThis
public removeHashtagsFromRanking(hashtag: string): Promise<void> {
return this.removeFromRanking('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, hashtag);
}
} }

View file

@ -11,6 +11,7 @@ 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 type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
@ -25,6 +26,7 @@ export class MetaService implements OnApplicationShutdown {
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
private featuredService: FeaturedService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
//this.onMessage = this.onMessage.bind(this); //this.onMessage = this.onMessage.bind(this);
@ -95,6 +97,8 @@ export class MetaService implements OnApplicationShutdown {
@bindThis @bindThis
public async update(data: Partial<MiMeta>): Promise<MiMeta> { public async update(data: Partial<MiMeta>): Promise<MiMeta> {
let before: MiMeta | undefined;
const updated = await this.db.transaction(async transactionalEntityManager => { const updated = await this.db.transaction(async transactionalEntityManager => {
const metas = await transactionalEntityManager.find(MiMeta, { const metas = await transactionalEntityManager.find(MiMeta, {
order: { order: {
@ -102,10 +106,10 @@ export class MetaService implements OnApplicationShutdown {
}, },
}); });
const meta = metas[0]; before = metas[0];
if (meta) { if (before) {
await transactionalEntityManager.update(MiMeta, meta.id, data); await transactionalEntityManager.update(MiMeta, before.id, data);
const metas = await transactionalEntityManager.find(MiMeta, { const metas = await transactionalEntityManager.find(MiMeta, {
order: { order: {
@ -119,6 +123,21 @@ export class MetaService implements OnApplicationShutdown {
} }
}); });
if (data.hiddenTags) {
process.nextTick(() => {
const hiddenTags = new Set<string>(data.hiddenTags);
if (before) {
for (const previousHiddenTag of before.hiddenTags) {
hiddenTags.delete(previousHiddenTag);
}
}
for (const hiddenTag of hiddenTags) {
this.featuredService.removeHashtagsFromRanking(hiddenTag);
}
});
}
this.globalEventService.publishInternalEvent('metaUpdated', updated); this.globalEventService.publishInternalEvent('metaUpdated', updated);
return updated; return updated;

View file

@ -6,7 +6,14 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { In } from 'typeorm'; import { In } from 'typeorm';
import type { MiRole, MiRoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js'; import { ModuleRef } from '@nestjs/core';
import type {
MiRole,
MiRoleAssignment,
RoleAssignmentsRepository,
RolesRepository,
UsersRepository,
} from '@/models/_.js';
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js'; import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -16,12 +23,13 @@ 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 type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import { NotificationService } from '@/core/NotificationService.js';
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
export type RolePolicies = { export type RolePolicies = {
gtlAvailable: boolean; gtlAvailable: boolean;
@ -80,14 +88,17 @@ export const DEFAULT_POLICIES: RolePolicies = {
}; };
@Injectable() @Injectable()
export class RoleService implements OnApplicationShutdown { export class RoleService implements OnApplicationShutdown, OnModuleInit {
private rolesCache: MemorySingleCache<MiRole[]>; private rolesCache: MemorySingleCache<MiRole[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>; private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
private notificationService: NotificationService;
public static AlreadyAssignedError = class extends Error {}; public static AlreadyAssignedError = class extends Error {};
public static NotAssignedError = class extends Error {}; public static NotAssignedError = class extends Error {};
constructor( constructor(
private moduleRef: ModuleRef,
@Inject(DI.redis) @Inject(DI.redis)
private redisClient: Redis.Redis, private redisClient: Redis.Redis,
@ -122,6 +133,10 @@ export class RoleService implements OnApplicationShutdown {
this.redisForSub.on('message', this.onMessage); this.redisForSub.on('message', this.onMessage);
} }
async onModuleInit() {
this.notificationService = this.moduleRef.get(NotificationService.name);
}
@bindThis @bindThis
private async onMessage(_: string, data: string): Promise<void> { private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data); const obj = JSON.parse(data);
@ -430,6 +445,12 @@ export class RoleService implements OnApplicationShutdown {
this.globalEventService.publishInternalEvent('userRoleAssigned', created); this.globalEventService.publishInternalEvent('userRoleAssigned', created);
if (role.isPublic) {
this.notificationService.createNotification(userId, 'roleAssigned', {
roleId: roleId,
});
}
if (moderator) { if (moderator) {
const user = await this.usersRepository.findOneByOrFail({ id: userId }); const user = await this.usersRepository.findOneByOrFail({ id: userId });
this.moderationLogService.log(moderator, 'assignRole', { this.moderationLogService.log(moderator, 'assignRole', {

View file

@ -10,15 +10,15 @@ import type { MiUser } from '@/models/User.js';
import type { MiUserList } from '@/models/UserList.js'; import type { MiUserList } from '@/models/UserList.js';
import type { MiUserListMembership } from '@/models/UserListMembership.js'; import type { MiUserListMembership } from '@/models/UserListMembership.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.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 { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.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 { RedisKVCache } from '@/misc/cache.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js'; import { RoleService } from '@/core/RoleService.js';
@Injectable() @Injectable()
export class UserListService implements OnApplicationShutdown { export class UserListService implements OnApplicationShutdown {

View file

@ -15,8 +15,8 @@ import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js'; import { isNotNull } from '@/misc/is-not-null.js';
import { FilterUnionByProperty, notificationTypes } from '@/types.js'; import { FilterUnionByProperty, notificationTypes } from '@/types.js';
import { RoleEntityService } from './RoleEntityService.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { UserEntityService } from './UserEntityService.js'; import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js';
import type { UserGroupInvitationEntityService } from './UserGroupInvitationEntityService.js'; import type { UserGroupInvitationEntityService } from './UserGroupInvitationEntityService.js';
@ -28,8 +28,8 @@ const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 're
export class NotificationEntityService implements OnModuleInit { export class NotificationEntityService implements OnModuleInit {
private userEntityService: UserEntityService; private userEntityService: UserEntityService;
private noteEntityService: NoteEntityService; private noteEntityService: NoteEntityService;
private roleEntityService: RoleEntityService;
private userGroupInvitationEntityService: UserGroupInvitationEntityService; private userGroupInvitationEntityService: UserGroupInvitationEntityService;
private customEmojiService: CustomEmojiService;
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
@ -49,15 +49,14 @@ export class NotificationEntityService implements OnModuleInit {
//private userEntityService: UserEntityService, //private userEntityService: UserEntityService,
//private noteEntityService: NoteEntityService, //private noteEntityService: NoteEntityService,
//private userGroupInvitationEntityService: UserGroupInvitationEntityService, //private userGroupInvitationEntityService: UserGroupInvitationEntityService,
//private customEmojiService: CustomEmojiService,
) { ) {
} }
onModuleInit() { onModuleInit() {
this.userEntityService = this.moduleRef.get('UserEntityService'); this.userEntityService = this.moduleRef.get('UserEntityService');
this.noteEntityService = this.moduleRef.get('NoteEntityService'); this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.roleEntityService = this.moduleRef.get('RoleEntityService');
this.userGroupInvitationEntityService = this.moduleRef.get('UserGroupInvitationEntityService'); this.userGroupInvitationEntityService = this.moduleRef.get('UserGroupInvitationEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
} }
@bindThis @bindThis
@ -88,6 +87,7 @@ export class NotificationEntityService implements OnModuleInit {
detail: false, detail: false,
}) })
) : undefined; ) : undefined;
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
return await awaitAll({ return await awaitAll({
id: notification.id, id: notification.id,
@ -99,6 +99,9 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'reaction' ? { ...(notification.type === 'reaction' ? {
reaction: notification.reaction, reaction: notification.reaction,
} : {}), } : {}),
...(notification.type === 'roleAssigned' ? {
role: role,
} : {}),
// ...(notification.type === 'pollEnded' ? { // ...(notification.type === 'pollEnded' ? {
// note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { // note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
// detail: true, // detail: true,
@ -240,6 +243,8 @@ export class NotificationEntityService implements OnModuleInit {
}); });
} }
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
return await awaitAll({ return await awaitAll({
id: notification.id, id: notification.id,
createdAt: new Date(notification.createdAt).toISOString(), createdAt: new Date(notification.createdAt).toISOString(),
@ -250,6 +255,9 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'reaction' ? { ...(notification.type === 'reaction' ? {
reaction: notification.reaction, reaction: notification.reaction,
} : {}), } : {}),
...(notification.type === 'roleAssigned' ? {
role: role,
} : {}),
...(notification.type === 'groupInvited' ? { ...(notification.type === 'groupInvited' ? {
invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId), invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId),
} : {}), } : {}),

View file

@ -40,6 +40,7 @@ import { packedFlashSchema } from '@/models/json-schema/flash.js';
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
import { packedSigninSchema } from '@/models/json-schema/signin.js'; import { packedSigninSchema } from '@/models/json-schema/signin.js';
import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js'; import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js';
import { packedAdSchema } from '@/models/json-schema/ad.js';
export const refs = { export const refs = {
UserLite: packedUserLiteSchema, UserLite: packedUserLiteSchema,
@ -52,6 +53,7 @@ export const refs = {
UserList: packedUserListSchema, UserList: packedUserListSchema,
UserGroup: packedUserGroupSchema, UserGroup: packedUserGroupSchema,
Ad: packedAdSchema,
Announcement: packedAnnouncementSchema, Announcement: packedAnnouncementSchema,
App: packedAppSchema, App: packedAppSchema,
MessagingMessage: packedMessagingMessageSchema, MessagingMessage: packedMessagingMessageSchema,

View file

@ -3,12 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { notificationTypes } from '@/types.js';
import { MiUser } from './User.js'; import { MiUser } from './User.js';
import { MiNote } from './Note.js'; import { MiNote } from './Note.js';
import { MiFollowRequest } from './FollowRequest.js';
import { MiUserGroupInvitation } from './UserGroupInvitation.js'; import { MiUserGroupInvitation } from './UserGroupInvitation.js';
import { MiAccessToken } from './AccessToken.js'; import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js';
export type MiNotification = { export type MiNotification = {
type: 'note'; type: 'note';
@ -69,6 +68,11 @@ export type MiNotification = {
id: string; id: string;
createdAt: string; createdAt: string;
notifierId: MiUser['id']; notifierId: MiUser['id'];
} | {
type: 'roleAssigned';
id: string;
createdAt: string;
roleId: MiRole['id'];
} | { } | {
type: 'groupInvited'; type: 'groupInvited';
id: string; id: string;

View file

@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const packedAdSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false,
nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
expiresAt: {
type: 'string',
optional: false,
nullable: false,
format: 'date-time',
},
startsAt: {
type: 'string',
optional: false,
nullable: false,
format: 'date-time',
},
place: {
type: 'string',
optional: false,
nullable: false,
},
priority: {
type: 'string',
optional: false,
nullable: false,
},
ratio: {
type: 'number',
optional: false,
nullable: false,
},
url: {
type: 'string',
optional: false,
nullable: false,
},
imageUrl: {
type: 'string',
optional: false,
nullable: false,
},
memo: {
type: 'string',
optional: false,
nullable: false,
},
dayOfWeek: {
type: 'integer',
optional: false,
nullable: false,
},
},
} as const;

View file

@ -566,9 +566,7 @@ export const packedMeDetailedOnlySchema = {
mention: notificationRecieveConfig, mention: notificationRecieveConfig,
reaction: notificationRecieveConfig, reaction: notificationRecieveConfig,
pollEnded: notificationRecieveConfig, pollEnded: notificationRecieveConfig,
achievementEarned: notificationRecieveConfig,
receiveFollowRequest: notificationRecieveConfig, receiveFollowRequest: notificationRecieveConfig,
followRequestAccepted: notificationRecieveConfig,
groupInvited: notificationRecieveConfig, groupInvited: notificationRecieveConfig,
}, },
}, },

View file

@ -155,8 +155,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.systemQueueWorker this.systemQueueWorker
.on('active', (job) => systemLogger.debug(`active id=${job.id}`)) .on('active', (job) => systemLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) .on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => systemLogger.error(`error ${err}`, { e: renderError(err) })) .on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion
@ -194,8 +194,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.dbQueueWorker this.dbQueueWorker
.on('active', (job) => dbLogger.debug(`active id=${job.id}`)) .on('active', (job) => dbLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) .on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => dbLogger.error(`error ${err}`, { e: renderError(err) })) .on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion
@ -218,8 +218,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.deliverQueueWorker this.deliverQueueWorker
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) .on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => deliverLogger.error(`error ${err}`, { e: renderError(err) })) .on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion
@ -242,8 +242,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.inboxQueueWorker this.inboxQueueWorker
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) })) .on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => inboxLogger.error(`error ${err}`, { e: renderError(err) })) .on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion
@ -266,8 +266,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.webhookDeliverQueueWorker this.webhookDeliverQueueWorker
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) .on('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => webhookLogger.error(`error ${err}`, { e: renderError(err) })) .on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion
@ -295,8 +295,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker this.relationshipQueueWorker
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) .on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => relationshipLogger.error(`error ${err}`, { e: renderError(err) })) .on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion
@ -318,8 +318,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.objectStorageQueueWorker this.objectStorageQueueWorker
.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => objectStorageLogger.error(`error ${err}`, { e: renderError(err) })) .on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion

View file

@ -25,6 +25,11 @@ export const meta = {
id: 'cb865949-8af5-4062-a88c-ef55e8786d1d', id: 'cb865949-8af5-4062-a88c-ef55e8786d1d',
}, },
}, },
res: {
type: 'object',
optional: false, nullable: false,
ref: 'User',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -17,6 +17,12 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireModerator: true, requireModerator: true,
res: {
type: 'object',
optional: false,
nullable: false,
ref: 'Ad',
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -63,7 +69,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
ad: ad, ad: ad,
}); });
return ad; return {
id: ad.id,
expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek,
url: ad.url,
imageUrl: ad.imageUrl,
priority: ad.priority,
ratio: ad.ratio,
place: ad.place,
memo: ad.memo,
};
}); });
} }
} }

View file

@ -16,6 +16,17 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireModerator: true, requireModerator: true,
res: {
type: 'array',
optional: false,
nullable: false,
items: {
type: 'object',
optional: false,
nullable: false,
ref: 'Ad',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -46,7 +57,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
const ads = await query.limit(ps.limit).getMany(); const ads = await query.limit(ps.limit).getMany();
return ads; return ads.map(ad => ({
id: ad.id,
expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek,
url: ad.url,
imageUrl: ad.imageUrl,
memo: ad.memo,
place: ad.place,
priority: ad.priority,
ratio: ad.ratio,
}));
}); });
} }
} }

View file

@ -31,6 +31,8 @@ export const meta = {
id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975',
}, },
}, },
ref: 'EmojiDetailed',
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -32,6 +32,8 @@ export const meta = {
id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975',
}, },
}, },
ref: 'EmojiDetailed',
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -15,6 +15,16 @@ export const meta = {
kind: 'read:admin', kind: 'read:admin',
tags: ['admin'], tags: ['admin'],
res: {
type: 'array',
items: {
type: 'object',
properties: {
tablename: { type: 'string' },
indexname: { type: 'string' },
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -16,6 +16,25 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireModerator: true, requireModerator: true,
res: {
type: 'array',
optional: false,
nullable: false,
items: {
type: 'object',
optional: false,
nullable: false,
properties: {
ip: { type: 'string' },
createdAt: {
type: 'string',
optional: false,
nullable: false,
format: 'date-time',
},
},
},
}
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -28,6 +28,20 @@ export const meta = {
id: '224eff5e-2488-4b18-b3e7-f50d94421648', id: '224eff5e-2488-4b18-b3e7-f50d94421648',
}, },
}, },
res: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
createdAt: { type: 'string', format: 'date-time' },
user: { ref: 'UserDetailed' },
expiresAt: { type: 'string', format: 'date-time', nullable: true },
},
required: ['id', 'createdAt', 'user'],
},
}
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -80,7 +94,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
id: assign.id, id: assign.id,
createdAt: this.idService.parse(assign.id).date.toISOString(), createdAt: this.idService.parse(assign.id).date.toISOString(),
user: await this.userEntityService.pack(assign.user!, me, { detail: true }), user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
expiresAt: assign.expiresAt, expiresAt: assign.expiresAt?.toISOString() ?? null,
}))); })));
}); });
} }

View file

@ -11,6 +11,23 @@ export const meta = {
requireCredential: false, requireCredential: false,
tags: ['meta'], tags: ['meta'],
res: {
type: 'object',
nullable: true,
properties: {
params: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
type: { type: 'string' },
},
},
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -18,6 +18,92 @@ export const meta = {
allowGet: true, allowGet: true,
cacheSec: 60 * 60, cacheSec: 60 * 60,
res: {
type: 'object',
optional: false,
nullable: false,
properties: {
topSubInstances: {
type: 'array',
optional: false,
nullable: false,
items: {
properties: {
id: { type: 'string' },
firstRetrievedAt: { type: 'string' },
host: { type: 'string' },
usersCount: { type: 'number' },
notesCount: { type: 'number' },
followingCount: { type: 'number' },
followersCount: { type: 'number' },
isNotResponding: { type: 'boolean' },
isSuspended: { type: 'boolean' },
isBlocked: { type: 'boolean' },
softwareName: { type: 'string' },
softwareVersion: { type: 'string' },
openRegistrations: { type: 'boolean' },
name: { type: 'string' },
description: { type: 'string' },
maintainerName: { type: 'string' },
maintainerEmail: { type: 'string' },
isSilenced: { type: 'boolean' },
iconUrl: { type: 'string' },
faviconUrl: { type: 'string' },
themeColor: { type: 'string' },
infoUpdatedAt: {
type: 'string',
nullable: true,
},
latestRequestReceivedAt: {
type: 'string',
nullable: true,
},
}
},
},
otherFollowersCount: { type: 'number' },
topPubInstances: {
type: 'array',
optional: false,
nullable: false,
items: {
properties: {
id: { type: 'string' },
firstRetrievedAt: { type: 'string' },
host: { type: 'string' },
usersCount: { type: 'number' },
notesCount: { type: 'number' },
followingCount: { type: 'number' },
followersCount: { type: 'number' },
isNotResponding: { type: 'boolean' },
isSuspended: { type: 'boolean' },
isBlocked: { type: 'boolean' },
softwareName: { type: 'string' },
softwareVersion: { type: 'string' },
openRegistrations: { type: 'boolean' },
name: { type: 'string' },
description: { type: 'string' },
maintainerName: { type: 'string' },
maintainerEmail: { type: 'string' },
isSilenced: { type: 'boolean' },
iconUrl: { type: 'string' },
faviconUrl: { type: 'string' },
themeColor: { type: 'string' },
infoUpdatedAt: {
type: 'string',
nullable: true,
},
latestRequestReceivedAt: {
type: 'string',
nullable: true,
},
}
},
},
otherFollowingCount: { type: 'number' },
},
}
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -32,6 +32,18 @@ export const meta = {
id: '693ba8ba-b486-40df-a174-72f8279b56a4', id: '693ba8ba-b486-40df-a174-72f8279b56a4',
}, },
}, },
res: {
type: 'object',
properties: {
type: {
type: 'string',
},
data: {
type: 'string',
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -16,6 +16,18 @@ export const meta = {
requireCredential: false, requireCredential: false,
allowGet: true, allowGet: true,
cacheSec: 60 * 3, cacheSec: 60 * 3,
res: {
type: 'object',
properties: {
items: {
type: 'array',
items: {
type: 'object',
},
}
}
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -27,6 +27,12 @@ export const meta = {
errors: { errors: {
}, },
res: {
type: 'object',
optional: false, nullable: false,
ref: 'Flash',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -16,6 +16,16 @@ export const meta = {
requireCredential: false, requireCredential: false,
allowGet: true, allowGet: true,
cacheSec: 60 * 1, cacheSec: 60 * 1,
res: {
type: 'object',
optional: false, nullable: false,
properties: {
count: {
type: 'number',
nullable: false,
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -32,6 +32,16 @@ export const meta = {
id: '798d6847-b1ed-4f9c-b1f9-163c42655995', id: '798d6847-b1ed-4f9c-b1f9-163c42655995',
}, },
}, },
res: {
type: 'object',
nullable: false,
optional: false,
properties: {
id: { type: 'string' },
name: { type: 'string' },
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -36,6 +36,140 @@ export const meta = {
id: 'bf32b864-449b-47b8-974e-f9a5468546f1', id: 'bf32b864-449b-47b8-974e-f9a5468546f1',
}, },
}, },
res: {
type: 'object',
nullable: false,
optional: false,
properties: {
rp: {
type: 'object',
properties: {
id: {
type: 'string',
nullable: true,
},
},
},
user: {
type: 'object',
properties: {
id: {
type: 'string',
},
name: {
type: 'string',
},
displayName: {
type: 'string',
},
},
},
challenge: {
type: 'string',
},
pubKeyCredParams: {
type: 'array',
items: {
type: 'object',
properties: {
type: {
type: 'string',
},
alg: {
type: 'number',
},
},
},
},
timeout: {
type: 'number',
nullable: true,
},
excludeCredentials: {
type: 'array',
nullable: true,
items: {
type: 'object',
properties: {
id: {
type: 'string',
},
type: {
type: 'string',
},
transports: {
type: 'array',
items: {
type: 'string',
enum: [
"ble",
"cable",
"hybrid",
"internal",
"nfc",
"smart-card",
"usb",
],
},
},
},
},
},
authenticatorSelection: {
type: 'object',
nullable: true,
properties: {
authenticatorAttachment: {
type: 'string',
enum: [
"cross-platform",
"platform",
],
},
requireResidentKey: {
type: 'boolean',
},
userVerification: {
type: 'string',
enum: [
"discouraged",
"preferred",
"required",
],
},
},
},
attestation: {
type: 'string',
nullable: true,
enum: [
"direct",
"enterprise",
"indirect",
"none",
],
},
extensions: {
type: 'object',
nullable: true,
properties: {
appid: {
type: 'string',
nullable: true,
},
credProps: {
type: 'boolean',
nullable: true,
},
hmacCreateSecret: {
type: 'boolean',
nullable: true,
},
},
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -26,6 +26,19 @@ export const meta = {
id: '78d6c839-20c9-4c66-b90a-fc0542168b48', id: '78d6c839-20c9-4c66-b90a-fc0542168b48',
}, },
}, },
res: {
type: 'object',
nullable: false,
optional: false,
properties: {
qr: { type: 'string' },
url: { type: 'string' },
secret: { type: 'string' },
label: { type: 'string' },
issuer: { type: 'string' },
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -13,6 +13,37 @@ export const meta = {
requireCredential: true, requireCredential: true,
secure: true, secure: true,
res: {
type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
name: {
type: 'string',
},
createdAt: {
type: 'string',
format: 'date-time',
},
lastUsedAt: {
type: 'string',
format: 'date-time',
},
permission: {
type: 'array',
uniqueItems: true,
items: {
type: 'string'
},
}
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -50,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
id: token.id, id: token.id,
name: token.name ?? token.app?.name, name: token.name ?? token.app?.name,
createdAt: this.idService.parse(token.id).date.toISOString(), createdAt: this.idService.parse(token.id).date.toISOString(),
lastUsedAt: token.lastUsedAt, lastUsedAt: token.lastUsedAt?.toISOString(),
permission: token.permission, permission: token.permission,
}))); })));
}); });

View file

@ -14,6 +14,36 @@ export const meta = {
requireCredential: true, requireCredential: true,
secure: true, secure: true,
res: {
type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
name: {
type: 'string',
},
callbackUrl: {
type: 'string',
nullable: true,
},
permission: {
type: 'array',
uniqueItems: true,
items: {
type: 'string'
},
},
isAuthorized: {
type: 'boolean',
},
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -64,6 +64,10 @@ export const meta = {
id: 'b234a14e-9ebe-4581-8000-074b3c215962', id: 'b234a14e-9ebe-4581-8000-074b3c215962',
}, },
}, },
res: {
type: 'object',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -9,6 +9,10 @@ import { RegistryApiService } from '@/core/RegistryApiService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
res: {
type: 'object',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -18,6 +18,10 @@ export const meta = {
id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a', id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a',
}, },
}, },
res: {
type: 'object',
}
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -18,6 +18,10 @@ export const meta = {
id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a', id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a',
}, },
}, },
res: {
type: 'object',
}
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -3,12 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { RegistryApiService } from '@/core/RegistryApiService.js'; import { RegistryApiService } from '@/core/RegistryApiService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
res: {
type: 'object',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -3,13 +3,35 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { RegistryApiService } from '@/core/RegistryApiService.js'; import { RegistryApiService } from '@/core/RegistryApiService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
secure: true, secure: true,
res: {
type: 'array',
items: {
type: 'object',
properties: {
scopes: {
type: 'array',
items: {
type: 'array',
items: {
type: 'string',
}
}
},
domain: {
type: 'string',
nullable: true,
},
},
},
}
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -40,6 +40,11 @@ export const meta = {
id: 'a2defefb-f220-8849-0af6-17f816099323', id: 'a2defefb-f220-8849-0af6-17f816099323',
}, },
}, },
res: {
type: 'object',
ref: 'UserDetailed',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -27,6 +27,33 @@ export const meta = {
id: '87a9bb19-111e-4e37-81d3-a3e7426453b0', id: '87a9bb19-111e-4e37-81d3-a3e7426453b0',
}, },
}, },
res: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id'
},
userId: {
type: 'string',
format: 'misskey:id',
},
name: { type: 'string' },
on: {
type: 'array',
items: {
type: 'string',
enum: webhookEventTypes,
}
},
url: { type: 'string' },
secret: { type: 'string' },
active: { type: 'boolean' },
latestSentAt: { type: 'string', format: 'date-time', nullable: true },
latestStatus: { type: 'integer', nullable: true },
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -73,7 +100,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.globalEventService.publishInternalEvent('webhookCreated', webhook); this.globalEventService.publishInternalEvent('webhookCreated', webhook);
return webhook; return {
id: webhook.id,
userId: webhook.userId,
name: webhook.name,
on: webhook.on,
url: webhook.url,
secret: webhook.secret,
active: webhook.active,
latestSentAt: webhook.latestSentAt?.toISOString(),
latestStatus: webhook.latestStatus,
};
}); });
} }
} }

View file

@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { webhookEventTypes } from '@/models/Webhook.js';
import type { WebhooksRepository } from '@/models/_.js'; import type { WebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -14,6 +15,36 @@ export const meta = {
requireCredential: true, requireCredential: true,
kind: 'read:account', kind: 'read:account',
res: {
type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id'
},
userId: {
type: 'string',
format: 'misskey:id',
},
name: { type: 'string' },
on: {
type: 'array',
items: {
type: 'string',
enum: webhookEventTypes,
}
},
url: { type: 'string' },
secret: { type: 'string' },
active: { type: 'boolean' },
latestSentAt: { type: 'string', format: 'date-time', nullable: true },
latestStatus: { type: 'integer', nullable: true },
},
}
}
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -33,7 +64,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: me.id, userId: me.id,
}); });
return webhooks; return webhooks.map(webhook => (
{
id: webhook.id,
userId: webhook.userId,
name: webhook.name,
on: webhook.on,
url: webhook.url,
secret: webhook.secret,
active: webhook.active,
latestSentAt: webhook.latestSentAt?.toISOString(),
latestStatus: webhook.latestStatus,
}
));
}); });
} }
} }

View file

@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { webhookEventTypes } from '@/models/Webhook.js';
import type { WebhooksRepository } from '@/models/_.js'; import type { WebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
@ -23,6 +24,33 @@ export const meta = {
id: '50f614d9-3047-4f7e-90d8-ad6b2d5fb098', id: '50f614d9-3047-4f7e-90d8-ad6b2d5fb098',
}, },
}, },
res: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
userId: {
type: 'string',
format: 'misskey:id',
},
name: { type: 'string' },
on: {
type: 'array',
items: {
type: 'string',
enum: webhookEventTypes,
},
},
url: { type: 'string' },
secret: { type: 'string' },
active: { type: 'boolean' },
latestSentAt: { type: 'string', format: 'date-time', nullable: true },
latestStatus: { type: 'integer', nullable: true },
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -49,7 +77,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchWebhook); throw new ApiError(meta.errors.noSuchWebhook);
} }
return webhook; return {
id: webhook.id,
userId: webhook.userId,
name: webhook.name,
on: webhook.on,
url: webhook.url,
secret: webhook.secret,
active: webhook.active,
latestSentAt: webhook.latestSentAt?.toISOString(),
latestStatus: webhook.latestStatus,
};
}); });
} }
} }

View file

@ -24,6 +24,25 @@ export const meta = {
id: '30aaaee3-4792-48dc-ab0d-cf501a575ac5', id: '30aaaee3-4792-48dc-ab0d-cf501a575ac5',
}, },
}, },
res: {
type: 'array',
items: {
type: 'object',
nullable: false,
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
user: {
type: 'object',
ref: 'User',
},
},
required: ['id', 'user'],
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -15,6 +15,53 @@ export const meta = {
cacheSec: 60 * 1, cacheSec: 60 * 1,
tags: ['meta'], tags: ['meta'],
res: {
type: 'object',
optional: false, nullable: false,
properties: {
machine: {
type: 'string',
nullable: false,
},
cpu: {
type: 'object',
nullable: false,
properties: {
model: {
type: 'string',
nullable: false,
},
cores: {
type: 'number',
nullable: false,
},
},
},
mem: {
type: 'object',
properties: {
total: {
type: 'number',
nullable: false,
},
},
},
fs: {
type: 'object',
nullable: false,
properties: {
total: {
type: 'number',
nullable: false,
},
used: {
type: 'number',
nullable: false,
},
},
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -12,6 +12,30 @@ export const meta = {
description: 'Endpoint for testing input validation.', description: 'Endpoint for testing input validation.',
requireCredential: false, requireCredential: false,
res: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
required: {
type: 'boolean',
},
string: {
type: 'string',
},
default: {
type: 'string',
},
nullableDefault: {
type: 'string',
default: 'hello',
nullable: true,
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -10,6 +10,21 @@ import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
res: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
unlockedAt: {
type: 'number',
},
},
},
}
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -25,6 +25,35 @@ export const meta = {
id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686', id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686',
}, },
}, },
res: {
type: 'array',
items: {
type: 'object',
nullable: false,
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
createdAt: {
type: 'string',
format: 'date-time',
},
userId: {
type: 'string',
format: 'misskey:id',
},
user: {
type: 'object',
ref: 'User',
},
withReplies: {
type: 'boolean',
},
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -14,12 +14,28 @@
* pollEnded - * pollEnded -
* receiveFollowRequest - * receiveFollowRequest -
* followRequestAccepted - * followRequestAccepted -
* roleAssigned -
* groupInvited - * groupInvited -
* achievementEarned - * achievementEarned -
* app - * app -
* test - * test -
*/ */
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app', 'test'] as const; export const notificationTypes = [
'note',
'follow',
'mention',
'reply',
'renote',
'quote',
'reaction',
'pollEnded',
'receiveFollowRequest',
'followRequestAccepted',
'roleAssigned',
'groupInvited',
'achievementEarned',
'app',
'test'] as const;
export const obsoleteNotificationTypes = ['pollVote'/*, 'groupInvited'*/] as const; export const obsoleteNotificationTypes = ['pollVote'/*, 'groupInvited'*/] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;

View file

@ -7,7 +7,7 @@ services:
- "127.0.0.1:56312:6379" - "127.0.0.1:56312:6379"
dbtest: dbtest:
image: postgres:13 image: postgres:15
ports: ports:
- "127.0.0.1:54312:5432" - "127.0.0.1:54312:5432"
environment: environment:

View file

@ -19,6 +19,7 @@ import { CacheService } from '@/core/CacheService.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 { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
import { NotificationService } from '@/core/NotificationService.js';
import { sleep } from '../utils.js'; import { sleep } from '../utils.js';
import type { TestingModule } from '@nestjs/testing'; import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock'; import type { MockFunctionMetadata } from 'jest-mock';
@ -32,6 +33,7 @@ describe('RoleService', () => {
let rolesRepository: RolesRepository; let rolesRepository: RolesRepository;
let roleAssignmentsRepository: RoleAssignmentsRepository; let roleAssignmentsRepository: RoleAssignmentsRepository;
let metaService: jest.Mocked<MetaService>; let metaService: jest.Mocked<MetaService>;
let notificationService: jest.Mocked<NotificationService>;
let clock: lolex.InstalledClock; let clock: lolex.InstalledClock;
function createUser(data: Partial<MiUser> = {}) { function createUser(data: Partial<MiUser> = {}) {
@ -76,6 +78,8 @@ describe('RoleService', () => {
.useMocker((token) => { .useMocker((token) => {
if (token === MetaService) { if (token === MetaService) {
return { fetch: jest.fn() }; return { fetch: jest.fn() };
} else if (token === NotificationService) {
return { createNotification: jest.fn() };
} }
if (typeof token === 'function') { if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>; const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
@ -93,6 +97,7 @@ describe('RoleService', () => {
roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository); roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>; metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>;
notificationService = app.get<NotificationService>(NotificationService) as jest.Mocked<NotificationService>;
}); });
afterEach(async () => { afterEach(async () => {
@ -273,4 +278,53 @@ describe('RoleService', () => {
expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true); expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
}); });
}); });
describe('assign', () => {
test('公開ロールの場合は通知される', async () => {
const user = await createUser();
const role = await createRole({
isPublic: true,
});
await roleService.assign(user.id, role.id);
await sleep(100);
const assignments = await roleAssignmentsRepository.find({
where: {
userId: user.id,
roleId: role.id,
},
});
expect(assignments).toHaveLength(1);
expect(notificationService.createNotification).toHaveBeenCalled();
expect(notificationService.createNotification.mock.lastCall![0]).toBe(user.id);
expect(notificationService.createNotification.mock.lastCall![1]).toBe('roleAssigned');
expect(notificationService.createNotification.mock.lastCall![2]).toBe({
roleId: role.id,
});
});
test('非公開ロールの場合は通知されない', async () => {
const user = await createUser();
const role = await createRole({
isPublic: false,
});
await roleService.assign(user.id, role.id);
await sleep(100);
const assignments = await roleAssignmentsRepository.find({
where: {
userId: user.id,
roleId: role.id,
},
});
expect(assignments).toHaveLength(1);
expect(notificationService.createNotification).not.toHaveBeenCalled();
});
});
}); });

View file

@ -2486,7 +2486,7 @@ type Notification_2 = components['schemas']['Notification'];
type NotificationsCreateRequest = operations['notifications/create']['requestBody']['content']['application/json']; type NotificationsCreateRequest = operations['notifications/create']['requestBody']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "achievementEarned"]; export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned"];
// @public (undocumented) // @public (undocumented)
type Page = components['schemas']['Page']; type Page = components['schemas']['Page'];

View file

@ -1,4 +1,4 @@
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'achievementEarned'] as const; export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;

View file

@ -3,16 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue'; import { computed, watch, version as vueVersion, App } from 'vue';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import widgets from '@/widgets/index.js'; import widgets from '@/widgets/index.js';
import directives from '@/directives/index.js'; import directives from '@/directives/index.js';
import components from '@/components/index.js'; import components from '@/components/index.js';
import { version, basedMisskeyVersion, ui, lang, updateLocale, locale } from '@/config.js'; import { version, basedMisskeyVersion, lang, updateLocale, locale } from '@/config.js';
import { applyTheme } from '@/scripts/theme.js'; import { applyTheme } from '@/scripts/theme.js';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
import { i18n, updateI18n } from '@/i18n.js'; import { updateI18n } from '@/i18n.js';
import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js'; import { $i, refreshAccount, login } from '@/account.js';
import { defaultStore, ColdDeviceStorage } from '@/store.js'; import { defaultStore, ColdDeviceStorage } from '@/store.js';
import { fetchInstance, instance } from '@/instance.js'; import { fetchInstance, instance } from '@/instance.js';
import { deviceKind } from '@/scripts/device-kind.js'; import { deviceKind } from '@/scripts/device-kind.js';

View file

@ -3,14 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; import { createApp, markRaw, defineAsyncComponent } from 'vue';
import { common } from './common.js'; import { common } from './common.js';
import { version, ui, lang, updateLocale } from '@/config.js'; import { ui } from '@/config.js';
import { i18n, updateI18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { confirm, alert, post, popup, welcomeToast } from '@/os.js'; import { confirm, alert, post, popup, welcomeToast } from '@/os.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js'; import { $i, updateAccount, signout } from '@/account.js';
import { defaultStore, ColdDeviceStorage } from '@/store.js'; import { defaultStore, ColdDeviceStorage } from '@/store.js';
import { makeHotkey } from '@/scripts/hotkey.js'; import { makeHotkey } from '@/scripts/hotkey.js';
import { reactionPicker } from '@/scripts/reaction-picker.js'; import { reactionPicker } from '@/scripts/reaction-picker.js';
@ -20,7 +20,6 @@ import { mainRouter } from '@/router.js';
import { initializeSw } from '@/scripts/initialize-sw.js'; import { initializeSw } from '@/scripts/initialize-sw.js';
import { deckStore } from '@/ui/deck/deck-store.js'; import { deckStore } from '@/ui/deck/deck-store.js';
import { emojiPicker } from '@/scripts/emoji-picker.js'; import { emojiPicker } from '@/scripts/emoji-picker.js';
import { SnowfallEffect } from '@/scripts/snowfall-effect.js';
import { userName } from '@/filters/user.js'; import { userName } from '@/filters/user.js';
import { vibrate } from '@/scripts/vibrate.js'; import { vibrate } from '@/scripts/vibrate.js';
@ -82,6 +81,7 @@ export async function mainBoot() {
if (defaultStore.state.enableSeasonalScreenEffect) { if (defaultStore.state.enableSeasonalScreenEffect) {
const month = new Date().getMonth() + 1; const month = new Date().getMonth() + 1;
if (month === 12 || month === 1) { if (month === 12 || month === 1) {
const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
new SnowfallEffect().render(); new SnowfallEffect().render();
} }
} }

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; import { createApp, defineAsyncComponent } from 'vue';
import { common } from './common.js'; import { common } from './common.js';
export async function subBoot() { export async function subBoot() {

View file

@ -24,8 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; import { ref, shallowRef, toRefs } from 'vue';
import { i18n } from '@/i18n.js';
const props = defineProps<{ const props = defineProps<{
modelValue: string | null; modelValue: string | null;

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div ref="root"> <div>
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/> <XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
<div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container"> <div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container">
<div <div
@ -27,41 +27,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</template> </template>
<script lang="ts">
/**
* アスペクト比算出のためにHTMLElement.clientWidthを使うが
* 大変重たいのでコンテナ要素とメディアリスト幅のペアをキャッシュする
* タイムラインごとにスクロールコンテナが存在する前提だが
*/
const widthCache = new Map<Element, number>();
/**
* コンテナ要素がリサイズされたらキャッシュを削除する
*/
const ro = new ResizeObserver(entries => {
for (const entry of entries) {
widthCache.delete(entry.target);
}
});
async function getClientWidthWithCache(targetEl: HTMLElement, containerEl: HTMLElement, count = 0) {
if (_DEV_) console.log('getClientWidthWithCache', { targetEl, containerEl, count, cache: widthCache.get(containerEl) });
if (widthCache.has(containerEl)) return widthCache.get(containerEl)!;
const width = targetEl.clientWidth;
if (count <= 10 && width < 64) {
// width64
await new Promise(resolve => setTimeout(resolve, 50));
return getClientWidthWithCache(targetEl, containerEl, count + 1);
}
widthCache.set(containerEl, width);
ro.observe(containerEl);
return width;
}
</script>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, onUnmounted, shallowRef } from 'vue'; import { computed, onMounted, onUnmounted, shallowRef } from 'vue';
import * as Misskey from 'cherrypick-js'; import * as Misskey from 'cherrypick-js';
@ -74,15 +39,12 @@ import XVideo from '@/components/MkMediaVideo.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const'; import { FILE_TYPE_BROWSERSAFE } from '@/const';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { getScrollContainer, getBodyScrollHeight } from '@/scripts/scroll.js';
const props = defineProps<{ const props = defineProps<{
mediaList: Misskey.entities.DriveFile[]; mediaList: Misskey.entities.DriveFile[];
raw?: boolean; raw?: boolean;
}>(); }>();
const root = shallowRef<HTMLDivElement>();
const container = shallowRef<HTMLElement | null | undefined>(undefined);
const gallery = shallowRef<HTMLDivElement>(); const gallery = shallowRef<HTMLDivElement>();
const pswpZIndex = os.claimZIndex('middle'); const pswpZIndex = os.claimZIndex('middle');
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString()); document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
@ -95,12 +57,8 @@ const popstateHandler = (): void => {
} }
}; };
/**
* アスペクト比をmediaListWithOneImageAppearanceに基づいていい感じに調整する
* aspect-ratioではなくheightを使う
*/
async function calcAspectRatio() { async function calcAspectRatio() {
if (!gallery.value || !root.value) return; if (!gallery.value) return;
let img = props.mediaList[0]; let img = props.mediaList[0];
@ -109,43 +67,24 @@ async function calcAspectRatio() {
return; return;
} }
if (!container.value) container.value = getScrollContainer(root.value); const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
const width = container.value ? await getClientWidthWithCache(root.value, container.value) : root.value.clientWidth;
const heightMin = (ratio: number) => {
const imgResizeRatio = width / img.properties.width;
const imgDrawHeight = img.properties.height * imgResizeRatio;
const maxHeight = width * ratio;
const height = Math.min(imgDrawHeight, maxHeight);
if (_DEV_) console.log('Image height calculated:', { width, properties: img.properties, imgResizeRatio, imgDrawHeight, maxHeight, height });
return `${height}px`;
};
switch (defaultStore.state.mediaListWithOneImageAppearance) { switch (defaultStore.state.mediaListWithOneImageAppearance) {
case '16_9': case '16_9':
gallery.value.style.height = heightMin(9 / 16); gallery.value.style.aspectRatio = ratioMax(16 / 9);
break; break;
case '1_1': case '1_1':
gallery.value.style.height = heightMin(1); gallery.value.style.aspectRatio = ratioMax(1 / 1);
break; break;
case '2_3': case '2_3':
gallery.value.style.height = heightMin(3 / 2); gallery.value.style.aspectRatio = ratioMax(2 / 3);
break; break;
default: { default:
const maxHeight = Math.max(64, (container.value ? container.value.clientHeight : getBodyScrollHeight()) * 0.5 || 360); gallery.value.style.aspectRatio = '';
if (width === 0 || !maxHeight) return;
const imgResizeRatio = width / img.properties.width;
const imgDrawHeight = img.properties.height * imgResizeRatio;
gallery.value.style.height = `${Math.max(64, Math.min(imgDrawHeight, maxHeight))}px`;
gallery.value.style.minHeight = 'initial';
gallery.value.style.maxHeight = 'initial';
break; break;
} }
} }
gallery.value.style.aspectRatio = 'initial';
}
onMounted(() => { onMounted(() => {
calcAspectRatio(); calcAspectRatio();

View file

@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts"> <script lang="ts">
import { Ref, computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'; import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
import { focusPrev, focusNext } from '@/scripts/focus.js'; import { focusPrev, focusNext } from '@/scripts/focus.js';
import MkSwitchButton from '@/components/MkSwitch.button.vue'; import MkSwitchButton from '@/components/MkSwitch.button.vue';
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu.js'; import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu.js';

View file

@ -198,7 +198,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent, watch, provide } from 'vue'; import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
import * as mfm from 'cherrypick-mfm-js'; import * as mfm from 'cherrypick-mfm-js';
import * as Misskey from 'cherrypick-js'; import * as Misskey from 'cherrypick-js';
import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSub from '@/components/MkNoteSub.vue';

View file

@ -303,11 +303,10 @@ import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { deepClone } from '@/scripts/clone.js'; import { deepClone } from '@/scripts/clone.js';
import { useTooltip } from '@/scripts/use-tooltip.js'; import { useTooltip } from '@/scripts/use-tooltip.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';

View file

@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.head"> <div :class="$style.head">
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="notification.type === 'roleAssigned'" :class="$style.icon" :user="$i" link preview/>
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/> <MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
@ -38,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> <i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i> <i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
<img v-else-if="notification.type === 'roleAssigned'" :src="notification.role.iconUrl" alt=""/>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> <!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<MkReactionIcon <MkReactionIcon
v-else-if="notification.type === 'reaction'" v-else-if="notification.type === 'reaction'"
@ -52,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<header :class="$style.header"> <header :class="$style.header">
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span> <span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span> <span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> <MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
@ -88,6 +91,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
<i class="ti ti-quote" :class="$style.quote"></i> <i class="ti ti-quote" :class="$style.quote"></i>
</MkA> </MkA>
<div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
{{ notification.role.name }}
</div>
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements"> <MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
{{ i18n.ts._achievements._types['_' + notification.achievement].title }} {{ i18n.ts._achievements._types['_' + notification.achievement].title }}
</MkA> </MkA>
@ -139,7 +145,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, shallowRef } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'cherrypick-js'; import * as Misskey from 'cherrypick-js';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkFollowButton from '@/components/MkFollowButton.vue'; import MkFollowButton from '@/components/MkFollowButton.vue';

View file

@ -24,13 +24,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated, watch } from 'vue'; import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import XNotification from '@/components/MkNotification.vue'; import XNotification from '@/components/MkNotification.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkNote from '@/components/MkNote.vue'; import MkNote from '@/components/MkNote.vue';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { notificationTypes } from '@/const.js'; import { notificationTypes } from '@/const.js';
import { infoImageUrl } from '@/instance.js'; import { infoImageUrl } from '@/instance.js';

View file

@ -23,8 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, watch, ref, shallowRef } from 'vue'; import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
import { deviceKind } from '@/scripts/device-kind.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { getScrollContainer } from '@/scripts/scroll.js'; import { getScrollContainer } from '@/scripts/scroll.js';
import { vibrate } from '@/scripts/vibrate.js'; import { vibrate } from '@/scripts/vibrate.js';

View file

@ -83,7 +83,6 @@ import { ref, computed } from 'vue';
import { toUnicode } from 'punycode/'; import { toUnicode } from 'punycode/';
import MkButton from './MkButton.vue'; import MkButton from './MkButton.vue';
import MkInput from './MkInput.vue'; import MkInput from './MkInput.vue';
import MkSwitch from './MkSwitch.vue';
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue'; import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
import * as config from '@/config.js'; import * as config from '@/config.js';
import * as os from '@/os.js'; import * as os from '@/os.js';

View file

@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'; import { computed, ref } from 'vue';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';

View file

@ -39,7 +39,6 @@ import XSignup from '@/components/MkSignupDialog.form.vue';
import XServerRules from '@/components/MkSignupDialog.rules.vue'; import XServerRules from '@/components/MkSignupDialog.rules.vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
autoSet?: boolean; autoSet?: boolean;

View file

@ -34,15 +34,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import XUser from '@/components/MkUserSetupDialog.User.vue'; import XUser from '@/components/MkUserSetupDialog.User.vue';
import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { $i } from '@/account.js';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true }; const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };

View file

@ -44,14 +44,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { $i } from '@/account.js';
const isLocked = ref(false); const isLocked = ref(false);
const hideOnlineStatus = ref(false); const hideOnlineStatus = ref(false);

View file

@ -30,8 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';

View file

@ -29,7 +29,6 @@ import * as Misskey from 'cherrypick-js';
import { ref } from 'vue'; import { ref } from 'vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
const props = defineProps<{ const props = defineProps<{

View file

@ -54,7 +54,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'cherrypick-js'; import * as Misskey from 'cherrypick-js';
import XTimeline from './welcome.timeline.vue';
import XSigninDialog from '@/components/MkSigninDialog.vue'; import XSigninDialog from '@/components/MkSigninDialog.vue';
import XSignupDialog from '@/components/MkSignupDialog.vue'; import XSignupDialog from '@/components/MkSignupDialog.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -65,7 +64,6 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { mainRouter } from '@/router.js'; import { mainRouter } from '@/router.js';
import number from '@/filters/number.js';
import MkNumber from '@/components/MkNumber.vue'; import MkNumber from '@/components/MkNumber.vue';
import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue'; import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue';

View file

@ -21,7 +21,6 @@ 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 MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const props = defineProps<{ const props = defineProps<{

View file

@ -14,7 +14,6 @@ import { computed } from 'vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js'; import { url } from '@/config.js';
import { popout as popout_ } from '@/scripts/popout.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js'; import { useRouter } from '@/router.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';

View file

@ -4,11 +4,8 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import MkAd from './MkAd.vue'; import MkAd from './MkAd.vue';
import { i18n } from '@/i18n.js';
let lock: Promise<undefined> | undefined; let lock: Promise<undefined> | undefined;

View file

@ -5,7 +5,6 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest'; import { expect } from '@storybook/jest';
import { userEvent, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import { userDetailed } from '../../../.storybook/fakes'; import { userDetailed } from '../../../.storybook/fakes';
import MkUserName from './MkUserName.vue'; import MkUserName from './MkUserName.vue';

View file

@ -16,7 +16,6 @@ import * as mfm from 'cherrypick-mfm-js';
import * as Misskey from 'cherrypick-js'; import * as Misskey from 'cherrypick-js';
import { TextBlock } from './block.type'; import { TextBlock } from './block.type';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
import { $i } from '@/account.js';
const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue')); const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));

View file

@ -10,7 +10,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, nextTick } from 'vue';
import * as Misskey from 'cherrypick-js'; import * as Misskey from 'cherrypick-js';
import XBlock from './page.block.vue'; import XBlock from './page.block.vue';

View file

@ -54,7 +54,22 @@ https://github.com/sindresorhus/file-type/blob/main/core.js
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
*/ */
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app'] as const; export const notificationTypes = [
'note',
'follow',
'mention',
'reply',
'renote',
'quote',
'reaction',
'pollEnded',
'receiveFollowRequest',
'followRequestAccepted',
'roleAssigned',
'groupInvited',
'achievementEarned',
'app',
] as const;
export const obsoleteNotificationTypes = ['pollVote'/*, 'groupInvited'*/] as const; export const obsoleteNotificationTypes = ['pollVote'/*, 'groupInvited'*/] as const;
export const ROLE_POLICIES = [ export const ROLE_POLICIES = [

View file

@ -218,12 +218,12 @@ import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { url } from '@/config.js'; import { url } from '@/config.js';
import { userPage, acct } from '@/filters/user.js'; import { acct } from '@/filters/user.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { iAmAdmin, $i } from '@/account.js'; import { iAmAdmin, $i } from '@/account.js';
import MkRolePreview from '@/components/MkRolePreview.vue'; import MkRolePreview from '@/components/MkRolePreview.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import { globalEvents } from '@/events.js'; import { globalEvents } from '@/events.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{

View file

@ -97,11 +97,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import JSON5 from 'json5'; import JSON5 from 'json5';
import XHeader from './_header_.vue'; import XHeader from './_header_.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { instance, fetchInstance } from '@/instance.js'; import { instance, fetchInstance } from '@/instance.js';

View file

@ -64,8 +64,6 @@ import XHeader from './_header_.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue'; import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { fetchInstance } from '@/instance.js'; import { fetchInstance } from '@/instance.js';

View file

@ -123,9 +123,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import * as Misskey from 'cherrypick-js'; import * as Misskey from 'cherrypick-js';
import { CodeDiff } from 'v-code-diff'; import { CodeDiff } from 'v-code-diff';
import JSON5 from 'json5'; import JSON5 from 'json5';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
const props = defineProps<{ const props = defineProps<{

View file

@ -69,7 +69,7 @@ import { useRouter } from '@/router.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import { infoImageUrl } from '@/instance.js'; import { infoImageUrl } from '@/instance.js';
const router = useRouter(); const router = useRouter();

View file

@ -16,8 +16,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, watch } from 'vue';
import * as os from '@/os.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';

View file

@ -46,9 +46,6 @@ import { ref, computed } from 'vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkInfo from '@/components/MkInfo.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';

View file

@ -86,7 +86,7 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { customEmojiCategories } from '@/custom-emojis.js'; import { customEmojiCategories } from '@/custom-emojis.js';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import { selectFile, selectFiles } from '@/scripts/select-file.js'; import { selectFile } from '@/scripts/select-file.js';
import MkRolePreview from '@/components/MkRolePreview.vue'; import MkRolePreview from '@/components/MkRolePreview.vue';
const props = defineProps<{ const props = defineProps<{

View file

@ -68,7 +68,7 @@ import MkInput from '@/components/MkInput.vue';
import { userListsCache } from '@/cache.js'; import { userListsCache } from '@/cache.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
const { const {
enableInfiniteScroll, enableInfiniteScroll,

View file

@ -29,7 +29,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
expanded?: boolean; expanded?: boolean;

View file

@ -47,18 +47,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'; import { ref } from 'vue';
import MkNotes from '@/components/MkNotes.vue'; import MkNotes from '@/components/MkNotes.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue'; import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { $i } from '@/account.js';
import { instance } from '@/instance.js';
import MkInfo from '@/components/MkInfo.vue';
import { useRouter } from '@/router.js'; import { useRouter } from '@/router.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';

View file

@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineAsyncComponent, onMounted, ref } from 'vue'; import { ref } from 'vue';
import MkUserList from '@/components/MkUserList.vue'; import MkUserList from '@/components/MkUserList.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue'; import MkRadios from '@/components/MkRadios.vue';
@ -33,9 +33,6 @@ import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { $i } from '@/account.js';
import { instance } from '@/instance.js';
import MkInfo from '@/components/MkInfo.vue';
import { useRouter } from '@/router.js'; import { useRouter } from '@/router.js';
const router = useRouter(); const router = useRouter();

Some files were not shown because too many files have changed in this diff Show more