Compare commits
22 Commits
781570e3e6
...
723fd17335
Author | SHA1 | Date |
---|---|---|
かっこかり | 723fd17335 | |
かっこかり | 3bf63dd9c5 | |
かっこかり | ce95323e49 | |
FineArchs | daf9ae5d4a | |
kakkokari-gtyih | 4ad11a238d | |
kakkokari-gtyih | e304c23c2c | |
まっちゃとーにゅ | d1d4bf1c19 | |
まっちゃとーにゅ | 0ed2b06bc4 | |
まっちゃとーにゅ | c7be019fdd | |
kakkokari-gtyih | e898f98b34 | |
かっこかり | 455783e414 | |
かっこかり | 3f05ec3b40 | |
かっこかり | aec7cb6fcf | |
かっこかり | 2f36c689ae | |
かっこかり | 51bb310318 | |
かっこかり | 04a6df59db | |
kakkokari-gtyih | d98df9edf1 | |
kakkokari-gtyih | 285c4b5d37 | |
kakkokari-gtyih | a0cc91adc1 | |
nenohi | ed45a38431 | |
nenohi | 3f79f3585f | |
nenohi | 01cbe12931 |
|
@ -1,7 +1,8 @@
|
|||
## Unreleased
|
||||
|
||||
### General
|
||||
-
|
||||
- Enhance: 人気ユーザーの算出基準を変更できるように
|
||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/653, https://github.com/MisskeyIO/misskey/pull/664, https://github.com/MisskeyIO/misskey/pull/728, https://github.com/MisskeyIO/misskey/pull/734, https://github.com/MisskeyIO/misskey/pull/737)
|
||||
|
||||
### Client
|
||||
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
|
||||
|
@ -9,10 +10,12 @@
|
|||
- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
|
||||
- Enhance: アイコンデコレーション管理画面にプレビューを追加
|
||||
- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく
|
||||
- Enhance: ScratchpadにUIインスペクターを追加
|
||||
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
|
||||
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
|
||||
- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正
|
||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725)
|
||||
- Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正
|
||||
|
||||
### Server
|
||||
- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように
|
||||
|
|
|
@ -3121,7 +3121,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"narrow": string;
|
||||
/**
|
||||
* 設定はページリロード後に反映されます。今すぐリロードしますか?
|
||||
* 設定はページリロード後に反映されます。
|
||||
*/
|
||||
"reloadToApplySetting": string;
|
||||
/**
|
||||
|
@ -5084,6 +5084,18 @@ export interface Locale extends ILocale {
|
|||
* これ以上このクリップにノートを追加できません。
|
||||
*/
|
||||
"clipNoteLimitExceeded": string;
|
||||
/**
|
||||
* ページ閲覧数
|
||||
*/
|
||||
"pageViewCount": string;
|
||||
/**
|
||||
* 人気のユーザーの算出基準
|
||||
*/
|
||||
"preferPopularUserFactor": string;
|
||||
/**
|
||||
* ページ閲覧数はローカルユーザーにのみ適用されます(リモートユーザーはフォロワー数で表示されます)。「無効」に設定すると、ローカル・リモートどちらの「人気のユーザー」セクションも表示されなくなります。
|
||||
*/
|
||||
"preferPopularUserFactorDescription": string;
|
||||
"_delivery": {
|
||||
/**
|
||||
* 配信状態
|
||||
|
|
|
@ -592,6 +592,8 @@ ascendingOrder: "昇順"
|
|||
descendingOrder: "降順"
|
||||
scratchpad: "スクラッチパッド"
|
||||
scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。"
|
||||
uiInspector: "UIインスペクター"
|
||||
uiInspectorDescription: "メモリ上に存在しているUIコンポーネントのインスタンスの一覧を見ることができます。UIコンポーネントはUi:C:系関数により生成されます。"
|
||||
output: "出力"
|
||||
script: "スクリプト"
|
||||
disablePagesScript: "Pagesのスクリプトを無効にする"
|
||||
|
@ -776,7 +778,7 @@ left: "左"
|
|||
center: "中央"
|
||||
wide: "広い"
|
||||
narrow: "狭い"
|
||||
reloadToApplySetting: "設定はページリロード後に反映されます。今すぐリロードしますか?"
|
||||
reloadToApplySetting: "設定はページリロード後に反映されます。"
|
||||
needReloadToApply: "反映には再起動が必要です。"
|
||||
showTitlebar: "タイトルバーを表示する"
|
||||
clearCache: "キャッシュをクリア"
|
||||
|
@ -1267,6 +1269,9 @@ fromX: "{x}から"
|
|||
genEmbedCode: "埋め込みコードを生成"
|
||||
noteOfThisUser: "このユーザーのノート一覧"
|
||||
clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
|
||||
pageViewCount: "ページ閲覧数"
|
||||
preferPopularUserFactor: "人気のユーザーの算出基準"
|
||||
preferPopularUserFactorDescription: "ページ閲覧数はローカルユーザーにのみ適用されます(リモートユーザーはフォロワー数で表示されます)。「無効」に設定すると、ローカル・リモートどちらの「人気のユーザー」セクションも表示されなくなります。"
|
||||
|
||||
_delivery:
|
||||
status: "配信状態"
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class PreferPopularUserFactor1720879227657 {
|
||||
name = 'PreferPopularUserFactor1720879227657'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "meta_preferPopularUserFactor_enum" AS ENUM('follower', 'pv', 'none')`)
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "preferPopularUserFactor" "meta_preferPopularUserFactor_enum" NOT NULL DEFAULT 'follower'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "preferPopularUserFactor"`);
|
||||
await queryRunner.query(`DROP TYPE "meta_preferPopularUserFactor_enum"`);
|
||||
}
|
||||
}
|
|
@ -123,11 +123,14 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
if (antenna.src === 'home') {
|
||||
// TODO
|
||||
} else if (antenna.src === 'list') {
|
||||
const listUsers = (await this.userListMembershipsRepository.findBy({
|
||||
userListId: antenna.userListId!,
|
||||
})).map(x => x.userId);
|
||||
|
||||
if (!listUsers.includes(note.userId)) return false;
|
||||
if (antenna.userListId == null) return false;
|
||||
const exists = await this.userListMembershipsRepository.exists({
|
||||
where: {
|
||||
userListId: antenna.userListId,
|
||||
userId: note.userId,
|
||||
},
|
||||
});
|
||||
if (!exists) return false;
|
||||
} else if (antenna.src === 'users') {
|
||||
const accts = antenna.users.map(x => {
|
||||
const { username, host } = Acct.parse(x);
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Injectable, Inject } from '@nestjs/common';
|
|||
import { DataSource } from 'typeorm';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { addTime, dateUTC, subtractTime } from '@/misc/prelude/time.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import Chart from '../core.js';
|
||||
|
@ -52,4 +53,32 @@ export default class PerUserPvChart extends Chart<typeof schema> { // eslint-dis
|
|||
'pv.visitor': 1,
|
||||
}, user.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getUsersRanking(span: 'hour' | 'day', order: 'ASC' | 'DESC', amount: number, cursor: Date | null, limit = 0, offset = 0): Promise<{ userId: string; count: number; }[]> {
|
||||
const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate();
|
||||
const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never;
|
||||
|
||||
const lt = dateUTC([y, m, d, h, _m, _s, _ms]);
|
||||
|
||||
const gt =
|
||||
span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') :
|
||||
span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') :
|
||||
new Error('not happen') as never;
|
||||
|
||||
const repository =
|
||||
span === 'hour' ? this.repositoryForHour :
|
||||
span === 'day' ? this.repositoryForDay :
|
||||
new Error('not happen') as never;
|
||||
|
||||
// ログ取得
|
||||
return await repository.createQueryBuilder()
|
||||
.select('"group" as "userId", sum("___upv_user" + "___upv_visitor") as "count"')
|
||||
.where('date BETWEEN :gt AND :lt', { gt: Chart.dateToTimestamp(gt), lt: Chart.dateToTimestamp(lt) })
|
||||
.groupBy('"userId"')
|
||||
.orderBy('"count"', order)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.getRawMany<{ userId: string, count: number }>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc
|
|||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MiRepository, miRepository } from '@/models/_.js';
|
||||
import type { DataSource, Repository } from 'typeorm';
|
||||
import type { DataSource, ObjectLiteral, Repository } from 'typeorm';
|
||||
|
||||
const COLUMN_PREFIX = '___' as const;
|
||||
const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
|
||||
|
@ -94,6 +94,8 @@ type ToJsonSchema<S> = {
|
|||
required: (keyof S)[];
|
||||
};
|
||||
|
||||
type MiAndOrmRepository<T extends ObjectLiteral> = Repository<T> & MiRepository<T>;
|
||||
|
||||
export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatten<ChartResult<S>>> {
|
||||
const unflatten = (str: string, parent: Record<string, any>) => {
|
||||
const keys = str.split('.');
|
||||
|
@ -146,11 +148,10 @@ export default abstract class Chart<T extends Schema> {
|
|||
group: string | null;
|
||||
}[] = [];
|
||||
// ↓にしたいけどfindOneとかで型エラーになる
|
||||
//private repositoryForHour: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>;
|
||||
//private repositoryForDay: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>;
|
||||
private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
|
||||
private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
|
||||
|
||||
//private repositoryForHour: MiAndOrmRepository<RawRecord<T>>;
|
||||
//private repositoryForDay: MiAndOrmRepository<RawRecord<T>>;
|
||||
protected repositoryForHour: MiAndOrmRepository<{ id: number; group?: string | null; date: number; }>;
|
||||
protected repositoryForDay: MiAndOrmRepository<{ id: number; group?: string | null; date: number; }>;
|
||||
/**
|
||||
* 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
|
||||
*/
|
||||
|
@ -186,11 +187,11 @@ export default abstract class Chart<T extends Schema> {
|
|||
return columns;
|
||||
}
|
||||
|
||||
private static dateToTimestamp(x: Date): number {
|
||||
protected static dateToTimestamp(x: Date): number {
|
||||
return Math.floor(x.getTime() / 1000);
|
||||
}
|
||||
|
||||
private static parseDate(date: Date): [number, number, number, number, number, number, number] {
|
||||
protected static parseDate(date: Date): [number, number, number, number, number, number, number] {
|
||||
const y = date.getUTCFullYear();
|
||||
const m = date.getUTCMonth();
|
||||
const d = date.getUTCDate();
|
||||
|
@ -202,7 +203,7 @@ export default abstract class Chart<T extends Schema> {
|
|||
return [y, m, d, h, _m, _s, _ms];
|
||||
}
|
||||
|
||||
private static getCurrentDate() {
|
||||
protected static getCurrentDate() {
|
||||
return Chart.parseDate(new Date());
|
||||
}
|
||||
|
||||
|
@ -276,8 +277,8 @@ export default abstract class Chart<T extends Schema> {
|
|||
this.logger = logger;
|
||||
|
||||
const { hour, day } = Chart.schemaToEntity(name, schema, grouped);
|
||||
this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
|
||||
this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
|
||||
this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour).extend(miRepository);
|
||||
this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -130,6 +130,7 @@ export class MetaEntityService {
|
|||
enableUrlPreview: instance.urlPreviewEnabled,
|
||||
noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
|
||||
maxFileSize: this.config.maxFileSize,
|
||||
preferPopularUserFactor: instance.preferPopularUserFactor,
|
||||
};
|
||||
|
||||
return packed;
|
||||
|
|
|
@ -625,4 +625,10 @@ export class MiMeta {
|
|||
nullable: true,
|
||||
})
|
||||
public urlPreviewUserAgent: string | null;
|
||||
|
||||
@Column('enum', {
|
||||
enum: ['follower', 'pv', 'none'],
|
||||
default: 'follower',
|
||||
})
|
||||
public preferPopularUserFactor: 'follower' | 'pv' | 'none';
|
||||
}
|
||||
|
|
|
@ -211,6 +211,11 @@ export const packedMetaLiteSchema = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
preferPopularUserFactor: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['follower', 'pv', 'none'],
|
||||
},
|
||||
backgroundImageUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
|
|
|
@ -491,6 +491,11 @@ export const meta = {
|
|||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
preferPopularUserFactor: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['follower', 'pv', 'none'],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@ -625,6 +630,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
|
||||
urlPreviewUserAgent: instance.urlPreviewUserAgent,
|
||||
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
|
||||
preferPopularUserFactor: instance.preferPopularUserFactor,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -167,6 +167,10 @@ export const paramDef = {
|
|||
urlPreviewRequireContentLength: { type: 'boolean' },
|
||||
urlPreviewUserAgent: { type: 'string', nullable: true },
|
||||
urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
|
||||
preferPopularUserFactor: {
|
||||
type: 'string',
|
||||
enum: ['follower', 'pv', 'none'],
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
@ -632,6 +636,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
|
||||
}
|
||||
|
||||
if (ps.preferPopularUserFactor !== undefined) {
|
||||
set.preferPopularUserFactor = ps.preferPopularUserFactor;
|
||||
}
|
||||
|
||||
const before = await this.metaService.fetch(true);
|
||||
|
||||
await this.metaService.update(set);
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
@ -31,7 +32,7 @@ export const paramDef = {
|
|||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
offset: { type: 'integer', default: 0 },
|
||||
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
|
||||
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt', '+pv', '-pv'] },
|
||||
state: { type: 'string', enum: ['all', 'alive'], default: 'all' },
|
||||
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
|
||||
hostname: {
|
||||
|
@ -49,6 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
private perUserPvChart: PerUserPvChart,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private queryService: QueryService,
|
||||
|
@ -71,6 +73,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() });
|
||||
}
|
||||
|
||||
let pvRankedUsers: { userId: string; count: number; }[] | undefined = undefined;
|
||||
if (ps.sort?.endsWith('pv')) {
|
||||
// 直近12時間のPVランキングを取得
|
||||
pvRankedUsers = await this.perUserPvChart.getUsersRanking(
|
||||
'hour', ps.sort.startsWith('+') ? 'DESC' : 'ASC',
|
||||
12, null, ps.limit, ps.offset,
|
||||
);
|
||||
}
|
||||
|
||||
switch (ps.sort) {
|
||||
case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
|
||||
case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
|
||||
|
@ -78,6 +89,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
case '-createdAt': query.orderBy('user.id', 'ASC'); break;
|
||||
case '+updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'DESC'); break;
|
||||
case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); break;
|
||||
case '+pv': query.andWhere((pvRankedUsers?.length ?? 0) > 0 ? 'user.id IN (:...userIds)' : '1 = 0', { userIds: pvRankedUsers?.map(user => user.userId) ?? [] }); break;
|
||||
case '-pv': query.andWhere((pvRankedUsers?.length ?? 0) > 0 ? 'user.id IN (:...userIds)' : '1 = 0', { userIds: pvRankedUsers?.map(user => user.userId) ?? [] }); break;
|
||||
default: query.orderBy('user.id', 'ASC'); break;
|
||||
}
|
||||
|
||||
|
@ -88,6 +101,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
query.offset(ps.offset);
|
||||
|
||||
const users = await query.getMany();
|
||||
if (ps.sort === '+pv') {
|
||||
users.sort((a, b) => {
|
||||
const aPv = pvRankedUsers?.find(u => u.userId === a.id)?.count ?? 0;
|
||||
const bPv = pvRankedUsers?.find(u => u.userId === b.id)?.count ?? 0;
|
||||
return bPv - aPv;
|
||||
});
|
||||
} else if (ps.sort === '-pv') {
|
||||
users.sort((a, b) => {
|
||||
const aPv = pvRankedUsers?.find(u => u.userId === a.id)?.count ?? 0;
|
||||
const bPv = pvRankedUsers?.find(u => u.userId === b.id)?.count ?? 0;
|
||||
return aPv - bPv;
|
||||
});
|
||||
}
|
||||
|
||||
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
|
||||
});
|
||||
|
|
|
@ -53,7 +53,7 @@ const popularUsers: Paging = {
|
|||
params: {
|
||||
state: 'alive',
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
sort: '+pv',
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -50,10 +50,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #caption>{{ i18n.ts.impressumDescription }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkTextarea v-model="pinnedUsers">
|
||||
<template #label>{{ i18n.ts.pinnedUsers }}</template>
|
||||
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
|
||||
</MkTextarea>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.explore }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkTextarea v-model="pinnedUsers">
|
||||
<template #label>{{ i18n.ts.pinnedUsers }}</template>
|
||||
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkRadios v-model="preferPopularUserFactor">
|
||||
<template #label>{{ i18n.ts.preferPopularUserFactor }}</template>
|
||||
<template #caption>{{ i18n.ts.preferPopularUserFactorDescription }}</template>
|
||||
<option value="follower">{{ i18n.ts.followersCount }}</option>
|
||||
<option value="pv">{{ i18n.ts.pageViewCount }}</option>
|
||||
<option value="none">{{ i18n.ts.disabled }}</option>
|
||||
</MkRadios>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.files }}</template>
|
||||
|
@ -222,6 +236,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import { entities as MisskeyEntities } from 'misskey-js';
|
||||
|
||||
const name = ref<string | null>(null);
|
||||
const shortName = ref<string | null>(null);
|
||||
|
@ -249,6 +265,7 @@ const urlPreviewMaximumContentLength = ref<number>(1024 * 1024 * 10);
|
|||
const urlPreviewRequireContentLength = ref<boolean>(true);
|
||||
const urlPreviewUserAgent = ref<string | null>(null);
|
||||
const urlPreviewSummaryProxyUrl = ref<string | null>(null);
|
||||
const preferPopularUserFactor = ref<MisskeyEntities.MetaLite['preferPopularUserFactor']>('follower');
|
||||
|
||||
async function init(): Promise<void> {
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
|
@ -278,6 +295,7 @@ async function init(): Promise<void> {
|
|||
urlPreviewRequireContentLength.value = meta.urlPreviewRequireContentLength;
|
||||
urlPreviewUserAgent.value = meta.urlPreviewUserAgent;
|
||||
urlPreviewSummaryProxyUrl.value = meta.urlPreviewSummaryProxyUrl;
|
||||
preferPopularUserFactor.value = meta.preferPopularUserFactor;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
|
@ -308,6 +326,7 @@ async function save() {
|
|||
urlPreviewRequireContentLength: urlPreviewRequireContentLength.value,
|
||||
urlPreviewUserAgent: urlPreviewUserAgent.value,
|
||||
urlPreviewSummaryProxyUrl: urlPreviewSummaryProxyUrl.value,
|
||||
preferPopularUserFactor: preferPopularUserFactor.value,
|
||||
});
|
||||
|
||||
fetchInstance(true);
|
||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #header><i class="ti ti-bookmark ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template>
|
||||
<MkUserList :pagination="pinnedUsers"/>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection class="_margin" persistKey="explore-popular-users">
|
||||
<MkFoldableSection v-if="instance.preferPopularUserFactor !== 'none'" class="_margin" persistKey="explore-popular-users">
|
||||
<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
||||
<MkUserList :pagination="popularUsers"/>
|
||||
</MkFoldableSection>
|
||||
|
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkFoldableSection>
|
||||
|
||||
<template v-if="tag == null">
|
||||
<MkFoldableSection class="_margin">
|
||||
<MkFoldableSection v-if="instance.preferPopularUserFactor !== 'none'" class="_margin">
|
||||
<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
||||
<MkUserList :pagination="popularUsersF"/>
|
||||
</MkFoldableSection>
|
||||
|
@ -69,7 +69,9 @@ import MkUserList from '@/components/MkUserList.vue';
|
|||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string;
|
||||
|
@ -94,34 +96,47 @@ const tagUsers = computed(() => ({
|
|||
},
|
||||
}));
|
||||
|
||||
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
|
||||
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
function createUserQueryPaging(params: Misskey.Endpoints['users']['req']): Paging {
|
||||
return {
|
||||
endpoint: 'users',
|
||||
limit: 10,
|
||||
noPaging: true,
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
const pinnedUsers = {
|
||||
endpoint: 'pinned-users',
|
||||
noPaging: true,
|
||||
limit: 10,
|
||||
} satisfies Paging;
|
||||
const popularUsers = createUserQueryPaging({
|
||||
state: 'alive',
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
} };
|
||||
const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
sort: instance.preferPopularUserFactor === 'pv' ? '+pv' : '+follower',
|
||||
});
|
||||
const recentlyUpdatedUsers = createUserQueryPaging({
|
||||
origin: 'local',
|
||||
sort: '+updatedAt',
|
||||
} };
|
||||
const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
});
|
||||
const recentlyRegisteredUsers = createUserQueryPaging({
|
||||
origin: 'local',
|
||||
state: 'alive',
|
||||
sort: '+createdAt',
|
||||
} };
|
||||
const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
});
|
||||
const popularUsersF = createUserQueryPaging({
|
||||
state: 'alive',
|
||||
origin: 'remote',
|
||||
sort: '+follower',
|
||||
} };
|
||||
const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
sort: '+follower', // リモートのpvは信用ならない
|
||||
});
|
||||
const recentlyUpdatedUsersF = createUserQueryPaging({
|
||||
origin: 'combined',
|
||||
sort: '+updatedAt',
|
||||
} };
|
||||
const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
||||
});
|
||||
const recentlyRegisteredUsersF = createUserQueryPaging({
|
||||
origin: 'combined',
|
||||
sort: '+createdAt',
|
||||
} };
|
||||
});
|
||||
|
||||
misskeyApi('hashtags/list', {
|
||||
sort: '+attachedLocalUsers',
|
||||
|
|
|
@ -30,6 +30,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkContainer>
|
||||
|
||||
<MkContainer :foldable="true" :expanded="false">
|
||||
<template #header>{{ i18n.ts.uiInspector }}</template>
|
||||
<div :class="$style.uiInspector">
|
||||
<div v-for="c in components" :key="c.value.id">
|
||||
<div :class="$style.uiInspectorType">{{ c.value.type }}</div>
|
||||
<div :class="$style.uiInspectorId">{{ c.value.id }}</div>
|
||||
<button :class="$style.uiInspectorPropsToggle" @click="() => uiInspectorOpenedComponents.set(c, !uiInspectorOpenedComponents.get(c))">
|
||||
<i v-if="uiInspectorOpenedComponents.get(c)" class="ti ti-chevron-up icon"></i>
|
||||
<i v-else class="ti ti-chevron-down icon"></i>
|
||||
</button>
|
||||
<div v-if="uiInspectorOpenedComponents.get(c)">
|
||||
<MkTextarea :modelValue="stringifyUiProps(c.value)" code readonly></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.uiInspectorDescription">{{ i18n.ts.uiInspectorDescription }}</div>
|
||||
</div>
|
||||
</MkContainer>
|
||||
|
||||
<div class="">
|
||||
{{ i18n.ts.scratchpadDescription }}
|
||||
</div>
|
||||
|
@ -43,6 +61,7 @@ import { onDeactivated, onUnmounted, Ref, ref, watch, computed } from 'vue';
|
|||
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||
import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -61,6 +80,7 @@ const logs = ref<any[]>([]);
|
|||
const root = ref<AsUiRoot>();
|
||||
const components = ref<Ref<AsUiComponent>[]>([]);
|
||||
const uiKey = ref(0);
|
||||
const uiInspectorOpenedComponents = ref(new Map<string, boolean>);
|
||||
|
||||
const saved = miLocalStorage.getItem('scratchpad');
|
||||
if (saved) {
|
||||
|
@ -71,6 +91,14 @@ watch(code, () => {
|
|||
miLocalStorage.setItem('scratchpad', code.value);
|
||||
});
|
||||
|
||||
function stringifyUiProps(uiProps) {
|
||||
return JSON.stringify(
|
||||
{ ...uiProps, type: undefined, id: undefined },
|
||||
(k, v) => typeof v === 'function' ? '<function>' : v,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
if (aiscript) aiscript.abort();
|
||||
root.value = undefined;
|
||||
|
@ -192,4 +220,35 @@ definePageMetadata(() => ({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uiInspector {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.uiInspectorType {
|
||||
display: inline-block;
|
||||
border: hidden;
|
||||
border-radius: 10px;
|
||||
background-color: var(--panelHighlight);
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.uiInspectorId {
|
||||
display: inline-block;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.uiInspectorDescription {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.uiInspectorPropsToggle {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -258,7 +258,7 @@ import { langs } from '@@/js/config.js';
|
|||
import { defaultStore } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import { reloadAsk } from '@/scripts/reload-ask.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
|
@ -270,16 +270,6 @@ const fontSize = ref(miLocalStorage.getItem('fontSize'));
|
|||
const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
|
||||
const dataSaver = ref(defaultStore.state.dataSaver);
|
||||
|
||||
async function reloadAsk() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: i18n.ts.reloadToApplySetting,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
const hemisphere = computed(defaultStore.makeGetterSetter('hemisphere'));
|
||||
const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
|
||||
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
|
||||
|
@ -369,7 +359,7 @@ watch([
|
|||
confirmWhenRevealingSensitiveMedia,
|
||||
contextMenu,
|
||||
], async () => {
|
||||
await reloadAsk();
|
||||
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
|
||||
});
|
||||
|
||||
const emojiIndexLangs = ['en-US', 'ja-JP', 'ja-JP_hira'] as const;
|
||||
|
|
|
@ -54,7 +54,7 @@ import MkContainer from '@/components/MkContainer.vue';
|
|||
import * as os from '@/os.js';
|
||||
import { navbarItemDef } from '@/navbar.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import { reloadAsk } from '@/scripts/reload-ask.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
|
@ -67,16 +67,6 @@ const items = ref(defaultStore.state.menu.map(x => ({
|
|||
|
||||
const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
|
||||
|
||||
async function reloadAsk() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: i18n.ts.reloadToApplySetting,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
async function addItem() {
|
||||
const menu = Object.keys(navbarItemDef).filter(k => !defaultStore.state.menu.includes(k));
|
||||
const { canceled, result: item } = await os.select({
|
||||
|
@ -100,7 +90,7 @@ function removeItem(index: number) {
|
|||
|
||||
async function save() {
|
||||
defaultStore.set('menu', items.value.map(x => x.type));
|
||||
await reloadAsk();
|
||||
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
|
||||
}
|
||||
|
||||
function reset() {
|
||||
|
@ -111,7 +101,7 @@ function reset() {
|
|||
}
|
||||
|
||||
watch(menuDisplay, async () => {
|
||||
await reloadAsk();
|
||||
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
|
||||
});
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
|
|
@ -98,7 +98,7 @@ import { defaultStore } from '@/store.js';
|
|||
import { signout, signinRequired } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import { reloadAsk } from '@/scripts/reload-ask.js';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
@ -132,16 +132,6 @@ async function deleteAccount() {
|
|||
await signout();
|
||||
}
|
||||
|
||||
async function reloadAsk() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: i18n.ts.reloadToApplySetting,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
async function updateRepliesAll(withReplies: boolean) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
|
@ -155,7 +145,7 @@ async function updateRepliesAll(withReplies: boolean) {
|
|||
watch([
|
||||
enableCondensedLineForAcct,
|
||||
], async () => {
|
||||
await reloadAsk();
|
||||
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
|
||||
});
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
|
|
@ -88,19 +88,9 @@ import { uniqueBy } from '@/scripts/array.js';
|
|||
import { fetchThemes, getThemes } from '@/theme-store.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import { reloadAsk } from '@/scripts/reload-ask.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
async function reloadAsk() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'info',
|
||||
text: i18n.ts.reloadToApplySetting,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
const installedThemes = ref(getThemes());
|
||||
const builtinThemes = getBuiltinThemesRef();
|
||||
|
||||
|
@ -148,13 +138,13 @@ watch(syncDeviceDarkMode, () => {
|
|||
}
|
||||
});
|
||||
|
||||
watch(wallpaper, () => {
|
||||
watch(wallpaper, async () => {
|
||||
if (wallpaper.value == null) {
|
||||
miLocalStorage.removeItem('wallpaper');
|
||||
} else {
|
||||
miLocalStorage.setItem('wallpaper', wallpaper.value);
|
||||
}
|
||||
reloadAsk();
|
||||
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
|
||||
let isReloadConfirming = false;
|
||||
|
||||
export async function reloadAsk(opts: {
|
||||
unison?: boolean;
|
||||
reason?: string;
|
||||
}) {
|
||||
if (isReloadConfirming) {
|
||||
return;
|
||||
}
|
||||
|
||||
isReloadConfirming = true;
|
||||
|
||||
const { canceled } = await os.confirm(opts.reason == null ? {
|
||||
type: 'info',
|
||||
text: i18n.ts.reloadConfirm,
|
||||
} : {
|
||||
type: 'info',
|
||||
title: i18n.ts.reloadConfirm,
|
||||
text: opts.reason,
|
||||
}).finally(() => {
|
||||
isReloadConfirming = false;
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
if (opts.unison) {
|
||||
unisonReload();
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}
|
|
@ -4935,6 +4935,8 @@ export type components = {
|
|||
translatorAvailable: boolean;
|
||||
mediaProxy: string;
|
||||
enableUrlPreview: boolean;
|
||||
/** @enum {string} */
|
||||
preferPopularUserFactor: 'follower' | 'pv' | 'none';
|
||||
backgroundImageUrl: string | null;
|
||||
impressumUrl: string | null;
|
||||
logoImageUrl: string | null;
|
||||
|
@ -5137,6 +5139,8 @@ export type operations = {
|
|||
urlPreviewRequireContentLength: boolean;
|
||||
urlPreviewUserAgent: string | null;
|
||||
urlPreviewSummaryProxyUrl: string | null;
|
||||
/** @enum {string} */
|
||||
preferPopularUserFactor: 'follower' | 'pv' | 'none';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -9386,6 +9390,8 @@ export type operations = {
|
|||
urlPreviewRequireContentLength?: boolean;
|
||||
urlPreviewUserAgent?: string | null;
|
||||
urlPreviewSummaryProxyUrl?: string | null;
|
||||
/** @enum {string} */
|
||||
preferPopularUserFactor?: 'follower' | 'pv' | 'none';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -25005,7 +25011,7 @@ export type operations = {
|
|||
/** @default 0 */
|
||||
offset?: number;
|
||||
/** @enum {string} */
|
||||
sort?: '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt';
|
||||
sort?: '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt' | '+pv' | '-pv';
|
||||
/**
|
||||
* @default all
|
||||
* @enum {string}
|
||||
|
|
Loading…
Reference in New Issue