Merge branch 'develop' into mkjs-n

This commit is contained in:
tamaina
2023-06-16 06:14:29 +00:00
75 changed files with 650 additions and 562 deletions

View File

@@ -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'`);
}
}

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -435,6 +435,7 @@ export class RoleService implements OnApplicationShutdown {
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
this.roleAssignmentByUserIdCache.dispose();
}
@bindThis

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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> {

View File

@@ -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,

View File

@@ -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();
}
}

View File

@@ -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,

View File

@@ -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

View File

@@ -42,7 +42,9 @@ export default class extends Endpoint<'admin/meta'> {
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,

View File

@@ -56,6 +56,18 @@ export default class extends Endpoint<'admin/update-meta'> {
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;
}
@@ -188,10 +200,6 @@ export default class extends Endpoint<'admin/update-meta'> {
set.smtpPass = ps.smtpPass;
}
if (ps.errorImageUrl !== undefined) {
set.errorImageUrl = ps.errorImageUrl;
}
if (ps.enableServiceWorker !== undefined) {
set.enableServiceWorker = ps.enableServiceWorker;
}

View File

@@ -146,7 +146,7 @@ export const paramDef = {
alwaysMarkNsfw: { type: 'boolean' },
autoSensitive: { type: 'boolean' },
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
pinnedPageId: { type: 'string', format: 'misskey:id' },
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
mutedWords: { type: 'array' },
mutedInstances: { type: 'array', items: {
type: 'string',

View File

@@ -1,6 +1,6 @@
import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import * as JSON5 from 'json5';
import JSON5 from 'json5';
import type { AdsRepository, UsersRepository } from '@/models/index.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -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,

View File

@@ -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);
});

View File

@@ -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) {

View File

@@ -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,
};
});
}
}

View File

@@ -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';
@@ -117,6 +117,18 @@ export class ClientServerService {
return (res);
}
@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);

View File

@@ -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}`)

View File

@@ -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}`

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 'misskey-js';
import { infoImageUrl } from '@/instance';
const props = defineProps<{
includeTypes?: typeof notificationTypes[number][];

View File

@@ -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;

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -22,7 +22,7 @@
<div :class="$style.username"><MkAcct :user="user"/></div>
</div>
<div :class="$style.description">
<Mfm v-if="user.description" :text="user.description" :author="user" :i="$i"/>
<Mfm v-if="user.description" :class="$style.mfm" :text="user.description" :author="user" :i="$i"/>
<div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div>
</div>
<div :class="$style.status">
@@ -192,6 +192,13 @@ onMounted(() => {
border-bottom: solid 1px var(--divider);
}
.mfm {
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
.status {
padding: 16px 26px 16px 26px;
}

View File

@@ -1,6 +1,6 @@
<template>
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true"/>
<img :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true"/>
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
<div v-if="user.isCat" :class="[$style.ears]">
<div :class="$style.earLeft">
@@ -24,7 +24,6 @@
<script lang="ts" setup>
import { watch } from 'vue';
import * as misskey from 'misskey-js';
import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
import MkA from './MkA.vue';
import { getStaticImageUrl } from '@/scripts/media-proxy';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';

View File

@@ -13,13 +13,20 @@ interface Props {
const contentSymbol = Symbol();
const observer = new ResizeObserver((entries) => {
const results: {
container: HTMLSpanElement;
transform: string;
}[] = [];
for (const entry of entries) {
const content = (entry.target[contentSymbol] ? entry.target : entry.target.firstElementChild) as HTMLSpanElement;
const props: Required<Props> = content[contentSymbol];
const container = content.parentElement as HTMLSpanElement;
const contentWidth = content.getBoundingClientRect().width;
const containerWidth = container.getBoundingClientRect().width;
container.style.transform = `scaleX(${Math.max(props.minScale, Math.min(1, containerWidth / contentWidth))})`;
results.push({ container, transform: `scaleX(${Math.max(props.minScale, Math.min(1, containerWidth / contentWidth))})` });
}
for (const result of results) {
result.container.style.transform = result.transform;
}
});
</script>

View File

@@ -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;

View File

@@ -75,3 +75,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';

View File

@@ -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,

View File

@@ -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;

View File

@@ -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>

View File

@@ -143,6 +143,11 @@ const menuDef = $computed(() => [{
text: i18n.ts.general,
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,

View File

@@ -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();

View File

@@ -29,41 +29,6 @@
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
</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>
@@ -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'),

View File

@@ -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,

View File

@@ -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>>();

View File

@@ -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;

View File

@@ -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(() => []);

View File

@@ -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;

View File

@@ -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>

View File

@@ -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);

View File

@@ -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');

View File

@@ -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>

View File

@@ -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 === 'しゅいろ') {

View File

@@ -177,10 +177,6 @@ export const routes = [{
path: '/accounts',
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',
@@ -392,6 +388,10 @@ export const routes = [{
path: '/settings',
name: 'settings',
component: page(() => import('./pages/admin/settings.vue')),
}, {
path: '/branding',
name: 'branding',
component: page(() => import('./pages/admin/branding.vue')),
}, {
path: '/moderation',
name: 'moderation',

View File

@@ -12,5 +12,14 @@ export function useStream(): Misskey.Stream {
token: $i.token,
} : null));
window.setTimeout(heartbeat, 1000 * 60);
return stream;
}
function heartbeat(): void {
if (stream != null && document.visibilityState === 'visible') {
stream.heartbeat();
}
window.setTimeout(heartbeat, 1000 * 60);
}

View File

@@ -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 {

View File

@@ -313,6 +313,7 @@ function onDrop(ev) {
> .body {
background: var(--bg) !important;
overflow-y: scroll !important;
&::-webkit-scrollbar-track {
background: inherit;

View File

@@ -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;

View File

@@ -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';

View File

@@ -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;
@@ -2606,6 +2604,10 @@ export class Stream extends EventEmitter<StreamEvents> {
//
// (undocumented)
disconnectToChannel(connection: NonSharedConnection): void;
// (undocumented)
heartbeat(): void;
// (undocumented)
ping(): void;
// Warning: (ae-forgotten-export) The symbol "SharedConnection" needs to be exported by the entry point index.d.ts
//
// (undocumented)

View File

@@ -584,5 +584,4 @@ export type Endpoints = {
$default: UserDetailed;
};
}; };
'users/stats': { req: TODO; res: TODO; };
};

View File

@@ -71,7 +71,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;

View File

@@ -186,6 +186,14 @@ export default class Stream extends EventEmitter<StreamEvents> {
this.stream.send(JSON.stringify(typeOrPayload));
}
public ping(): void {
this.stream.send('ping');
}
public heartbeat(): void {
this.stream.send('h');
}
/**
* Close this connection
*/