Merge 4ad11a238d
into 3bf63dd9c5
This commit is contained in:
commit
723fd17335
|
@ -1,7 +1,8 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### General
|
### 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
|
### Client
|
||||||
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
|
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
|
||||||
|
|
|
@ -5084,6 +5084,18 @@ export interface Locale extends ILocale {
|
||||||
* これ以上このクリップにノートを追加できません。
|
* これ以上このクリップにノートを追加できません。
|
||||||
*/
|
*/
|
||||||
"clipNoteLimitExceeded": string;
|
"clipNoteLimitExceeded": string;
|
||||||
|
/**
|
||||||
|
* ページ閲覧数
|
||||||
|
*/
|
||||||
|
"pageViewCount": string;
|
||||||
|
/**
|
||||||
|
* 人気のユーザーの算出基準
|
||||||
|
*/
|
||||||
|
"preferPopularUserFactor": string;
|
||||||
|
/**
|
||||||
|
* ページ閲覧数はローカルユーザーにのみ適用されます(リモートユーザーはフォロワー数で表示されます)。「無効」に設定すると、ローカル・リモートどちらの「人気のユーザー」セクションも表示されなくなります。
|
||||||
|
*/
|
||||||
|
"preferPopularUserFactorDescription": string;
|
||||||
"_delivery": {
|
"_delivery": {
|
||||||
/**
|
/**
|
||||||
* 配信状態
|
* 配信状態
|
||||||
|
|
|
@ -1269,6 +1269,9 @@ fromX: "{x}から"
|
||||||
genEmbedCode: "埋め込みコードを生成"
|
genEmbedCode: "埋め込みコードを生成"
|
||||||
noteOfThisUser: "このユーザーのノート一覧"
|
noteOfThisUser: "このユーザーのノート一覧"
|
||||||
clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
|
clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
|
||||||
|
pageViewCount: "ページ閲覧数"
|
||||||
|
preferPopularUserFactor: "人気のユーザーの算出基準"
|
||||||
|
preferPopularUserFactorDescription: "ページ閲覧数はローカルユーザーにのみ適用されます(リモートユーザーはフォロワー数で表示されます)。「無効」に設定すると、ローカル・リモートどちらの「人気のユーザー」セクションも表示されなくなります。"
|
||||||
|
|
||||||
_delivery:
|
_delivery:
|
||||||
status: "配信状態"
|
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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import { AppLockService } from '@/core/AppLockService.js';
|
||||||
|
import { addTime, dateUTC, subtractTime } from '@/misc/prelude/time.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import Chart from '../core.js';
|
import Chart from '../core.js';
|
||||||
|
@ -52,4 +53,32 @@ export default class PerUserPvChart extends Chart<typeof schema> { // eslint-dis
|
||||||
'pv.visitor': 1,
|
'pv.visitor': 1,
|
||||||
}, user.id);
|
}, 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 type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MiRepository, miRepository } from '@/models/_.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 COLUMN_PREFIX = '___' as const;
|
||||||
const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
|
const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
|
||||||
|
@ -94,6 +94,8 @@ type ToJsonSchema<S> = {
|
||||||
required: (keyof 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>>> {
|
export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatten<ChartResult<S>>> {
|
||||||
const unflatten = (str: string, parent: Record<string, any>) => {
|
const unflatten = (str: string, parent: Record<string, any>) => {
|
||||||
const keys = str.split('.');
|
const keys = str.split('.');
|
||||||
|
@ -146,11 +148,10 @@ export default abstract class Chart<T extends Schema> {
|
||||||
group: string | null;
|
group: string | null;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
// ↓にしたいけどfindOneとかで型エラーになる
|
// ↓にしたいけどfindOneとかで型エラーになる
|
||||||
//private repositoryForHour: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>;
|
//private repositoryForHour: MiAndOrmRepository<RawRecord<T>>;
|
||||||
//private repositoryForDay: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>;
|
//private repositoryForDay: MiAndOrmRepository<RawRecord<T>>;
|
||||||
private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
|
protected repositoryForHour: MiAndOrmRepository<{ 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; }>;
|
protected repositoryForDay: MiAndOrmRepository<{ id: number; group?: string | null; date: number; }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
|
* 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
|
||||||
*/
|
*/
|
||||||
|
@ -186,11 +187,11 @@ export default abstract class Chart<T extends Schema> {
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static dateToTimestamp(x: Date): number {
|
protected static dateToTimestamp(x: Date): number {
|
||||||
return Math.floor(x.getTime() / 1000);
|
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 y = date.getUTCFullYear();
|
||||||
const m = date.getUTCMonth();
|
const m = date.getUTCMonth();
|
||||||
const d = date.getUTCDate();
|
const d = date.getUTCDate();
|
||||||
|
@ -202,7 +203,7 @@ export default abstract class Chart<T extends Schema> {
|
||||||
return [y, m, d, h, _m, _s, _ms];
|
return [y, m, d, h, _m, _s, _ms];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getCurrentDate() {
|
protected static getCurrentDate() {
|
||||||
return Chart.parseDate(new Date());
|
return Chart.parseDate(new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,8 +277,8 @@ export default abstract class Chart<T extends Schema> {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
|
||||||
const { hour, day } = Chart.schemaToEntity(name, schema, grouped);
|
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.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 as MiRepository<{ id: number; group?: string | null; date: number; }>);
|
this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -130,6 +130,7 @@ export class MetaEntityService {
|
||||||
enableUrlPreview: instance.urlPreviewEnabled,
|
enableUrlPreview: instance.urlPreviewEnabled,
|
||||||
noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
|
noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
|
||||||
maxFileSize: this.config.maxFileSize,
|
maxFileSize: this.config.maxFileSize,
|
||||||
|
preferPopularUserFactor: instance.preferPopularUserFactor,
|
||||||
};
|
};
|
||||||
|
|
||||||
return packed;
|
return packed;
|
||||||
|
|
|
@ -625,4 +625,10 @@ export class MiMeta {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
public urlPreviewUserAgent: string | null;
|
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',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
preferPopularUserFactor: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['follower', 'pv', 'none'],
|
||||||
|
},
|
||||||
backgroundImageUrl: {
|
backgroundImageUrl: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
|
|
@ -491,6 +491,11 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
preferPopularUserFactor: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['follower', 'pv', 'none'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -625,6 +630,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
|
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
|
||||||
urlPreviewUserAgent: instance.urlPreviewUserAgent,
|
urlPreviewUserAgent: instance.urlPreviewUserAgent,
|
||||||
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
|
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
|
||||||
|
preferPopularUserFactor: instance.preferPopularUserFactor,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,10 @@ export const paramDef = {
|
||||||
urlPreviewRequireContentLength: { type: 'boolean' },
|
urlPreviewRequireContentLength: { type: 'boolean' },
|
||||||
urlPreviewUserAgent: { type: 'string', nullable: true },
|
urlPreviewUserAgent: { type: 'string', nullable: true },
|
||||||
urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
|
urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
|
||||||
|
preferPopularUserFactor: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['follower', 'pv', 'none'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -632,6 +636,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
|
set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.preferPopularUserFactor !== undefined) {
|
||||||
|
set.preferPopularUserFactor = ps.preferPopularUserFactor;
|
||||||
|
}
|
||||||
|
|
||||||
const before = await this.metaService.fetch(true);
|
const before = await this.metaService.fetch(true);
|
||||||
|
|
||||||
await this.metaService.update(set);
|
await this.metaService.update(set);
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UsersRepository } from '@/models/_.js';
|
import type { UsersRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.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 { QueryService } from '@/core/QueryService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
@ -31,7 +32,7 @@ export const paramDef = {
|
||||||
properties: {
|
properties: {
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
offset: { type: 'integer', default: 0 },
|
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' },
|
state: { type: 'string', enum: ['all', 'alive'], default: 'all' },
|
||||||
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
|
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
|
||||||
hostname: {
|
hostname: {
|
||||||
|
@ -49,6 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
private perUserPvChart: PerUserPvChart,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private queryService: QueryService,
|
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() });
|
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) {
|
switch (ps.sort) {
|
||||||
case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
|
case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
|
||||||
case '-follower': query.orderBy('user.followersCount', 'ASC'); 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 '-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', 'DESC'); break;
|
||||||
case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); 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;
|
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);
|
query.offset(ps.offset);
|
||||||
|
|
||||||
const users = await query.getMany();
|
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' });
|
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
|
||||||
});
|
});
|
||||||
|
|
|
@ -53,7 +53,7 @@ const popularUsers: Paging = {
|
||||||
params: {
|
params: {
|
||||||
state: 'alive',
|
state: 'alive',
|
||||||
origin: 'local',
|
origin: 'local',
|
||||||
sort: '+follower',
|
sort: '+pv',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -50,11 +50,25 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #caption>{{ i18n.ts.impressumDescription }}</template>
|
<template #caption>{{ i18n.ts.impressumDescription }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
|
<FormSection>
|
||||||
|
<template #label>{{ i18n.ts.explore }}</template>
|
||||||
|
|
||||||
|
<div class="_gaps_m">
|
||||||
<MkTextarea v-model="pinnedUsers">
|
<MkTextarea v-model="pinnedUsers">
|
||||||
<template #label>{{ i18n.ts.pinnedUsers }}</template>
|
<template #label>{{ i18n.ts.pinnedUsers }}</template>
|
||||||
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
|
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
|
||||||
</MkTextarea>
|
</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>
|
<FormSection>
|
||||||
<template #label>{{ i18n.ts.files }}</template>
|
<template #label>{{ i18n.ts.files }}</template>
|
||||||
|
|
||||||
|
@ -222,6 +236,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkSelect from '@/components/MkSelect.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 name = ref<string | null>(null);
|
||||||
const shortName = 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 urlPreviewRequireContentLength = ref<boolean>(true);
|
||||||
const urlPreviewUserAgent = ref<string | null>(null);
|
const urlPreviewUserAgent = ref<string | null>(null);
|
||||||
const urlPreviewSummaryProxyUrl = ref<string | null>(null);
|
const urlPreviewSummaryProxyUrl = ref<string | null>(null);
|
||||||
|
const preferPopularUserFactor = ref<MisskeyEntities.MetaLite['preferPopularUserFactor']>('follower');
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
async function init(): Promise<void> {
|
||||||
const meta = await misskeyApi('admin/meta');
|
const meta = await misskeyApi('admin/meta');
|
||||||
|
@ -278,6 +295,7 @@ async function init(): Promise<void> {
|
||||||
urlPreviewRequireContentLength.value = meta.urlPreviewRequireContentLength;
|
urlPreviewRequireContentLength.value = meta.urlPreviewRequireContentLength;
|
||||||
urlPreviewUserAgent.value = meta.urlPreviewUserAgent;
|
urlPreviewUserAgent.value = meta.urlPreviewUserAgent;
|
||||||
urlPreviewSummaryProxyUrl.value = meta.urlPreviewSummaryProxyUrl;
|
urlPreviewSummaryProxyUrl.value = meta.urlPreviewSummaryProxyUrl;
|
||||||
|
preferPopularUserFactor.value = meta.preferPopularUserFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
@ -308,6 +326,7 @@ async function save() {
|
||||||
urlPreviewRequireContentLength: urlPreviewRequireContentLength.value,
|
urlPreviewRequireContentLength: urlPreviewRequireContentLength.value,
|
||||||
urlPreviewUserAgent: urlPreviewUserAgent.value,
|
urlPreviewUserAgent: urlPreviewUserAgent.value,
|
||||||
urlPreviewSummaryProxyUrl: urlPreviewSummaryProxyUrl.value,
|
urlPreviewSummaryProxyUrl: urlPreviewSummaryProxyUrl.value,
|
||||||
|
preferPopularUserFactor: preferPopularUserFactor.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchInstance(true);
|
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>
|
<template #header><i class="ti ti-bookmark ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template>
|
||||||
<MkUserList :pagination="pinnedUsers"/>
|
<MkUserList :pagination="pinnedUsers"/>
|
||||||
</MkFoldableSection>
|
</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>
|
<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
||||||
<MkUserList :pagination="popularUsers"/>
|
<MkUserList :pagination="popularUsers"/>
|
||||||
</MkFoldableSection>
|
</MkFoldableSection>
|
||||||
|
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkFoldableSection>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<template v-if="tag == null">
|
<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>
|
<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
||||||
<MkUserList :pagination="popularUsersF"/>
|
<MkUserList :pagination="popularUsersF"/>
|
||||||
</MkFoldableSection>
|
</MkFoldableSection>
|
||||||
|
@ -69,7 +69,9 @@ import MkUserList from '@/components/MkUserList.vue';
|
||||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import MkTab from '@/components/MkTab.vue';
|
import MkTab from '@/components/MkTab.vue';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
|
import { instance } from '@/instance.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import type { Paging } from '@/components/MkPagination.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tag?: string;
|
tag?: string;
|
||||||
|
@ -94,34 +96,47 @@ const tagUsers = computed(() => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
|
function createUserQueryPaging(params: Misskey.Endpoints['users']['req']): Paging {
|
||||||
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
return {
|
||||||
|
endpoint: 'users',
|
||||||
|
limit: 10,
|
||||||
|
noPaging: true,
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinnedUsers = {
|
||||||
|
endpoint: 'pinned-users',
|
||||||
|
noPaging: true,
|
||||||
|
limit: 10,
|
||||||
|
} satisfies Paging;
|
||||||
|
const popularUsers = createUserQueryPaging({
|
||||||
state: 'alive',
|
state: 'alive',
|
||||||
origin: 'local',
|
origin: 'local',
|
||||||
sort: '+follower',
|
sort: instance.preferPopularUserFactor === 'pv' ? '+pv' : '+follower',
|
||||||
} };
|
});
|
||||||
const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
const recentlyUpdatedUsers = createUserQueryPaging({
|
||||||
origin: 'local',
|
origin: 'local',
|
||||||
sort: '+updatedAt',
|
sort: '+updatedAt',
|
||||||
} };
|
});
|
||||||
const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
const recentlyRegisteredUsers = createUserQueryPaging({
|
||||||
origin: 'local',
|
origin: 'local',
|
||||||
state: 'alive',
|
state: 'alive',
|
||||||
sort: '+createdAt',
|
sort: '+createdAt',
|
||||||
} };
|
});
|
||||||
const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
const popularUsersF = createUserQueryPaging({
|
||||||
state: 'alive',
|
state: 'alive',
|
||||||
origin: 'remote',
|
origin: 'remote',
|
||||||
sort: '+follower',
|
sort: '+follower', // リモートのpvは信用ならない
|
||||||
} };
|
});
|
||||||
const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
const recentlyUpdatedUsersF = createUserQueryPaging({
|
||||||
origin: 'combined',
|
origin: 'combined',
|
||||||
sort: '+updatedAt',
|
sort: '+updatedAt',
|
||||||
} };
|
});
|
||||||
const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
|
const recentlyRegisteredUsersF = createUserQueryPaging({
|
||||||
origin: 'combined',
|
origin: 'combined',
|
||||||
sort: '+createdAt',
|
sort: '+createdAt',
|
||||||
} };
|
});
|
||||||
|
|
||||||
misskeyApi('hashtags/list', {
|
misskeyApi('hashtags/list', {
|
||||||
sort: '+attachedLocalUsers',
|
sort: '+attachedLocalUsers',
|
||||||
|
|
|
@ -4935,6 +4935,8 @@ export type components = {
|
||||||
translatorAvailable: boolean;
|
translatorAvailable: boolean;
|
||||||
mediaProxy: string;
|
mediaProxy: string;
|
||||||
enableUrlPreview: boolean;
|
enableUrlPreview: boolean;
|
||||||
|
/** @enum {string} */
|
||||||
|
preferPopularUserFactor: 'follower' | 'pv' | 'none';
|
||||||
backgroundImageUrl: string | null;
|
backgroundImageUrl: string | null;
|
||||||
impressumUrl: string | null;
|
impressumUrl: string | null;
|
||||||
logoImageUrl: string | null;
|
logoImageUrl: string | null;
|
||||||
|
@ -5137,6 +5139,8 @@ export type operations = {
|
||||||
urlPreviewRequireContentLength: boolean;
|
urlPreviewRequireContentLength: boolean;
|
||||||
urlPreviewUserAgent: string | null;
|
urlPreviewUserAgent: string | null;
|
||||||
urlPreviewSummaryProxyUrl: string | null;
|
urlPreviewSummaryProxyUrl: string | null;
|
||||||
|
/** @enum {string} */
|
||||||
|
preferPopularUserFactor: 'follower' | 'pv' | 'none';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -9386,6 +9390,8 @@ export type operations = {
|
||||||
urlPreviewRequireContentLength?: boolean;
|
urlPreviewRequireContentLength?: boolean;
|
||||||
urlPreviewUserAgent?: string | null;
|
urlPreviewUserAgent?: string | null;
|
||||||
urlPreviewSummaryProxyUrl?: string | null;
|
urlPreviewSummaryProxyUrl?: string | null;
|
||||||
|
/** @enum {string} */
|
||||||
|
preferPopularUserFactor?: 'follower' | 'pv' | 'none';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -25005,7 +25011,7 @@ export type operations = {
|
||||||
/** @default 0 */
|
/** @default 0 */
|
||||||
offset?: number;
|
offset?: number;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
sort?: '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt';
|
sort?: '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt' | '+pv' | '-pv';
|
||||||
/**
|
/**
|
||||||
* @default all
|
* @default all
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
|
|
Loading…
Reference in New Issue