diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-06-13 16:46:01 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-06-13 16:46:01 +0900 |
| commit | 7093662ce5c17a8096c33712d0056de9fd4b5a41 (patch) | |
| tree | a89436b3c3b474a1b1c985876d9e769ea1ac9f9e /packages | |
| parent | Merge pull request #10961 from misskey-dev/develop (diff) | |
| parent | ユーザー統計表示機能を削除 (diff) | |
| download | misskey-7093662ce5c17a8096c33712d0056de9fd4b5a41.tar.gz misskey-7093662ce5c17a8096c33712d0056de9fd4b5a41.tar.bz2 misskey-7093662ce5c17a8096c33712d0056de9fd4b5a41.zip | |
Merge pull request #10990 from misskey-dev/develop
Release: 13.13.2
Diffstat (limited to 'packages')
56 files changed, 468 insertions, 534 deletions
diff --git a/packages/backend/migration/1685973839966-errorImageUrl.js b/packages/backend/migration/1685973839966-errorImageUrl.js new file mode 100644 index 0000000000..fd5d467162 --- /dev/null +++ b/packages/backend/migration/1685973839966-errorImageUrl.js @@ -0,0 +1,17 @@ +export class ErrorImageUrl1685973839966 { + name = 'ErrorImageUrl1685973839966' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "errorImageUrl"`); + await queryRunner.query(`ALTER TABLE "meta" ADD "serverErrorImageUrl" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "notFoundImageUrl" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "infoImageUrl" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "infoImageUrl"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "notFoundImageUrl"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "serverErrorImageUrl"`); + await queryRunner.query(`ALTER TABLE "meta" ADD "errorImageUrl" character varying(1024) DEFAULT 'https://xn--931a.moe/aiart/yubitun.png'`); + } +} diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index de33e4c243..2b7f9a48da 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -168,6 +168,17 @@ export class CacheService implements OnApplicationShutdown { @bindThis public dispose(): void { this.redisForSub.off('message', this.onMessage); + this.userByIdCache.dispose(); + this.localUserByNativeTokenCache.dispose(); + this.localUserByIdCache.dispose(); + this.uriPersonCache.dispose(); + this.userProfileCache.dispose(); + this.userMutingsCache.dispose(); + this.userBlockingCache.dispose(); + this.userBlockedCache.dispose(); + this.renoteMutingsCache.dispose(); + this.userFollowingsCache.dispose(); + this.userFollowingChannelsCache.dispose(); } @bindThis diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 3499df38b7..5f2ced77eb 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { DataSource, In, IsNull } from 'typeorm'; import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; @@ -18,7 +18,7 @@ import type { Serialized } from '@/server/api/stream/types.js'; const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; @Injectable() -export class CustomEmojiService { +export class CustomEmojiService implements OnApplicationShutdown { private cache: MemoryKVCache<Emoji | null>; public localEmojisCache: RedisSingleCache<Map<string, Emoji>>; @@ -349,4 +349,14 @@ export class CustomEmojiService { this.cache.set(`${emoji.name} ${emoji.host}`, emoji); } } + + @bindThis + public dispose(): void { + this.cache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts index 8b9a87a380..3603d59dcc 100644 --- a/packages/backend/src/core/FederatedInstanceService.ts +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; import type { InstancesRepository } from '@/models/index.js'; import type { Instance } from '@/models/entities/Instance.js'; @@ -9,7 +9,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; @Injectable() -export class FederatedInstanceService { +export class FederatedInstanceService implements OnApplicationShutdown { public federatedInstanceCache: RedisKVCache<Instance | null>; constructor( @@ -77,4 +77,14 @@ export class FederatedInstanceService { this.federatedInstanceCache.set(result.host, result); } + + @bindThis + public dispose(): void { + this.federatedInstanceCache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index a4c569bdec..15a1d74878 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import push from 'web-push'; import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; @@ -42,7 +42,7 @@ function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: Pus } @Injectable() -export class PushNotificationService { +export class PushNotificationService implements OnApplicationShutdown { private subscriptionsCache: RedisKVCache<SwSubscription[]>; constructor( @@ -115,4 +115,14 @@ export class PushNotificationService { }); } } + + @bindThis + public dispose(): void { + this.subscriptionsCache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 40ae106662..79922d0a87 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -435,6 +435,7 @@ export class RoleService implements OnApplicationShutdown { @bindThis public dispose(): void { this.redisForSub.off('message', this.onMessage); + this.roleAssignmentByUserIdCache.dispose(); } @bindThis diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 72c35c529c..d768f08650 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; import type { User } from '@/models/entities/User.js'; import type { UserKeypairsRepository } from '@/models/index.js'; @@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @Injectable() -export class UserKeypairService { +export class UserKeypairService implements OnApplicationShutdown { private cache: RedisKVCache<UserKeypair>; constructor( @@ -31,4 +31,14 @@ export class UserKeypairService { public async getUserKeypair(userId: User['id']): Promise<UserKeypair> { return await this.cache.fetch(userId); } + + @bindThis + public dispose(): void { + this.cache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 2b404ebeca..2d9e7a14ee 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import escapeRegexp from 'escape-regexp'; import { DI } from '@/di-symbols.js'; import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; @@ -30,7 +30,7 @@ export type UriParseResult = { }; @Injectable() -export class ApDbResolverService { +export class ApDbResolverService implements OnApplicationShutdown { private publicKeyCache: MemoryKVCache<UserPublickey | null>; private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>; @@ -162,4 +162,15 @@ export class ApDbResolverService { key, }; } + + @bindThis + public dispose(): void { + this.publicKeyCache.dispose(); + this.publicKeyByUserIdCache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 5610929648..f130a7db8b 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -83,6 +83,16 @@ export class RedisKVCache<T> { // TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする } + + @bindThis + public gc() { + this.memoryCache.gc(); + } + + @bindThis + public dispose() { + this.memoryCache.dispose(); + } } export class RedisSingleCache<T> { @@ -174,10 +184,15 @@ export class RedisSingleCache<T> { export class MemoryKVCache<T> { public cache: Map<string, { date: number; value: T; }>; private lifetime: number; + private gcIntervalHandle: NodeJS.Timer; constructor(lifetime: MemoryKVCache<never>['lifetime']) { this.cache = new Map(); this.lifetime = lifetime; + + this.gcIntervalHandle = setInterval(() => { + this.gc(); + }, 1000 * 60 * 3); } @bindThis @@ -200,7 +215,7 @@ export class MemoryKVCache<T> { } @bindThis - public delete(key: string) { + public delete(key: string): void { this.cache.delete(key); } @@ -255,6 +270,21 @@ export class MemoryKVCache<T> { } return value; } + + @bindThis + public gc(): void { + const now = Date.now(); + for (const [key, { date }] of this.cache.entries()) { + if ((now - date) > this.lifetime) { + this.cache.delete(key); + } + } + } + + @bindThis + public dispose(): void { + clearInterval(this.gcIntervalHandle); + } } export class MemorySingleCache<T> { diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts index 6d44e4edc7..f799551f30 100644 --- a/packages/backend/src/models/entities/Meta.ts +++ b/packages/backend/src/models/entities/Meta.ts @@ -101,13 +101,25 @@ export class Meta { length: 1024, nullable: true, }) - public errorImageUrl: string | null; + public iconUrl: string | null; @Column('varchar', { length: 1024, nullable: true, }) - public iconUrl: string | null; + public serverErrorImageUrl: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public notFoundImageUrl: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public infoImageUrl: string | null; @Column('boolean', { default: true, diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index e23591d876..4ad0197d87 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js'; import type { LocalUser } from '@/models/entities/User.js'; @@ -17,7 +17,7 @@ export class AuthenticationError extends Error { } @Injectable() -export class AuthenticateService { +export class AuthenticateService implements OnApplicationShutdown { private appCache: MemoryKVCache<App>; constructor( @@ -85,4 +85,14 @@ export class AuthenticateService { } } } + + @bindThis + public dispose(): void { + this.appCache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 1e32e9988d..d1ff3fe925 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -333,7 +333,6 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js'; import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js'; import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; -import * as ep___users_stats from './endpoints/users/stats.js'; import * as ep___users_achievements from './endpoints/users/achievements.js'; import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; @@ -674,7 +673,6 @@ const $users_reportAbuse: Provider = { provide: 'ep:users/report-abuse', useClas const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-username-and-host', useClass: ep___users_searchByUsernameAndHost.default }; const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default }; const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default }; -const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default }; const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default }; const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default }; const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; @@ -1019,7 +1017,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_searchByUsernameAndHost, $users_search, $users_show, - $users_stats, $users_achievements, $users_updateMemo, $fetchRss, @@ -1356,7 +1353,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_searchByUsernameAndHost, $users_search, $users_show, - $users_stats, $users_achievements, $users_updateMemo, $fetchRss, diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index 893dfe956e..d1394d6d76 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -128,26 +128,27 @@ export class StreamingApiServerService { ev.removeAllListeners(); stream.dispose(); this.redisForSub.off('message', onRedisMessage); + this.#connections.delete(connection); if (userUpdateIntervalId) clearInterval(userUpdateIntervalId); }); - connection.on('message', async (data) => { + connection.on('pong', () => { this.#connections.set(connection, Date.now()); - if (data.toString() === 'ping') { - connection.send('pong'); - } }); }); + // 一定期間通信が無いコネクションは実際には切断されている可能性があるため定期的にterminateする this.#cleanConnectionsIntervalId = setInterval(() => { const now = Date.now(); for (const [connection, lastActive] of this.#connections.entries()) { - if (now - lastActive > 1000 * 60 * 5) { + if (now - lastActive > 1000 * 60 * 2) { connection.terminate(); this.#connections.delete(connection); + } else { + connection.ping(); } } - }, 1000 * 60 * 5); + }, 1000 * 60); } @bindThis diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 7e678a6404..94206ef870 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -333,7 +333,6 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js'; import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js'; import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; -import * as ep___users_stats from './endpoints/users/stats.js'; import * as ep___users_achievements from './endpoints/users/achievements.js'; import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; @@ -672,7 +671,6 @@ const eps = [ ['users/search-by-username-and-host', ep___users_searchByUsernameAndHost], ['users/search', ep___users_search], ['users/show', ep___users_show], - ['users/stats', ep___users_stats], ['users/achievements', ep___users_achievements], ['users/update-memo', ep___users_updateMemo], ['fetch-rss', ep___fetchRss], diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 87a2d22ac2..4cc1b6011f 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -61,10 +61,17 @@ export const meta = { type: 'string', optional: false, nullable: true, }, - errorImageUrl: { + serverErrorImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + infoImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + notFoundImageUrl: { type: 'string', optional: false, nullable: true, - default: 'https://xn--931a.moe/aiart/yubitun.png', }, iconUrl: { type: 'string', @@ -305,7 +312,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl, bannerUrl: instance.bannerUrl, - errorImageUrl: instance.errorImageUrl, + serverErrorImageUrl: instance.serverErrorImageUrl, + notFoundImageUrl: instance.notFoundImageUrl, + infoImageUrl: instance.infoImageUrl, iconUrl: instance.iconUrl, backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 0e94f56cfd..1de5e9efd3 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -32,7 +32,9 @@ export const paramDef = { themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, mascotImageUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true }, - errorImageUrl: { type: 'string', nullable: true }, + serverErrorImageUrl: { type: 'string', nullable: true }, + infoImageUrl: { type: 'string', nullable: true }, + notFoundImageUrl: { type: 'string', nullable: true }, iconUrl: { type: 'string', nullable: true }, backgroundImageUrl: { type: 'string', nullable: true }, logoImageUrl: { type: 'string', nullable: true }, @@ -149,6 +151,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { set.iconUrl = ps.iconUrl; } + if (ps.serverErrorImageUrl !== undefined) { + set.serverErrorImageUrl = ps.serverErrorImageUrl; + } + + if (ps.infoImageUrl !== undefined) { + set.infoImageUrl = ps.infoImageUrl; + } + + if (ps.notFoundImageUrl !== undefined) { + set.notFoundImageUrl = ps.notFoundImageUrl; + } + if (ps.backgroundImageUrl !== undefined) { set.backgroundImageUrl = ps.backgroundImageUrl; } @@ -281,10 +295,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { set.smtpPass = ps.smtpPass; } - if (ps.errorImageUrl !== undefined) { - set.errorImageUrl = ps.errorImageUrl; - } - if (ps.enableServiceWorker !== undefined) { set.enableServiceWorker = ps.enableServiceWorker; } diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index fe68467a64..3b3c5caa00 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -124,10 +124,17 @@ export const meta = { type: 'string', optional: false, nullable: false, }, - errorImageUrl: { + serverErrorImageUrl: { type: 'string', - optional: false, nullable: false, - default: 'https://xn--931a.moe/aiart/yubitun.png', + optional: false, nullable: true, + }, + infoImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + notFoundImageUrl: { + type: 'string', + optional: false, nullable: true, }, iconUrl: { type: 'string', @@ -288,7 +295,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl, bannerUrl: instance.bannerUrl, - errorImageUrl: instance.errorImageUrl, + infoImageUrl: instance.infoImageUrl, + serverErrorImageUrl: instance.serverErrorImageUrl, + notFoundImageUrl: instance.notFoundImageUrl, iconUrl: instance.iconUrl, backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, diff --git a/packages/backend/src/server/api/endpoints/roles/list.ts b/packages/backend/src/server/api/endpoints/roles/list.ts index d61c6b8dc6..5ad29839c2 100644 --- a/packages/backend/src/server/api/endpoints/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/roles/list.ts @@ -30,6 +30,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { super(meta, paramDef, async (ps, me) => { const roles = await this.rolesRepository.findBy({ isPublic: true, + isExplorable: true, }); return await this.roleEntityService.packMany(roles, me); }); diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index 607dc24206..b2cb8b42a8 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -49,6 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { const role = await this.rolesRepository.findOneBy({ id: ps.roleId, isPublic: true, + isExplorable: true, }); if (role == null) { diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts deleted file mode 100644 index 7479793afe..0000000000 --- a/packages/backend/src/server/api/endpoints/users/stats.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import { DI } from '@/di-symbols.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, DriveFilesRepository, NoteReactionsRepository, PageLikesRepository, NoteFavoritesRepository, PollVotesRepository } from '@/models/index.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['users'], - - requireCredential: false, - - description: 'Show statistics about a user.', - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '9e638e45-3b25-4ef7-8f95-07e8498f1819', - }, - }, - - res: { - type: 'object', - optional: false, nullable: false, - properties: { - notesCount: { - type: 'integer', - optional: false, nullable: false, - }, - repliesCount: { - type: 'integer', - optional: false, nullable: false, - }, - renotesCount: { - type: 'integer', - optional: false, nullable: false, - }, - repliedCount: { - type: 'integer', - optional: false, nullable: false, - }, - renotedCount: { - type: 'integer', - optional: false, nullable: false, - }, - pollVotesCount: { - type: 'integer', - optional: false, nullable: false, - }, - pollVotedCount: { - type: 'integer', - optional: false, nullable: false, - }, - localFollowingCount: { - type: 'integer', - optional: false, nullable: false, - }, - remoteFollowingCount: { - type: 'integer', - optional: false, nullable: false, - }, - localFollowersCount: { - type: 'integer', - optional: false, nullable: false, - }, - remoteFollowersCount: { - type: 'integer', - optional: false, nullable: false, - }, - followingCount: { - type: 'integer', - optional: false, nullable: false, - }, - followersCount: { - type: 'integer', - optional: false, nullable: false, - }, - sentReactionsCount: { - type: 'integer', - optional: false, nullable: false, - }, - receivedReactionsCount: { - type: 'integer', - optional: false, nullable: false, - }, - noteFavoritesCount: { - type: 'integer', - optional: false, nullable: false, - }, - pageLikesCount: { - type: 'integer', - optional: false, nullable: false, - }, - pageLikedCount: { - type: 'integer', - optional: false, nullable: false, - }, - driveFilesCount: { - type: 'integer', - optional: false, nullable: false, - }, - driveUsage: { - type: 'integer', - optional: false, nullable: false, - description: 'Drive usage in bytes', - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint<typeof meta, typeof paramDef> { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - - @Inject(DI.noteReactionsRepository) - private noteReactionsRepository: NoteReactionsRepository, - - @Inject(DI.pageLikesRepository) - private pageLikesRepository: PageLikesRepository, - - @Inject(DI.noteFavoritesRepository) - private noteFavoritesRepository: NoteFavoritesRepository, - - @Inject(DI.pollVotesRepository) - private pollVotesRepository: PollVotesRepository, - - private driveFileEntityService: DriveFileEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - if (user == null) { - throw new ApiError(meta.errors.noSuchUser); - } - - const result = await awaitAll({ - notesCount: this.notesRepository.createQueryBuilder('note') - .where('note.userId = :userId', { userId: user.id }) - .getCount(), - repliesCount: this.notesRepository.createQueryBuilder('note') - .where('note.userId = :userId', { userId: user.id }) - .andWhere('note.replyId IS NOT NULL') - .getCount(), - renotesCount: this.notesRepository.createQueryBuilder('note') - .where('note.userId = :userId', { userId: user.id }) - .andWhere('note.renoteId IS NOT NULL') - .getCount(), - repliedCount: this.notesRepository.createQueryBuilder('note') - .where('note.replyUserId = :userId', { userId: user.id }) - .getCount(), - renotedCount: this.notesRepository.createQueryBuilder('note') - .where('note.renoteUserId = :userId', { userId: user.id }) - .getCount(), - pollVotesCount: this.pollVotesRepository.createQueryBuilder('vote') - .where('vote.userId = :userId', { userId: user.id }) - .getCount(), - pollVotedCount: this.pollVotesRepository.createQueryBuilder('vote') - .innerJoin('vote.note', 'note') - .where('note.userId = :userId', { userId: user.id }) - .getCount(), - localFollowingCount: this.followingsRepository.createQueryBuilder('following') - .where('following.followerId = :userId', { userId: user.id }) - .andWhere('following.followeeHost IS NULL') - .getCount(), - remoteFollowingCount: this.followingsRepository.createQueryBuilder('following') - .where('following.followerId = :userId', { userId: user.id }) - .andWhere('following.followeeHost IS NOT NULL') - .getCount(), - localFollowersCount: this.followingsRepository.createQueryBuilder('following') - .where('following.followeeId = :userId', { userId: user.id }) - .andWhere('following.followerHost IS NULL') - .getCount(), - remoteFollowersCount: this.followingsRepository.createQueryBuilder('following') - .where('following.followeeId = :userId', { userId: user.id }) - .andWhere('following.followerHost IS NOT NULL') - .getCount(), - sentReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction') - .where('reaction.userId = :userId', { userId: user.id }) - .getCount(), - receivedReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction') - .innerJoin('reaction.note', 'note') - .where('note.userId = :userId', { userId: user.id }) - .getCount(), - noteFavoritesCount: this.noteFavoritesRepository.createQueryBuilder('favorite') - .where('favorite.userId = :userId', { userId: user.id }) - .getCount(), - pageLikesCount: this.pageLikesRepository.createQueryBuilder('like') - .where('like.userId = :userId', { userId: user.id }) - .getCount(), - pageLikedCount: this.pageLikesRepository.createQueryBuilder('like') - .innerJoin('like.page', 'page') - .where('page.userId = :userId', { userId: user.id }) - .getCount(), - driveFilesCount: this.driveFilesRepository.createQueryBuilder('file') - .where('file.userId = :userId', { userId: user.id }) - .getCount(), - driveUsage: this.driveFileEntityService.calcDriveUsageOf(user), - }); - - return { - ...result, - followingCount: result.localFollowingCount + result.remoteFollowingCount, - followersCount: result.localFollowersCount + result.remoteFollowersCount, - }; - }); - } -} diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index f780280c1f..07ba2731c3 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -26,7 +26,7 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; -import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, Meta, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type Logger from '@/logger.js'; import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; @@ -118,6 +118,18 @@ export class ClientServerService { } @bindThis + private generateCommonPugData(meta: Meta) { + return { + instanceName: meta.name ?? 'Misskey', + icon: meta.iconUrl, + themeColor: meta.themeColor, + serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg', + infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg', + notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg', + }; + } + + @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.register(fastifyCookie, {}); @@ -341,12 +353,10 @@ export class ClientServerService { reply.header('Cache-Control', 'public, max-age=30'); return await reply.view('base', { img: meta.bannerUrl, - title: meta.name ?? 'Misskey', - instanceName: meta.name ?? 'Misskey', url: this.config.url, + title: meta.name ?? 'Misskey', desc: meta.description, - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); }; @@ -431,9 +441,7 @@ export class ClientServerService { user, profile, me, avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), sub: request.params.sub, - instanceName: meta.name ?? 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); } else { // リモートユーザーなので @@ -481,9 +489,7 @@ export class ClientServerService { avatarUrl: _note.user.avatarUrl, // TODO: Let locale changeable by instance setting summary: getNoteSummary(_note), - instanceName: meta.name ?? 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -522,9 +528,7 @@ export class ClientServerService { page: _page, profile, avatarUrl: _page.user.avatarUrl, - instanceName: meta.name ?? 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -550,9 +554,7 @@ export class ClientServerService { flash: _flash, profile, avatarUrl: _flash.user.avatarUrl, - instanceName: meta.name ?? 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -578,9 +580,7 @@ export class ClientServerService { clip: _clip, profile, avatarUrl: _clip.user.avatarUrl, - instanceName: meta.name ?? 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -604,9 +604,7 @@ export class ClientServerService { post: _post, profile, avatarUrl: _post.user.avatarUrl, - instanceName: meta.name ?? 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -625,9 +623,7 @@ export class ClientServerService { reply.header('Cache-Control', 'public, max-age=15'); return await reply.view('channel', { channel: _channel, - instanceName: meta.name ?? 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 69b3f68e05..1216fc73f7 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -31,9 +31,9 @@ html link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png') link(rel='manifest' href='/manifest.json') link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`) - link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg') - link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') - link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') + link(rel='prefetch' href=serverErrorImageUrl) + link(rel='prefetch' href=infoImageUrl) + link(rel='prefetch' href=notFoundImageUrl) //- https://github.com/misskey-dev/misskey/issues/9842 link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.21.0') link(rel='modulepreload' href=`/vite/${clientEntry.file}`) diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug index 874c48c602..ea0917a80e 100644 --- a/packages/backend/src/server/web/views/note.pug +++ b/packages/backend/src/server/web/views/note.pug @@ -5,8 +5,8 @@ block vars - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`; - const url = `${config.url}/notes/${note.id}`; - const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null; - - const image = (note.files || []).find(file => file.type.startsWith('image/') && !file.type.isSensitive) - - const video = (note.files || []).find(file => file.type.startsWith('video/') && !file.type.isSensitive) + - const image = (note.files || []).find(file => file.type.startsWith('image/') && !file.isSensitive) + - const video = (note.files || []).find(file => file.type.startsWith('video/') && !file.isSensitive) block title = `${title} | ${instanceName}` diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue index 4050520eb9..2d3ea8d177 100644 --- a/packages/frontend/src/components/MkChannelList.vue +++ b/packages/frontend/src/components/MkChannelList.vue @@ -2,7 +2,7 @@ <MkPagination :pagination="pagination"> <template #empty> <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.notFound }}</div> </div> </template> @@ -17,6 +17,7 @@ import MkChannelPreview from '@/components/MkChannelPreview.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue'; import { i18n } from '@/i18n'; +import { infoImageUrl } from '@/instance'; const props = withDefaults(defineProps<{ pagination: Paging; diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue index 9cc2b7a967..b49c8fa8b7 100644 --- a/packages/frontend/src/components/MkNotes.vue +++ b/packages/frontend/src/components/MkNotes.vue @@ -2,7 +2,7 @@ <MkPagination ref="pagingComponent" :pagination="pagination"> <template #empty> <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.noNotes }}</div> </div> </template> @@ -32,6 +32,7 @@ import MkNote from '@/components/MkNote.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue'; import { i18n } from '@/i18n'; +import { infoImageUrl } from '@/instance'; const props = defineProps<{ pagination: Paging; diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 70224bffa1..d4a30d1916 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -2,7 +2,7 @@ <MkPagination ref="pagingComponent" :pagination="pagination"> <template #empty> <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.noNotifications }}</div> </div> </template> @@ -26,6 +26,7 @@ import { useStream } from '@/stream'; import { $i } from '@/account'; import { i18n } from '@/i18n'; import { notificationTypes } from '@/const'; +import { infoImageUrl } from '@/instance'; const props = defineProps<{ includeTypes?: typeof notificationTypes[number][]; diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 740094b113..598529bf58 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -13,7 +13,7 @@ <div v-else-if="empty" key="_empty_" class="empty"> <slot name="empty"> <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.nothing }}</div> </div> </slot> @@ -73,6 +73,8 @@ export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> }; </script> <script lang="ts" setup> +import { infoImageUrl } from '@/instance'; + const props = withDefaults(defineProps<{ pagination: Paging; disableAutoLoad?: boolean; diff --git a/packages/frontend/src/components/MkReactedUsersDialog.vue b/packages/frontend/src/components/MkReactedUsersDialog.vue index cd2a359d5c..0a858a8965 100644 --- a/packages/frontend/src/components/MkReactedUsersDialog.vue +++ b/packages/frontend/src/components/MkReactedUsersDialog.vue @@ -11,7 +11,7 @@ <MkSpacer :marginMin="20" :marginMax="28"> <div v-if="note" class="_gaps"> <div v-if="reactions.length === 0" class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.nothing }}</div> </div> <template v-else> @@ -42,6 +42,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue'; import { userPage } from '@/filters/user'; import { i18n } from '@/i18n'; import * as os from '@/os'; +import { infoImageUrl } from '@/instance'; const emit = defineEmits<{ (ev: 'closed'): void, diff --git a/packages/frontend/src/components/MkRenotedUsersDialog.vue b/packages/frontend/src/components/MkRenotedUsersDialog.vue index 814a68d4da..484cb2f9a7 100644 --- a/packages/frontend/src/components/MkRenotedUsersDialog.vue +++ b/packages/frontend/src/components/MkRenotedUsersDialog.vue @@ -11,7 +11,7 @@ <MkSpacer :marginMin="20" :marginMax="28"> <div v-if="renotes" class="_gaps"> <div v-if="renotes.length === 0" class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.nothing }}</div> </div> <template v-else> @@ -35,6 +35,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue'; import { userPage } from '@/filters/user'; import { i18n } from '@/i18n'; import * as os from '@/os'; +import { infoImageUrl } from '@/instance'; const emit = defineEmits<{ (ev: 'closed'): void, diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue index 3571ca84d9..2a23f3e70d 100644 --- a/packages/frontend/src/components/MkUserList.vue +++ b/packages/frontend/src/components/MkUserList.vue @@ -2,7 +2,7 @@ <MkPagination :pagination="pagination"> <template #empty> <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.noUsers }}</div> </div> </template> @@ -19,6 +19,7 @@ import MkUserInfo from '@/components/MkUserInfo.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue'; import { i18n } from '@/i18n'; +import { infoImageUrl } from '@/instance'; const props = withDefaults(defineProps<{ pagination: Paging; diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue index 513ef21d35..503e00387c 100644 --- a/packages/frontend/src/components/global/MkError.vue +++ b/packages/frontend/src/components/global/MkError.vue @@ -1,7 +1,7 @@ <template> <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear> <div :class="$style.root"> - <img :class="$style.img" src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> + <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p> <MkButton :class="$style.button" @click="() => emit('retry')">{{ i18n.ts.retry }}</MkButton> </div> @@ -12,6 +12,7 @@ import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; +import { serverErrorImageUrl } from '@/instance'; const emit = defineEmits<{ (ev: 'retry'): void; diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index aaa3d10302..ad7fa372e9 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -78,3 +78,7 @@ export const ROLE_POLICIES = [ //export const CURRENT_STICKY_BOTTOM = Symbol('CURRENT_STICKY_BOTTOM'); export const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP'; export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM'; + +export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error.jpg'; +export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://xn--931a.moe/assets/not-found.jpg'; +export const DEFAULT_INFO_IMAGE_URL = 'https://xn--931a.moe/assets/info.jpg'; diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts index f4c1988704..9cfcbcbc3f 100644 --- a/packages/frontend/src/instance.ts +++ b/packages/frontend/src/instance.ts @@ -1,7 +1,8 @@ -import { reactive } from 'vue'; +import { computed, reactive } from 'vue'; import * as Misskey from 'misskey-js'; import { api } from './os'; import { miLocalStorage } from './local-storage'; +import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@/const'; // TODO: 他のタブと永続化されたstateを同期 @@ -13,6 +14,12 @@ export const instance: Misskey.entities.InstanceMetadata = reactive(cached ? JSO // TODO: set default values }); +export const serverErrorImageUrl = computed(() => instance.serverErrorImageUrl ?? DEFAULT_SERVER_ERROR_IMAGE_URL); + +export const infoImageUrl = computed(() => instance.infoImageUrl ?? DEFAULT_INFO_IMAGE_URL); + +export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL); + export async function fetchInstance() { const meta = await api('meta', { detail: false, diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue index f27d2df336..eee661bd8a 100644 --- a/packages/frontend/src/pages/_error_.vue +++ b/packages/frontend/src/pages/_error_.vue @@ -2,7 +2,7 @@ <MkLoading v-if="!loaded"/> <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear> <div v-show="loaded" :class="$style.root"> - <img src="https://xn--931a.moe/assets/error.jpg" class="_ghost" :class="$style.img"/> + <img :src="serverErrorImageUrl" class="_ghost" :class="$style.img"/> <div class="_gaps"> <p><b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.pageLoadError }}</b></p> <p v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</p> @@ -30,6 +30,7 @@ import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import { miLocalStorage } from '@/local-storage'; import { defaultStore } from '@/store'; +import { serverErrorImageUrl } from '@/instance'; const props = withDefaults(defineProps<{ error?: Error; diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue new file mode 100644 index 0000000000..65ce9e0068 --- /dev/null +++ b/packages/frontend/src/pages/admin/branding.vue @@ -0,0 +1,133 @@ +<template> +<div> + <MkStickyContainer> + <template #header><XHeader :tabs="headerTabs"/></template> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> + <FormSuspense :p="init"> + <div class="_gaps_m"> + <MkInput v-model="iconUrl"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts.iconUrl }}</template> + </MkInput> + + <MkInput v-model="bannerUrl"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts.bannerUrl }}</template> + </MkInput> + + <MkInput v-model="backgroundImageUrl"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts.backgroundImageUrl }}</template> + </MkInput> + + <MkInput v-model="notFoundImageUrl"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts.notFoundDescription }}</template> + </MkInput> + + <MkInput v-model="infoImageUrl"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts.nothing }}</template> + </MkInput> + + <MkInput v-model="serverErrorImageUrl"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts.somethingHappened }}</template> + </MkInput> + + <MkColorInput v-model="themeColor"> + <template #label>{{ i18n.ts.themeColor }}</template> + </MkColorInput> + + <MkTextarea v-model="defaultLightTheme"> + <template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template> + <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template> + </MkTextarea> + + <MkTextarea v-model="defaultDarkTheme"> + <template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template> + <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template> + </MkTextarea> + </div> + </FormSuspense> + </MkSpacer> + <template #footer> + <div :class="$style.footer"> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> + <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + </MkSpacer> + </div> + </template> + </MkStickyContainer> +</div> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import XHeader from './_header_.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import MkInput from '@/components/MkInput.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 * as os from '@/os'; +import { fetchInstance } from '@/instance'; +import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; +import MkButton from '@/components/MkButton.vue'; +import MkColorInput from '@/components/MkColorInput.vue'; + +let iconUrl: string | null = $ref(null); +let bannerUrl: string | null = $ref(null); +let backgroundImageUrl: string | null = $ref(null); +let themeColor: any = $ref(null); +let defaultLightTheme: any = $ref(null); +let defaultDarkTheme: any = $ref(null); +let serverErrorImageUrl: string | null = $ref(null); +let infoImageUrl: string | null = $ref(null); +let notFoundImageUrl: string | null = $ref(null); + +async function init() { + const meta = await os.api('admin/meta'); + iconUrl = meta.iconUrl; + bannerUrl = meta.bannerUrl; + backgroundImageUrl = meta.backgroundImageUrl; + themeColor = meta.themeColor; + defaultLightTheme = meta.defaultLightTheme; + defaultDarkTheme = meta.defaultDarkTheme; + serverErrorImageUrl = meta.serverErrorImageUrl; + infoImageUrl = meta.infoImageUrl; + notFoundImageUrl = meta.notFoundImageUrl; +} + +function save() { + os.apiWithDialog('admin/update-meta', { + iconUrl, + bannerUrl, + backgroundImageUrl, + themeColor: themeColor === '' ? null : themeColor, + defaultLightTheme: defaultLightTheme === '' ? null : defaultLightTheme, + defaultDarkTheme: defaultDarkTheme === '' ? null : defaultDarkTheme, + infoImageUrl, + notFoundImageUrl, + serverErrorImageUrl, + }).then(() => { + fetchInstance(); + }); +} + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.branding, + icon: 'ti ti-paint', +}); +</script> + +<style lang="scss" module> +.footer { + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); +} +</style> diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 5cbbcaa44c..8b083bc896 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -144,6 +144,11 @@ const menuDef = $computed(() => [{ to: '/admin/settings', active: currentPage?.route.name === 'settings', }, { + icon: 'ti ti-paint', + text: i18n.ts.branding, + to: '/admin/branding', + active: currentPage?.route.name === 'branding', + }, { icon: 'ti ti-shield', text: i18n.ts.moderation, to: '/admin/moderation', diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index 4ed6abf200..6cbe7ae658 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -23,7 +23,7 @@ <MkPagination :pagination="usersPagination"> <template #empty> <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.noUsers }}</div> </div> </template> @@ -69,6 +69,7 @@ import MkButton from '@/components/MkButton.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue'; +import { infoImageUrl } from '@/instance'; const router = useRouter(); diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 39d5ae8607..4c2fe46f28 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -30,41 +30,6 @@ </MkTextarea> <FormSection> - <template #label>{{ i18n.ts.theme }}</template> - - <div class="_gaps_m"> - <MkInput v-model="iconUrl"> - <template #prefix><i class="ti ti-link"></i></template> - <template #label>{{ i18n.ts.iconUrl }}</template> - </MkInput> - - <MkInput v-model="bannerUrl"> - <template #prefix><i class="ti ti-link"></i></template> - <template #label>{{ i18n.ts.bannerUrl }}</template> - </MkInput> - - <MkInput v-model="backgroundImageUrl"> - <template #prefix><i class="ti ti-link"></i></template> - <template #label>{{ i18n.ts.backgroundImageUrl }}</template> - </MkInput> - - <MkColorInput v-model="themeColor"> - <template #label>{{ i18n.ts.themeColor }}</template> - </MkColorInput> - - <MkTextarea v-model="defaultLightTheme"> - <template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template> - <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template> - </MkTextarea> - - <MkTextarea v-model="defaultDarkTheme"> - <template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template> - <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template> - </MkTextarea> - </div> - </FormSection> - - <FormSection> <template #label>{{ i18n.ts.files }}</template> <div class="_gaps_m"> @@ -145,12 +110,6 @@ let name: string | null = $ref(null); let description: string | null = $ref(null); let maintainerName: string | null = $ref(null); let maintainerEmail: string | null = $ref(null); -let iconUrl: string | null = $ref(null); -let bannerUrl: string | null = $ref(null); -let backgroundImageUrl: string | null = $ref(null); -let themeColor: any = $ref(null); -let defaultLightTheme: any = $ref(null); -let defaultDarkTheme: any = $ref(null); let pinnedUsers: string = $ref(''); let cacheRemoteFiles: boolean = $ref(false); let enableServiceWorker: boolean = $ref(false); @@ -163,12 +122,6 @@ async function init() { const meta = await os.api('admin/meta'); name = meta.name; description = meta.description; - iconUrl = meta.iconUrl; - bannerUrl = meta.bannerUrl; - backgroundImageUrl = meta.backgroundImageUrl; - themeColor = meta.themeColor; - defaultLightTheme = meta.defaultLightTheme; - defaultDarkTheme = meta.defaultDarkTheme; maintainerName = meta.maintainerName; maintainerEmail = meta.maintainerEmail; pinnedUsers = meta.pinnedUsers.join('\n'); @@ -184,12 +137,6 @@ function save() { os.apiWithDialog('admin/update-meta', { name, description, - iconUrl, - bannerUrl, - backgroundImageUrl, - themeColor: themeColor === '' ? null : themeColor, - defaultLightTheme: defaultLightTheme === '' ? null : defaultLightTheme, - defaultDarkTheme: defaultDarkTheme === '' ? null : defaultDarkTheme, maintainerName, maintainerEmail, pinnedUsers: pinnedUsers.split('\n'), diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue index 460bf65d1e..21c306148b 100644 --- a/packages/frontend/src/pages/favorites.vue +++ b/packages/frontend/src/pages/favorites.vue @@ -5,7 +5,7 @@ <MkPagination :pagination="pagination"> <template #empty> <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.noNotes }}</div> </div> </template> @@ -26,6 +26,7 @@ import MkNote from '@/components/MkNote.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { infoImageUrl } from '@/instance'; const pagination = { endpoint: 'i/favorites' as const, diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue index 1452942a1e..a70a4894a5 100644 --- a/packages/frontend/src/pages/follow-requests.vue +++ b/packages/frontend/src/pages/follow-requests.vue @@ -5,7 +5,7 @@ <MkPagination ref="paginationComponent" :pagination="pagination"> <template #empty> <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.noFollowRequests }}</div> </div> </template> @@ -39,6 +39,7 @@ import { userPage, acct } from '@/filters/user'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { infoImageUrl } from '@/instance'; const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>(); diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue index f92c06d1c5..40934fb71d 100644 --- a/packages/frontend/src/pages/list.vue +++ b/packages/frontend/src/pages/list.vue @@ -3,7 +3,7 @@ <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> <div :class="$style.root"> - <img :class="$style.img" src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> + <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <p :class="$style.text"> <i class="ti ti-alert-triangle"></i> {{ i18n.ts.nothing }} @@ -36,6 +36,7 @@ import { i18n } from '@/i18n'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkButton from '@/components/MkButton.vue'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { serverErrorImageUrl } from '@/instance'; const props = defineProps<{ listId: string; diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue index 2c9d949017..43dc41e7cc 100644 --- a/packages/frontend/src/pages/not-found.vue +++ b/packages/frontend/src/pages/not-found.vue @@ -1,7 +1,7 @@ <template> <div> <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/not-found.jpg" class="_ghost"/> + <img :src="notFoundImageUrl" class="_ghost"/> <div>{{ i18n.ts.notFoundDescription }}</div> </div> </div> @@ -10,6 +10,7 @@ <script lang="ts" setup> import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { notFoundImageUrl } from '@/instance'; const headerActions = $computed(() => []); diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue index e85ab0917a..fc04468d5a 100644 --- a/packages/frontend/src/pages/role.vue +++ b/packages/frontend/src/pages/role.vue @@ -3,7 +3,7 @@ <template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template> <MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> <div :class="$style.root"> - <img :class="$style.img" src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> + <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <p :class="$style.text"> <i class="ti ti-alert-triangle"></i> {{ error }} @@ -30,6 +30,7 @@ import { definePageMetadata } from '@/scripts/page-metadata'; import { i18n } from '@/i18n'; import MkTimeline from '@/components/MkTimeline.vue'; import { instanceName } from '@/config'; +import { serverErrorImageUrl } from '@/instance'; const props = withDefaults(defineProps<{ role: string; diff --git a/packages/frontend/src/pages/settings/account-stats.vue b/packages/frontend/src/pages/settings/account-stats.vue deleted file mode 100644 index a0f1541b40..0000000000 --- a/packages/frontend/src/pages/settings/account-stats.vue +++ /dev/null @@ -1,146 +0,0 @@ -<template> -<div class="_gaps_m"> - <FormSection v-if="stats" first> - <template #label>{{ i18n.ts.statistics }}</template> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.notesCount }}</template> - <template #value>{{ number(stats.notesCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.repliesCount }}</template> - <template #value>{{ number(stats.repliesCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.renotesCount }}</template> - <template #value>{{ number(stats.renotesCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.repliedCount }}</template> - <template #value>{{ number(stats.repliedCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.renotedCount }}</template> - <template #value>{{ number(stats.renotedCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.pollVotesCount }}</template> - <template #value>{{ number(stats.pollVotesCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.pollVotedCount }}</template> - <template #value>{{ number(stats.pollVotedCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.sentReactionsCount }}</template> - <template #value>{{ number(stats.sentReactionsCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.receivedReactionsCount }}</template> - <template #value>{{ number(stats.receivedReactionsCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.noteFavoritesCount }}</template> - <template #value>{{ number(stats.noteFavoritesCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.followingCount }}</template> - <template #value>{{ number(stats.followingCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.followingCount }} ({{ i18n.ts.local }})</template> - <template #value>{{ number(stats.localFollowingCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.followingCount }} ({{ i18n.ts.remote }})</template> - <template #value>{{ number(stats.remoteFollowingCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.followersCount }}</template> - <template #value>{{ number(stats.followersCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.followersCount }} ({{ i18n.ts.local }})</template> - <template #value>{{ number(stats.localFollowersCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.followersCount }} ({{ i18n.ts.remote }})</template> - <template #value>{{ number(stats.remoteFollowersCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.pageLikesCount }}</template> - <template #value>{{ number(stats.pageLikesCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.pageLikedCount }}</template> - <template #value>{{ number(stats.pageLikedCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.driveFilesCount }}</template> - <template #value>{{ number(stats.driveFilesCount) }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>{{ i18n.ts.driveUsage }}</template> - <template #value>{{ bytes(stats.driveUsage) }}</template> - </MkKeyValue> - </FormSection> - - <FormSection> - <template #label>{{ i18n.ts.other }}</template> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>emailVerified</template> - <template #value>{{ $i.emailVerified ? i18n.ts.yes : i18n.ts.no }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>twoFactorEnabled</template> - <template #value>{{ $i.twoFactorEnabled ? i18n.ts.yes : i18n.ts.no }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>securityKeys</template> - <template #value>{{ $i.securityKeys ? i18n.ts.yes : i18n.ts.no }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>usePasswordLessLogin</template> - <template #value>{{ $i.usePasswordLessLogin ? i18n.ts.yes : i18n.ts.no }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>isModerator</template> - <template #value>{{ $i.isModerator ? i18n.ts.yes : i18n.ts.no }}</template> - </MkKeyValue> - <MkKeyValue oneline style="margin: 1em 0;"> - <template #key>isAdmin</template> - <template #value>{{ $i.isAdmin ? i18n.ts.yes : i18n.ts.no }}</template> - </MkKeyValue> - </FormSection> -</div> -</template> - -<script lang="ts" setup> -import { onMounted, ref } from 'vue'; -import FormSection from '@/components/form/section.vue'; -import MkKeyValue from '@/components/MkKeyValue.vue'; -import * as os from '@/os'; -import number from '@/filters/number'; -import bytes from '@/filters/bytes'; -import { $i } from '@/account'; -import { i18n } from '@/i18n'; -import { definePageMetadata } from '@/scripts/page-metadata'; - -const stats = ref<any>({}); - -onMounted(() => { - os.api('users/stats', { - userId: $i!.id, - }).then(response => { - stats.value = response; - }); -}); - -const headerActions = $computed(() => []); - -const headerTabs = $computed(() => []); - -definePageMetadata({ - title: i18n.ts.accountInfo, - icon: 'ti ti-info-circle', -}); -</script> diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue index fbb78200d4..cadce49230 100644 --- a/packages/frontend/src/pages/settings/apps.vue +++ b/packages/frontend/src/pages/settings/apps.vue @@ -3,7 +3,7 @@ <FormPagination ref="list" :pagination="pagination"> <template #empty> <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.nothing }}</div> </div> </template> @@ -47,6 +47,7 @@ import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import MkKeyValue from '@/components/MkKeyValue.vue'; import MkButton from '@/components/MkButton.vue'; +import { infoImageUrl } from '@/instance'; const list = ref<any>(null); diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index 3d0463f708..e0785ab9fe 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -10,7 +10,7 @@ <MkPagination :pagination="renoteMutingPagination"> <template #empty> <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.noUsers }}</div> </div> </template> @@ -38,7 +38,7 @@ <MkPagination :pagination="mutingPagination"> <template #empty> <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.noUsers }}</div> </div> </template> @@ -68,7 +68,7 @@ <MkPagination :pagination="blockingPagination"> <template #empty> <div class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.noUsers }}</div> </div> </template> @@ -107,6 +107,7 @@ import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import * as os from '@/os'; +import { infoImageUrl } from '@/instance'; let tab = $ref('renoteMute'); diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 0b73780a8b..3d8bb59277 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -26,8 +26,6 @@ <template #key>{{ i18n.ts.registeredDate }}</template> <template #value><MkTime :time="$i.createdAt" mode="detail"/></template> </MkKeyValue> - - <FormLink to="/settings/account-stats"><template #icon><i class="ti ti-info-circle"></i></template>{{ i18n.ts.statistics }}</FormLink> </div> </MkFolder> diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 58217d0475..81f0518a06 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -127,7 +127,6 @@ const profile = reactive({ lang: $i.lang, isBot: $i.isBot, isCat: $i.isCat, - showTimelineReplies: $i.showTimelineReplies, }); watch(() => profile, () => { @@ -151,7 +150,7 @@ while (fields.value.length < 4) { addField(); } -function deleteField(index: number) { +function deleteField(index: number) { fields.value.splice(index, 1); } @@ -176,7 +175,6 @@ function save() { lang: profile.lang || null, isBot: !!profile.isBot, isCat: !!profile.isCat, - showTimelineReplies: !!profile.showTimelineReplies, }); claimAchievement('profileFilled'); if (profile.name === 'syuilo' || profile.name === 'しゅいろ') { diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 6b11137d79..fe9bc5938e 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -178,10 +178,6 @@ export const routes = [{ name: 'profile', component: page(() => import('./pages/settings/accounts.vue')), }, { - path: '/account-stats', - name: 'other', - component: page(() => import('./pages/settings/account-stats.vue')), - }, { path: '/other', name: 'other', component: page(() => import('./pages/settings/other.vue')), @@ -393,6 +389,10 @@ export const routes = [{ name: 'settings', component: page(() => import('./pages/admin/settings.vue')), }, { + path: '/branding', + name: 'branding', + component: page(() => import('./pages/admin/branding.vue')), + }, { path: '/moderation', name: 'moderation', component: page(() => import('./pages/admin/moderation.vue')), diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index c828731773..bd5d5beb84 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -254,6 +254,28 @@ async function deleteProfile() { } </script> +<style> +html, +body { + width: 100%; + height: 100%; + overflow: clip; + position: fixed; + top: 0; + left: 0; + overscroll-behavior: none; +} + +#misskey_app { + width: 100%; + height: 100%; + overflow: clip; + position: absolute; + top: 0; + left: 0; +} +</style> + <style lang="scss" module> .transition_menuDrawerBg_enterActive, .transition_menuDrawerBg_leaveActive { diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index c376eb2b47..c8d6744a37 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -313,6 +313,7 @@ function onDrop(ev) { > .body { background: var(--bg) !important; + overflow-y: scroll !important; &::-webkit-scrollbar-track { background: inherit; diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index c0da59a57d..8abb20300f 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -215,6 +215,28 @@ watch($$(navFooter), () => { }); </script> +<style> +html, +body { + width: 100%; + height: 100%; + overflow: clip; + position: fixed; + top: 0; + left: 0; + overscroll-behavior: none; +} + +#misskey_app { + width: 100%; + height: 100%; + overflow: clip; + position: absolute; + top: 0; + left: 0; +} +</style> + <style lang="scss" module> $ui-font-size: 1em; // TODO: どこかに集約したい $widgets-hide-threshold: 1090px; diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue index 1be882c66d..953ce2908d 100644 --- a/packages/frontend/src/widgets/WidgetRss.vue +++ b/packages/frontend/src/widgets/WidgetRss.vue @@ -7,7 +7,7 @@ <div class="ekmkgxbj"> <MkLoading v-if="fetching"/> <div v-else-if="(!items || items.length === 0) && widgetProps.showHeader" class="_fullinfo"> - <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <img :src="infoImageUrl" class="_ghost"/> <div>{{ i18n.ts.nothing }}</div> </div> <div v-else :class="$style.feed"> @@ -25,6 +25,7 @@ import MkContainer from '@/components/MkContainer.vue'; import { url as base } from '@/config'; import { i18n } from '@/i18n'; import { useInterval } from '@/scripts/use-interval'; +import { infoImageUrl } from '@/instance'; const name = 'rss'; diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 396187a439..ca8eee01a1 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2147,10 +2147,6 @@ export type Endpoints = { }; }; }; - 'users/stats': { - req: TODO; - res: TODO; - }; }; declare namespace entities { @@ -2324,7 +2320,9 @@ type LiteInstanceMetadata = { themeColor: string | null; mascotImageUrl: string | null; bannerUrl: string | null; - errorImageUrl: string | null; + serverErrorImageUrl: string | null; + infoImageUrl: string | null; + notFoundImageUrl: string | null; iconUrl: string | null; backgroundImageUrl: string | null; logoImageUrl: string | null; diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index cc88c4b1a4..b8c59e7b15 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -602,5 +602,4 @@ export type Endpoints = { $default: UserDetailed; }; }; }; - 'users/stats': { req: TODO; res: TODO; }; }; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 04065c51c9..383b17f0b9 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -294,7 +294,9 @@ export type LiteInstanceMetadata = { themeColor: string | null; mascotImageUrl: string | null; bannerUrl: string | null; - errorImageUrl: string | null; + serverErrorImageUrl: string | null; + infoImageUrl: string | null; + notFoundImageUrl: string | null; iconUrl: string | null; backgroundImageUrl: string | null; logoImageUrl: string | null; |