mirror of
https://github.com/MisskeyIO/misskey
synced 2024-11-23 14:46:40 +09:00
enhance(profile): 相互リンク機能の改修 (MisskeyIO#684)
This commit is contained in:
parent
b6a5a36eaa
commit
5a9d8a5564
68
locales/index.d.ts
vendored
68
locales/index.d.ts
vendored
@ -2401,13 +2401,13 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"unsetUserBannerConfirm": string;
|
||||
/**
|
||||
* 相互バナーを解除
|
||||
* 相互リンクを解除
|
||||
*/
|
||||
"unsetUserMutualBanner": string;
|
||||
"unsetUserMutualLink": string;
|
||||
/**
|
||||
* 相互バナーを解除しますか?
|
||||
* 相互リンクを解除しますか?
|
||||
*/
|
||||
"unsetUserMutualBannerConfirm": string;
|
||||
"unsetUserMutualLinkConfirm": string;
|
||||
/**
|
||||
* すべてのファイルを削除
|
||||
*/
|
||||
@ -5092,9 +5092,9 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"here": string;
|
||||
/**
|
||||
* 相互バナー
|
||||
* 相互リンク
|
||||
*/
|
||||
"mutualBanner": string;
|
||||
"mutualLink": string;
|
||||
/**
|
||||
* このユーザーのバナー
|
||||
*/
|
||||
@ -6884,6 +6884,14 @@ export interface Locale extends ILocale {
|
||||
* アイコンデコレーションの最大取付個数
|
||||
*/
|
||||
"avatarDecorationLimit": string;
|
||||
/**
|
||||
* 相互リンクのセクションの最大数
|
||||
*/
|
||||
"mutualLinkSectionLimit": string;
|
||||
/**
|
||||
* セクション内の相互リンクの最大数
|
||||
*/
|
||||
"mutualLinkLimit": string;
|
||||
};
|
||||
"_condition": {
|
||||
/**
|
||||
@ -8233,9 +8241,9 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"write:admin:unset-user-banner": string;
|
||||
/**
|
||||
* ユーザーの相互バナーを削除する
|
||||
* ユーザーの相互リンクを削除する
|
||||
*/
|
||||
"write:admin:unset-user-mutual-banner": string;
|
||||
"write:admin:unset-user-mutual-link": string;
|
||||
/**
|
||||
* ユーザーの凍結を解除する
|
||||
*/
|
||||
@ -8823,21 +8831,49 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"avatarDecorationMax": ParameterizedString<"max">;
|
||||
/**
|
||||
* 自身の相互リンクのバナーを設定
|
||||
* 相互リンクを編集
|
||||
*/
|
||||
"myMutualBanner": string;
|
||||
/**
|
||||
* あなた自身が相互リンクのバナーとして設定してほしい画像を設定することができます。
|
||||
*/
|
||||
"myMutualBannerDescription": string;
|
||||
"mutualLinksEdit": string;
|
||||
/**
|
||||
* 相互リンクのバナー
|
||||
*/
|
||||
"mutualBanner": string;
|
||||
"mutualLinksBanner": string;
|
||||
/**
|
||||
* 説明
|
||||
*/
|
||||
"mutualBannerDescriptionEdit": string;
|
||||
"mutualLinksDescriptionEdit": string;
|
||||
/**
|
||||
* リンク先のURL
|
||||
*/
|
||||
"mutualLinksUrl": string;
|
||||
/**
|
||||
* このセクションをプロフィールにピン留め
|
||||
*/
|
||||
"mutualLinkPining": string;
|
||||
/**
|
||||
* 相互リンクを設定すると、あなたのプロフィールにバナーが表示されます。
|
||||
*/
|
||||
"mutualLinksDescription": string;
|
||||
/**
|
||||
* 相互リンクを追加
|
||||
*/
|
||||
"addMutualLink": string;
|
||||
/**
|
||||
* セクションを追加
|
||||
*/
|
||||
"addMutualLinkSection": string;
|
||||
/**
|
||||
* セクション名
|
||||
*/
|
||||
"sectionName": string;
|
||||
/**
|
||||
* セクション名を表示しないようにする
|
||||
*/
|
||||
"sectionNameNoneDescription": string;
|
||||
/**
|
||||
* セクション名を表示しない
|
||||
*/
|
||||
"sectionNameNone": string;
|
||||
};
|
||||
"_exportOrImport": {
|
||||
/**
|
||||
|
@ -596,8 +596,8 @@ unsetUserAvatar: "アイコンを解除"
|
||||
unsetUserAvatarConfirm: "アイコンを解除しますか?"
|
||||
unsetUserBanner: "バナーを解除"
|
||||
unsetUserBannerConfirm: "バナーを解除しますか?"
|
||||
unsetUserMutualBanner: "相互バナーを解除"
|
||||
unsetUserMutualBannerConfirm: "相互バナーを解除しますか?"
|
||||
unsetUserMutualLink: "相互リンクを削除"
|
||||
unsetUserMutualLinkConfirm: "相互リンクを削除しますか?"
|
||||
deleteAllFiles: "すべてのファイルを削除"
|
||||
deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
|
||||
removeAllFollowing: "フォローを全解除"
|
||||
@ -1268,7 +1268,7 @@ reportComplete: "通報完了"
|
||||
blockThisUser: "このユーザーをブロックする"
|
||||
muteThisUser: "このユーザーをミュートする"
|
||||
here: "こちら"
|
||||
mutualBanner: "相互バナー"
|
||||
mutualLink: "相互リンク"
|
||||
mutualBannerThisUser: "このユーザーのバナー"
|
||||
maximum: "最大"
|
||||
|
||||
@ -1776,6 +1776,8 @@ _role:
|
||||
canUseDriveFileInSoundSettings: "サウンド設定でドライブのファイルを利用"
|
||||
canUseReaction: "リアクションの利用"
|
||||
avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
|
||||
mutualLinkSectionLimit: "相互リンクのセクションの最大数"
|
||||
mutualLinkLimit: "セクション内の相互リンクの最大数"
|
||||
_condition:
|
||||
roleAssignedTo: "マニュアルロールにアサイン済み"
|
||||
isLocal: "ローカルユーザー"
|
||||
@ -2157,7 +2159,7 @@ _permissions:
|
||||
"write:admin:suspend-user": "ユーザーを凍結する"
|
||||
"write:admin:unset-user-avatar": "ユーザーのアバターを削除する"
|
||||
"write:admin:unset-user-banner": "ユーザーのバーナーを削除する"
|
||||
"write:admin:unset-user-mutual-banner": "ユーザーの相互バナーを削除する"
|
||||
"write:admin:unset-user-mutual-link": "ユーザーの相互リンクを削除する"
|
||||
"write:admin:unsuspend-user": "ユーザーの凍結を解除する"
|
||||
"write:admin:meta": "インスタンスのメタデータを操作する"
|
||||
"write:admin:user-note": "モデレーションノートを操作する"
|
||||
@ -2319,10 +2321,17 @@ _profile:
|
||||
changeBanner: "バナー画像を変更"
|
||||
verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。"
|
||||
avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。"
|
||||
myMutualBanner: "自身の相互リンクのバナーを設定"
|
||||
myMutualBannerDescription: "あなた自身が相互リンクのバナーとして設定してほしい画像を設定することができます。"
|
||||
mutualBanner: "相互リンクのバナー"
|
||||
mutualBannerDescriptionEdit: "説明"
|
||||
mutualLinksEdit: "相互リンクを編集"
|
||||
mutualLinksBanner: "相互リンクのバナー"
|
||||
mutualLinksDescriptionEdit: "説明"
|
||||
mutualLinksUrl: "リンク先のURL"
|
||||
mutualLinkPining: "このセクションをプロフィールにピン留め"
|
||||
mutualLinksDescription: "相互リンクを設定すると、あなたのプロフィールにバナーが表示されます。"
|
||||
addMutualLink: "相互リンクを追加"
|
||||
addMutualLinkSection: "セクションを追加"
|
||||
sectionName: "セクション名"
|
||||
sectionNameNoneDescription: "セクション名を表示しないようにする"
|
||||
sectionNameNone: "セクション名を表示しない"
|
||||
|
||||
_exportOrImport:
|
||||
allNotes: "全てのノート"
|
||||
|
@ -1,27 +0,0 @@
|
||||
export class MutualBanner1723213482131 {
|
||||
name = 'MutualBanner1723213482131'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "user_banner" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "description" character varying(1024), "url" character varying(1024), "fileId" character varying(32) NOT NULL, CONSTRAINT "PK_0d9a418f048e308dbfb6562149d" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_fa06ea2e2375449537ced781f1" ON "user_banner" ("userId") `);
|
||||
await queryRunner.query(`CREATE TABLE "user_banner_pining" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "pinnedBannerId" character varying(32) NOT NULL, CONSTRAINT "PK_970d24f72e8d2b20f8c21ec5d11" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_3b74dc21b68da606011c81609c" ON "user_banner_pining" ("userId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_7d51b5a8ae859e0023a98837a1" ON "user_banner_pining" ("userId", "pinnedBannerId") `);
|
||||
await queryRunner.query(`ALTER TABLE "user_banner" ADD CONSTRAINT "FK_fa06ea2e2375449537ced781f15" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "user_banner" ADD CONSTRAINT "FK_3de9f17cce2c10f6938fb261c0b" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "user_banner_pining" ADD CONSTRAINT "FK_3b74dc21b68da606011c81609c9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "user_banner_pining" ADD CONSTRAINT "FK_d13be8242980f7018d664f780f6" FOREIGN KEY ("pinnedBannerId") REFERENCES "user_banner"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_banner_pining" DROP CONSTRAINT "FK_d13be8242980f7018d664f780f6"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_banner_pining" DROP CONSTRAINT "FK_3b74dc21b68da606011c81609c9"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_banner" DROP CONSTRAINT "FK_3de9f17cce2c10f6938fb261c0b"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_banner" DROP CONSTRAINT "FK_fa06ea2e2375449537ced781f15"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_7d51b5a8ae859e0023a98837a1"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_3b74dc21b68da606011c81609c"`);
|
||||
await queryRunner.query(`DROP TABLE "user_banner_pining"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_fa06ea2e2375449537ced781f1"`);
|
||||
await queryRunner.query(`DROP TABLE "user_banner"`);
|
||||
}
|
||||
}
|
11
packages/backend/migration/1723311628855-mutuallinks.js
Normal file
11
packages/backend/migration/1723311628855-mutuallinks.js
Normal file
@ -0,0 +1,11 @@
|
||||
export class Mutuallinks1723311628855 {
|
||||
name = 'Mutuallinks1723311628855'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutualLinkSections" jsonb NOT NULL DEFAULT '[]'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutualLinkSections"`);
|
||||
}
|
||||
}
|
@ -5,8 +5,6 @@
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
||||
import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js';
|
||||
import { UserBannerPiningEntityService } from '@/core/entities/UserBannerPiningEntityService.js';
|
||||
import { AccountMoveService } from './AccountMoveService.js';
|
||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||
import { AiService } from './AiService.js';
|
||||
@ -37,8 +35,6 @@ import { ModerationLogService } from './ModerationLogService.js';
|
||||
import { NoteCreateService } from './NoteCreateService.js';
|
||||
import { NoteDeleteService } from './NoteDeleteService.js';
|
||||
import { NotePiningService } from './NotePiningService.js';
|
||||
import { UserBannerPiningService } from './UserBannerPiningService.js';
|
||||
import { UserBannerService } from './UserBannerService.js';
|
||||
import { NoteReadService } from './NoteReadService.js';
|
||||
import { NotificationService } from './NotificationService.js';
|
||||
import { PollService } from './PollService.js';
|
||||
@ -177,8 +173,6 @@ const $ModerationLogService: Provider = { provide: 'ModerationLogService', useEx
|
||||
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
|
||||
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
|
||||
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
|
||||
const $UserBannerPiningService: Provider = { provide: 'UserBannerPiningService', useExisting: UserBannerPiningService };
|
||||
const $UserBannerService: Provider = { provide: 'UserBannerService', useExisting: UserBannerService };
|
||||
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
|
||||
const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService };
|
||||
const $PollService: Provider = { provide: 'PollService', useExisting: PollService };
|
||||
@ -259,8 +253,6 @@ const $PageLikeEntityService: Provider = { provide: 'PageLikeEntityService', use
|
||||
const $SigninEntityService: Provider = { provide: 'SigninEntityService', useExisting: SigninEntityService };
|
||||
const $UserEntityService: Provider = { provide: 'UserEntityService', useExisting: UserEntityService };
|
||||
const $UserListEntityService: Provider = { provide: 'UserListEntityService', useExisting: UserListEntityService };
|
||||
const $UserBannerEntityService: Provider = { provide: 'UserBannerEntityService', useExisting: UserBannerEntityService };
|
||||
const $UserBannerPiningEntityService: Provider = { provide: 'UserBannerPiningEntityService', useExisting: UserBannerPiningEntityService };
|
||||
const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService };
|
||||
const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
|
||||
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
|
||||
@ -323,8 +315,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
NoteCreateService,
|
||||
NoteDeleteService,
|
||||
NotePiningService,
|
||||
UserBannerPiningService,
|
||||
UserBannerService,
|
||||
NoteReadService,
|
||||
NotificationService,
|
||||
PollService,
|
||||
@ -403,8 +393,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
SigninEntityService,
|
||||
UserEntityService,
|
||||
UserListEntityService,
|
||||
UserBannerEntityService,
|
||||
UserBannerPiningEntityService,
|
||||
FlashEntityService,
|
||||
FlashLikeEntityService,
|
||||
RoleEntityService,
|
||||
@ -463,8 +451,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$NoteCreateService,
|
||||
$NoteDeleteService,
|
||||
$NotePiningService,
|
||||
$UserBannerService,
|
||||
$UserBannerPiningService,
|
||||
$NoteReadService,
|
||||
$NotificationService,
|
||||
$PollService,
|
||||
@ -543,8 +529,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$SigninEntityService,
|
||||
$UserEntityService,
|
||||
$UserListEntityService,
|
||||
$UserBannerEntityService,
|
||||
$UserBannerPiningEntityService,
|
||||
$FlashEntityService,
|
||||
$FlashLikeEntityService,
|
||||
$RoleEntityService,
|
||||
@ -604,8 +588,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
NoteCreateService,
|
||||
NoteDeleteService,
|
||||
NotePiningService,
|
||||
UserBannerService,
|
||||
UserBannerPiningService,
|
||||
NoteReadService,
|
||||
NotificationService,
|
||||
PollService,
|
||||
@ -683,8 +665,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
SigninEntityService,
|
||||
UserEntityService,
|
||||
UserListEntityService,
|
||||
UserBannerEntityService,
|
||||
UserBannerPiningEntityService,
|
||||
FlashEntityService,
|
||||
FlashLikeEntityService,
|
||||
RoleEntityService,
|
||||
@ -743,8 +723,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$NoteCreateService,
|
||||
$NoteDeleteService,
|
||||
$NotePiningService,
|
||||
$UserBannerService,
|
||||
$UserBannerPiningService,
|
||||
$NoteReadService,
|
||||
$NotificationService,
|
||||
$PollService,
|
||||
@ -822,8 +800,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$SigninEntityService,
|
||||
$UserEntityService,
|
||||
$UserListEntityService,
|
||||
$UserBannerEntityService,
|
||||
$UserBannerPiningEntityService,
|
||||
$FlashEntityService,
|
||||
$FlashLikeEntityService,
|
||||
$RoleEntityService,
|
||||
|
@ -69,6 +69,8 @@ export type RolePolicies = {
|
||||
userEachUserListsLimit: number;
|
||||
rateLimitFactor: number;
|
||||
avatarDecorationLimit: number;
|
||||
mutualLinkSectionLimit: number;
|
||||
mutualLinkLimit: number;
|
||||
};
|
||||
|
||||
export const DEFAULT_POLICIES: RolePolicies = {
|
||||
@ -108,6 +110,8 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
userEachUserListsLimit: 50,
|
||||
rateLimitFactor: 1,
|
||||
avatarDecorationLimit: 1,
|
||||
mutualLinkSectionLimit: 1,
|
||||
mutualLinkLimit: 15,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@ -420,6 +424,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
|
||||
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
|
||||
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
|
||||
mutualLinkSectionLimit: calc('mutualLinkSectionLimit', vs => Math.max(...vs)),
|
||||
mutualLinkLimit: calc('mutualLinkLimit', vs => Math.max(...vs)),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,55 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { MiUserBanner } from '@/models/UserBanner.js';
|
||||
import type { MiUserBannerPining, UserBannerPiningRepository, UserBannerRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserBannerPiningService {
|
||||
constructor(
|
||||
@Inject(DI.userBannerRepository)
|
||||
private userBannerRepository: UserBannerRepository,
|
||||
@Inject(DI.userBannerPiningRepository)
|
||||
private userBannerPiningRepository: UserBannerPiningRepository,
|
||||
|
||||
private idService: IdService,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定したユーザーのバナーをピン留めします
|
||||
* @param userId
|
||||
* @param bannerIds
|
||||
*/
|
||||
public async addPinned(userId: MiUser['id'], bannerIds: MiUserBanner['id'][]) {
|
||||
const pinsToInsert = bannerIds.map(bannerId => ({
|
||||
id: this.idService.gen(),
|
||||
userId,
|
||||
pinnedBannerId: bannerId,
|
||||
} as MiUserBannerPining));
|
||||
await this.userBannerPiningRepository
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.values(pinsToInsert)
|
||||
.orIgnore()
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定したユーザーのバナーのピン留めを解除します
|
||||
* @param userId
|
||||
* @param bannerIds
|
||||
*/
|
||||
@bindThis
|
||||
public async removePinned(userId:MiUser['id'], bannerIds:MiUserBanner['id'][]) {
|
||||
await this.userBannerPiningRepository.delete({
|
||||
userId,
|
||||
pinnedBannerId: In(bannerIds),
|
||||
});
|
||||
}
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { MiUserBanner } from '@/models/UserBanner.js';
|
||||
import type { DriveFilesRepository, MiDriveFile, UserBannerRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserBannerService {
|
||||
constructor(
|
||||
@Inject(DI.userBannerRepository)
|
||||
private userBannerRepository: UserBannerRepository,
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private idService: IdService,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定したユーザーのバナーを作成します
|
||||
* @param userId
|
||||
* @param description
|
||||
* @param url
|
||||
* @param fileId
|
||||
*/
|
||||
@bindThis
|
||||
public async create(userId: MiUser['id'], description: string | null, url: string, fileId: MiDriveFile['id']) {
|
||||
const banner = await this.userBannerRepository.findOneBy({
|
||||
userId,
|
||||
});
|
||||
|
||||
if (banner) throw new IdentifiableError('9dab45d9-cc66-4dfa-8305-610834e7f256', 'Already exists.');
|
||||
|
||||
const file = await this.driveFilesRepository.findOneBy({
|
||||
id: fileId,
|
||||
});
|
||||
|
||||
if (file == null) throw new IdentifiableError('e61187d1-9270-426b-8dc6-6b233c545133', 'No such file.');
|
||||
|
||||
return await this.userBannerRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
userId,
|
||||
description: description ?? null,
|
||||
fileId: file.id,
|
||||
url: url,
|
||||
} as MiUserBanner);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定したユーザーのバナーを更新します
|
||||
* @param userId
|
||||
* @param bannerId
|
||||
* @param description
|
||||
* @param url
|
||||
* @param fileId
|
||||
*/
|
||||
@bindThis
|
||||
public async update(userId: MiUser['id'], bannerId: MiUserBanner['id'], description: string | null, url: string | null, fileId: MiDriveFile['id'] ) {
|
||||
const banner = await this.userBannerRepository.findOneBy({
|
||||
id: bannerId,
|
||||
});
|
||||
|
||||
if (banner == null) {
|
||||
throw new IdentifiableError('ac26da32-1659-4fbb-82c2-fc11a494799f', 'No such banner.');
|
||||
}
|
||||
|
||||
if (banner.userId !== userId) {
|
||||
throw new IdentifiableError('dfe79730-96f7-4d65-8c2a-b0975bf3524c', 'Not this user banner.');
|
||||
}
|
||||
|
||||
const file = await this.driveFilesRepository.findOneBy({
|
||||
id: fileId,
|
||||
});
|
||||
|
||||
if (file == null) {
|
||||
throw new IdentifiableError('e61187d1-9270-426b-8dc6-6b233c545133', 'No such file.');
|
||||
}
|
||||
|
||||
await this.userBannerRepository.update({
|
||||
id: bannerId,
|
||||
}, {
|
||||
description: description ?? null,
|
||||
fileId: file.id,
|
||||
url: url ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定したユーザーのバナー削除します
|
||||
* @param userId
|
||||
* @param bannerId
|
||||
*/
|
||||
@bindThis
|
||||
public async delete(userId: MiUser['id'], bannerId: MiUserBanner['id']) {
|
||||
const banner = await this.userBannerRepository.findOneBy({
|
||||
id: bannerId,
|
||||
});
|
||||
|
||||
if (banner == null) {
|
||||
throw new IdentifiableError('f4b158a5-610f-4ed3-b228-3507ebe1bba6', 'No such banner.');
|
||||
}
|
||||
|
||||
if (banner.userId !== userId) {
|
||||
throw new IdentifiableError('ad84053d-0cf4-4446-ac72-209adef15835', 'Not this user banner.');
|
||||
}
|
||||
|
||||
await this.userBannerRepository.delete({
|
||||
id: bannerId,
|
||||
});
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, MiUserBanner, UserBannerRepository } from '@/models/_.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserBannerEntityService implements OnModuleInit {
|
||||
private userEntityService: UserEntityService;
|
||||
constructor(
|
||||
@Inject(DI.userBannerRepository)
|
||||
private userBannerRepository: UserBannerRepository,
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
private moduleRef: ModuleRef,
|
||||
) {
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.userEntityService = this.moduleRef.get(UserEntityService.name);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async pack(
|
||||
src: MiUserBanner | MiUserBanner['id'] | null | undefined,
|
||||
me: { id: MiUser['id'] } | null | undefined,
|
||||
): Promise<Packed<'UserBanner'>> {
|
||||
if (!src) throw new IdentifiableError('9dab45d9-cc66-4dfa-8305-610834e7f256', 'No such banner.');
|
||||
|
||||
const banner = typeof src === 'object' ? src : await this.userBannerRepository.findOneByOrFail({ id: src });
|
||||
const file = await this.driveFilesRepository.findOneByOrFail({ id: banner.fileId });
|
||||
|
||||
return {
|
||||
id: banner.id,
|
||||
user: await this.userEntityService.pack(banner.userId, me),
|
||||
description: banner.description,
|
||||
imgUrl: file.url,
|
||||
url: banner.url,
|
||||
fileId: file.id,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packMany(
|
||||
src: MiUserBanner[] | MiUserBanner['id'][],
|
||||
me: { id: MiUser['id'] } | null | undefined,
|
||||
): Promise<Packed<'UserBanner'>[]> {
|
||||
return (await Promise.allSettled(src.map(x => this.pack(x, me))))
|
||||
.filter(result => result.status === 'fulfilled')
|
||||
.map(result => (result as PromiseFulfilledResult<Packed<'UserBanner'>>).value);
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiUserBannerPining } from '@/models/_.js';
|
||||
import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserBannerPiningEntityService {
|
||||
constructor(
|
||||
private userBannerEntityService: UserBannerEntityService,
|
||||
) {}
|
||||
|
||||
@bindThis
|
||||
public async packMany(
|
||||
src: MiUserBannerPining[],
|
||||
me: { id: MiUser['id'] } | null | undefined,
|
||||
) : Promise<Packed<'UserBanner'>[]> {
|
||||
return (await Promise.allSettled(src.map(pining => this.userBannerEntityService.pack(pining.pinnedBannerId, me))))
|
||||
.filter(result => result.status === 'fulfilled')
|
||||
.map(result => (result as PromiseFulfilledResult<Packed<'UserBanner'>>).value);
|
||||
}
|
||||
}
|
@ -28,20 +28,16 @@ import type {
|
||||
FollowingsRepository,
|
||||
FollowRequestsRepository,
|
||||
MiFollowing,
|
||||
MiUserBanner,
|
||||
MiUserNotePining,
|
||||
MiUserProfile,
|
||||
MutingsRepository,
|
||||
NoteUnreadsRepository,
|
||||
RenoteMutingsRepository,
|
||||
UserBannerRepository,
|
||||
UserBannerPiningRepository,
|
||||
UserMemoRepository,
|
||||
UserNotePiningsRepository,
|
||||
UserProfilesRepository,
|
||||
UserSecurityKeysRepository,
|
||||
UsersRepository,
|
||||
MiUserBannerPining,
|
||||
} from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
@ -53,8 +49,6 @@ import type { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js';
|
||||
import { UserBannerPiningEntityService } from '@/core/entities/UserBannerPiningEntityService.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
import type { NoteEntityService } from './NoteEntityService.js';
|
||||
import type { PageEntityService } from './PageEntityService.js';
|
||||
@ -135,19 +129,11 @@ export class UserEntityService implements OnModuleInit {
|
||||
@Inject(DI.userNotePiningsRepository)
|
||||
private userNotePiningsRepository: UserNotePiningsRepository,
|
||||
|
||||
@Inject(DI.userBannerRepository)
|
||||
private userBannerRepository: UserBannerRepository,
|
||||
@Inject(DI.userBannerPiningRepository)
|
||||
private userBannerPiningRepository: UserBannerPiningRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
@Inject(DI.userMemosRepository)
|
||||
private userMemosRepository: UserMemoRepository,
|
||||
|
||||
private userBannerEntityService: UserBannerEntityService,
|
||||
private userBannerPiningEntityService: UserBannerPiningEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -457,8 +443,6 @@ export class UserEntityService implements OnModuleInit {
|
||||
}
|
||||
|
||||
let pins: MiUserNotePining[] = [];
|
||||
let myMutualBanner: MiUserBanner | null = null;
|
||||
let mutualBanners: MiUserBannerPining[] = [];
|
||||
if (isDetailed) {
|
||||
if (opts.pinNotes) {
|
||||
pins = opts.pinNotes.get(user.id) ?? [];
|
||||
@ -469,12 +453,6 @@ export class UserEntityService implements OnModuleInit {
|
||||
.orderBy('pin.id', 'DESC')
|
||||
.getMany();
|
||||
}
|
||||
if (user.id) {
|
||||
[myMutualBanner, mutualBanners] = await Promise.all([
|
||||
this.userBannerRepository.findOneBy({ userId: user.id }),
|
||||
this.userBannerPiningRepository.findBy({ userId: user.id }),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const followingCount = profile == null ? null :
|
||||
@ -555,8 +533,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
lang: profile!.lang,
|
||||
fields: profile!.fields,
|
||||
verifiedLinks: profile!.verifiedLinks,
|
||||
mutualBanners: mutualBanners.length > 0 ? this.userBannerPiningEntityService.packMany(mutualBanners, me) : [],
|
||||
myMutualBanner: myMutualBanner ? this.userBannerEntityService.pack(myMutualBanner, me) : null,
|
||||
mutualLinkSections: profile!.mutualLinkSections,
|
||||
followersCount: followersCount ?? 0,
|
||||
followingCount: followingCount ?? 0,
|
||||
notesCount: user.notesCount,
|
||||
@ -727,7 +704,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes }))))
|
||||
return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap?.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes }))))
|
||||
.filter(result => result.status === 'fulfilled')
|
||||
.map(result => (result as PromiseFulfilledResult<Packed<S>>).value);
|
||||
}
|
||||
|
@ -28,8 +28,6 @@ export const DI = {
|
||||
pollsRepository: Symbol('pollsRepository'),
|
||||
pollVotesRepository: Symbol('pollVotesRepository'),
|
||||
userProfilesRepository: Symbol('userProfilesRepository'),
|
||||
userBannerRepository: Symbol('userBannerRepository'),
|
||||
userBannerPiningRepository: Symbol('userBannerPiningRepository'),
|
||||
userKeypairsRepository: Symbol('userKeypairsRepository'),
|
||||
userPendingsRepository: Symbol('userPendingsRepository'),
|
||||
userSecurityKeysRepository: Symbol('userSecurityKeysRepository'),
|
||||
|
@ -11,7 +11,6 @@ import {
|
||||
packedMeDetailedSchema,
|
||||
packedUserDetailedSchema,
|
||||
packedUserSchema,
|
||||
packedUserBannerSchema,
|
||||
} from '@/models/json-schema/user.js';
|
||||
import { packedAbuseUserReportSchema } from '@/models/json-schema/abuse-user-report.js';
|
||||
import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
|
||||
@ -69,7 +68,6 @@ export const refs = {
|
||||
MeDetailed: packedMeDetailedSchema,
|
||||
UserDetailed: packedUserDetailedSchema,
|
||||
User: packedUserSchema,
|
||||
UserBanner: packedUserBannerSchema,
|
||||
|
||||
UserList: packedUserListSchema,
|
||||
UserListMembership: packedUserListMembershipSchema,
|
||||
|
@ -76,8 +76,6 @@ import {
|
||||
MiWebhook,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
MiUserBannerPining,
|
||||
MiUserBanner,
|
||||
} from './_.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
@ -208,18 +206,6 @@ const $userNotePiningsRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $userBannerRepository: Provider = {
|
||||
provide: DI.userBannerRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiUserBanner),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $userBannerPiningRepository: Provider = {
|
||||
provide: DI.userBannerPiningRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiUserBannerPining),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $userIpsRepository: Provider = {
|
||||
provide: DI.userIpsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiUserIp),
|
||||
@ -539,8 +525,6 @@ const $abuseReportResolversRepository: Provider = {
|
||||
$userListFavoritesRepository,
|
||||
$userListMembershipsRepository,
|
||||
$userNotePiningsRepository,
|
||||
$userBannerPiningRepository,
|
||||
$userBannerRepository,
|
||||
$userIpsRepository,
|
||||
$usedUsernamesRepository,
|
||||
$followingsRepository,
|
||||
@ -613,8 +597,6 @@ const $abuseReportResolversRepository: Provider = {
|
||||
$userListFavoritesRepository,
|
||||
$userListMembershipsRepository,
|
||||
$userNotePiningsRepository,
|
||||
$userBannerPiningRepository,
|
||||
$userBannerRepository,
|
||||
$userIpsRepository,
|
||||
$usedUsernamesRepository,
|
||||
$followingsRepository,
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { Entity, Column, Index, JoinColumn, PrimaryColumn, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
import { MiDriveFile } from './DriveFile.js';
|
||||
|
||||
@Entity('user_banner')
|
||||
export class MiUserBanner {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public description: string | null;
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public url: string | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public fileId: MiDriveFile['id'];
|
||||
|
||||
@ManyToOne(type => MiDriveFile, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public file: MiDriveFile;
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { Entity, Column, Index, JoinColumn, PrimaryColumn, ManyToOne, OneToOne } from 'typeorm';
|
||||
import { MiUserBanner } from '@/models/UserBanner.js';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
@Entity('user_banner_pining')
|
||||
@Index(['userId', 'pinnedBannerId'], { unique: true })
|
||||
export class MiUserBannerPining {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public pinnedBannerId: MiUserBanner['id'];
|
||||
|
||||
@ManyToOne(type => MiUserBanner, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public pinnedBanner: MiUserBanner;
|
||||
}
|
@ -9,6 +9,7 @@ import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
import { MiPage } from './Page.js';
|
||||
import { MiUserList } from './UserList.js';
|
||||
import type { MiDriveFile } from './DriveFile.js';
|
||||
|
||||
// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
|
||||
// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
|
||||
@ -42,6 +43,18 @@ export class MiUserProfile {
|
||||
})
|
||||
public description: string | null;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
public mutualLinkSections: {
|
||||
name: string | null;
|
||||
mutualLinks: {
|
||||
fileId: MiDriveFile['id'];
|
||||
description: string | null;
|
||||
imgSrc: string;
|
||||
}[];
|
||||
}[] | [];
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
|
@ -60,8 +60,6 @@ import { MiUserListMembership } from '@/models/UserListMembership.js';
|
||||
import { MiUserNotePining } from '@/models/UserNotePining.js';
|
||||
import { MiUserPending } from '@/models/UserPending.js';
|
||||
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import { MiUserBanner } from '@/models/UserBanner.js';
|
||||
import { MiUserBannerPining } from '@/models/UserBannerPining.js';
|
||||
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
||||
import { MiUserMemo } from '@/models/UserMemo.js';
|
||||
@ -136,8 +134,6 @@ export {
|
||||
MiUserNotePining,
|
||||
MiUserPending,
|
||||
MiUserProfile,
|
||||
MiUserBanner,
|
||||
MiUserBannerPining,
|
||||
MiUserPublickey,
|
||||
MiUserSecurityKey,
|
||||
MiWebhook,
|
||||
@ -210,8 +206,6 @@ export type UserListMembershipsRepository = Repository<MiUserListMembership>;
|
||||
export type UserNotePiningsRepository = Repository<MiUserNotePining>;
|
||||
export type UserPendingsRepository = Repository<MiUserPending>;
|
||||
export type UserProfilesRepository = Repository<MiUserProfile>;
|
||||
export type UserBannerRepository = Repository<MiUserBanner>;
|
||||
export type UserBannerPiningRepository = Repository<MiUserBannerPining>;
|
||||
export type UserPublickeysRepository = Repository<MiUserPublickey>;
|
||||
export type UserSecurityKeysRepository = Repository<MiUserSecurityKey>;
|
||||
export type WebhooksRepository = Repository<MiWebhook>;
|
||||
|
@ -312,6 +312,14 @@ export const packedRolePoliciesSchema = {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
mutualLinkSectionLimit: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
mutualLinkLimit: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -386,6 +386,29 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||
type: 'string',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
mutualLinkSections: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', nullable: true },
|
||||
mutualLinks: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string' },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
description: { type: 'string', nullable: true },
|
||||
imgSrc: { type: 'string' },
|
||||
},
|
||||
required: ['url', 'fileId'],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['mutualLinks'],
|
||||
},
|
||||
},
|
||||
//#region relations
|
||||
isFollowing: {
|
||||
type: 'boolean',
|
||||
@ -428,80 +451,6 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
mutualBanners: {
|
||||
type: 'array',
|
||||
nullable: true, optional: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
nullable: false, optional: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
format: 'id',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
nullable: false, optional: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
imgUrl: {
|
||||
type: 'string',
|
||||
format: 'url',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
format: 'url',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
format: 'id',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
myMutualBanner: {
|
||||
type: 'object',
|
||||
nullable: true, optional: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
format: 'id',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
nullable: false, optional: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
imgUrl: {
|
||||
type: 'string',
|
||||
format: 'url',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
format: 'url',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
format: 'id',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
//#endregion
|
||||
},
|
||||
} as const;
|
||||
@ -787,37 +736,3 @@ export const packedUserSchema = {
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
export const packedUserBannerSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
format: 'id',
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
nullable: false, optional: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
imgUrl: {
|
||||
type: 'string',
|
||||
format: 'url',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
format: 'id',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@ -83,8 +83,6 @@ import { MiFlashLike } from '@/models/FlashLike.js';
|
||||
import { MiUserMemo } from '@/models/UserMemo.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { MiUserBanner } from '@/models/UserBanner.js';
|
||||
import { MiUserBannerPining } from '@/models/UserBannerPining.js';
|
||||
|
||||
import { Config } from '@/config.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@ -205,8 +203,6 @@ export const entities = [
|
||||
MiFlash,
|
||||
MiFlashLike,
|
||||
MiUserMemo,
|
||||
MiUserBanner,
|
||||
MiUserBannerPining,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
...charts,
|
||||
|
@ -31,7 +31,7 @@ import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-dec
|
||||
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
||||
import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
|
||||
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
|
||||
import * as ep___admin_unsetUserMutualBanner from './endpoints/admin/unset-user-mutual-banner.js';
|
||||
import * as ep___admin_unsetUserMutualLink from './endpoints/admin/unset-user-mutual-link.js';
|
||||
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
||||
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
||||
import * as ep___admin_drive_deleteAllFilesOfAUser from './endpoints/admin/drive/delete-all-files-of-a-user.js';
|
||||
@ -422,7 +422,7 @@ const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-deco
|
||||
const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
|
||||
const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default };
|
||||
const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default };
|
||||
const $admin_unsetUserMutualBanner: Provider = { provide: 'ep:admin/unset-user-mutual-banner', useClass: ep___admin_unsetUserMutualBanner.default };
|
||||
const $admin_unsetUserMutualLink: Provider = { provide: 'ep:admin/unset-user-mutual-link', useClass: ep___admin_unsetUserMutualLink.default };
|
||||
const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
|
||||
const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
|
||||
const $admin_drive_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/drive/delete-all-files-of-a-user', useClass: ep___admin_drive_deleteAllFilesOfAUser.default };
|
||||
@ -817,7 +817,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||
$admin_avatarDecorations_update,
|
||||
$admin_unsetUserAvatar,
|
||||
$admin_unsetUserBanner,
|
||||
$admin_unsetUserMutualBanner,
|
||||
$admin_unsetUserMutualLink,
|
||||
$admin_drive_cleanRemoteFiles,
|
||||
$admin_drive_cleanup,
|
||||
$admin_drive_deleteAllFilesOfAUser,
|
||||
@ -1206,7 +1206,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||
$admin_avatarDecorations_update,
|
||||
$admin_unsetUserAvatar,
|
||||
$admin_unsetUserBanner,
|
||||
$admin_unsetUserMutualBanner,
|
||||
$admin_unsetUserMutualLink,
|
||||
$admin_drive_cleanRemoteFiles,
|
||||
$admin_drive_cleanup,
|
||||
$admin_drive_deleteAllFilesOfAUser,
|
||||
|
@ -31,7 +31,7 @@ import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-dec
|
||||
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
||||
import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
|
||||
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
|
||||
import * as ep___admin_unsetUserMutualBanner from './endpoints/admin/unset-user-mutual-banner.js';
|
||||
import * as ep___admin_unsetUserMutualLink from './endpoints/admin/unset-user-mutual-link.js';
|
||||
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
||||
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
||||
import * as ep___admin_drive_deleteAllFilesOfAUser from './endpoints/admin/drive/delete-all-files-of-a-user.js';
|
||||
@ -420,7 +420,7 @@ const eps = [
|
||||
['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
|
||||
['admin/unset-user-avatar', ep___admin_unsetUserAvatar],
|
||||
['admin/unset-user-banner', ep___admin_unsetUserBanner],
|
||||
['admin/unset-user-mutual-banner', ep___admin_unsetUserMutualBanner],
|
||||
['admin/unset-user-mutual-link', ep___admin_unsetUserMutualLink],
|
||||
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
|
||||
['admin/drive/cleanup', ep___admin_drive_cleanup],
|
||||
['admin/drive/delete-all-files-of-a-user', ep___admin_drive_deleteAllFilesOfAUser],
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserBannerRepository, UsersRepository } from '@/models/_.js';
|
||||
import type {
|
||||
UsersRepository,
|
||||
UserProfilesRepository,
|
||||
} from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
@ -9,7 +12,7 @@ export const meta = {
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:unset-user-mutual-banner',
|
||||
kind: 'write:admin:unset-user-mutual-link',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
@ -26,32 +29,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
@Inject(DI.userBannerRepository)
|
||||
private userBannerRepository: UserBannerRepository,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
|
||||
|
||||
if (user == null) {
|
||||
if (user == null || userProfile == null) {
|
||||
throw new Error('user not found');
|
||||
}
|
||||
|
||||
const mutualBanner = await this.userBannerRepository.findOneBy({ userId: user.id });
|
||||
|
||||
if (mutualBanner == null) return;
|
||||
|
||||
await this.userBannerRepository.delete({
|
||||
id: mutualBanner.id,
|
||||
await this.userProfilesRepository.update(user.id, {
|
||||
mutualLinkSections: [],
|
||||
});
|
||||
|
||||
this.moderationLogService.log(me, 'unsetUserMutualBanner', {
|
||||
this.moderationLogService.log(me, 'unsetUserMutualLink', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userBannerDescription: mutualBanner.description,
|
||||
userBannerUrl: mutualBanner.url,
|
||||
fileId: mutualBanner.fileId,
|
||||
userMutualLinkSections: userProfile.mutualLinkSections,
|
||||
});
|
||||
});
|
||||
}
|
@ -11,14 +11,7 @@ import { JSDOM } from 'jsdom';
|
||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import type {
|
||||
UsersRepository,
|
||||
DriveFilesRepository,
|
||||
UserProfilesRepository,
|
||||
PagesRepository,
|
||||
UserBannerRepository,
|
||||
UserBannerPiningRepository,
|
||||
} from '@/models/_.js';
|
||||
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
|
||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
@ -40,8 +33,6 @@ import type { Config } from '@/config.js';
|
||||
import { safeForSql } from '@/misc/safe-for-sql.js';
|
||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
|
||||
import { UserBannerService } from '@/core/UserBannerService.js';
|
||||
import { UserBannerPiningService } from '@/core/UserBannerPiningService.js';
|
||||
import { ApiLoggerService } from '../../ApiLoggerService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
@ -234,23 +225,27 @@ export const paramDef = {
|
||||
uniqueItems: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
mutualBannerPining: {
|
||||
mutualLinkSections: {
|
||||
type: 'array',
|
||||
nullable: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
format: 'misskey:id',
|
||||
},
|
||||
},
|
||||
myMutualBanner: {
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
properties: {
|
||||
name: { type: 'string', nullable: true },
|
||||
mutualLinks: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', format: 'url' },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
description: { type: 'string' },
|
||||
url: { type: 'string', nullable: true, format: 'url' },
|
||||
description: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['url', 'fileId'],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['mutualLinks'],
|
||||
},
|
||||
required: ['fileId'],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@ -270,17 +265,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
@Inject(DI.userBannerRepository)
|
||||
private userBannerRepository: UserBannerRepository,
|
||||
|
||||
@Inject(DI.pagesRepository)
|
||||
private pagesRepository: PagesRepository,
|
||||
|
||||
@Inject(DI.userBannerPiningRepository)
|
||||
private userBannerPiningRepository: UserBannerPiningRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private userBannerService: UserBannerService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
@ -292,7 +280,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private cacheService: CacheService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private avatarDecorationService: AvatarDecorationService,
|
||||
private userBannerPiningService: UserBannerPiningService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, _user, token) => {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
|
||||
@ -369,48 +356,41 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
updates.avatarBlurhash = null;
|
||||
}
|
||||
|
||||
if (ps.mutualBannerPining) {
|
||||
const bannerPiningNow = await this.userBannerPiningRepository.findBy({ userId: user.id });
|
||||
|
||||
const bannerPiningNowIds = new Set(bannerPiningNow.map(b => b.pinnedBannerId));
|
||||
const mutualBannerPiningIds = new Set(ps.mutualBannerPining);
|
||||
|
||||
const bannersToAdd = [...mutualBannerPiningIds].filter(bannerId => !bannerPiningNowIds.has(bannerId));
|
||||
const bannersToRemove = [...bannerPiningNowIds].filter(bannerId => !mutualBannerPiningIds.has(bannerId));
|
||||
|
||||
if (bannersToAdd.length > 0) {
|
||||
await this.userBannerPiningService.addPinned(user.id, bannersToAdd);
|
||||
if (ps.mutualLinkSections) {
|
||||
if (ps.mutualLinkSections.length > policy.mutualLinkSectionLimit) {
|
||||
throw new ApiError(meta.errors.restrictedByRole);
|
||||
}
|
||||
|
||||
if (bannersToRemove.length > 0) {
|
||||
await this.userBannerPiningService.removePinned(user.id, bannersToRemove);
|
||||
}
|
||||
const mutualLinkSections = ps.mutualLinkSections.map(async (section) => {
|
||||
if (section.mutualLinks.length > policy.mutualLinkLimit) {
|
||||
throw new ApiError(meta.errors.restrictedByRole);
|
||||
}
|
||||
|
||||
if (ps.myMutualBanner) {
|
||||
const banner = await this.userBannerRepository.findOneBy({
|
||||
userId: user.id,
|
||||
const mutualLinks = await Promise.all(section.mutualLinks.map(async (mutualLink) => {
|
||||
const file = await this.driveFilesRepository.findOneBy({ id: mutualLink.fileId });
|
||||
|
||||
if (!file) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
if (!file.type.startsWith('image/')) {
|
||||
throw new ApiError(meta.errors.fileNotAnImage);
|
||||
}
|
||||
|
||||
return {
|
||||
url: mutualLink.url,
|
||||
fileId: file.id,
|
||||
imgSrc: this.driveFileEntityService.getPublicUrl(file),
|
||||
description: mutualLink.description ?? null,
|
||||
};
|
||||
}));
|
||||
|
||||
return {
|
||||
name: section.name ?? null,
|
||||
mutualLinks,
|
||||
};
|
||||
});
|
||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.myMutualBanner.fileId });
|
||||
const profileUrl = this.config.url + '/@' + user.username;
|
||||
|
||||
if (file === null) throw new ApiError(meta.errors.noSuchFile);
|
||||
if (!file.type.startsWith('image/')) throw new ApiError(meta.errors.fileNotAnImage);
|
||||
|
||||
if (banner) {
|
||||
await this.userBannerService.update(user.id, banner.id, ps.myMutualBanner.description ?? null, ps.myMutualBanner.url ?? profileUrl, ps.myMutualBanner.fileId);
|
||||
} else {
|
||||
await this.userBannerService.create(user.id, ps.myMutualBanner.description ?? null, ps.myMutualBanner.url ?? profileUrl, ps.myMutualBanner.fileId);
|
||||
}
|
||||
}
|
||||
|
||||
if (ps.myMutualBanner === null) {
|
||||
const banner = await this.userBannerRepository.findOneBy({
|
||||
userId: user.id,
|
||||
});
|
||||
if (banner) {
|
||||
await this.userBannerService.delete(user.id, banner.id);
|
||||
}
|
||||
profileUpdates.mutualLinkSections = await Promise.all(mutualLinkSections);
|
||||
}
|
||||
|
||||
if (ps.bannerId) {
|
||||
|
@ -96,7 +96,7 @@ export const moderationLogTypes = [
|
||||
'deleteAvatarDecoration',
|
||||
'unsetUserAvatar',
|
||||
'unsetUserBanner',
|
||||
'unsetUserMutualBanner',
|
||||
'unsetUserMutualLink',
|
||||
] as const;
|
||||
|
||||
export type ModerationLogPayloads = {
|
||||
@ -315,12 +315,10 @@ export type ModerationLogPayloads = {
|
||||
userHost: string | null;
|
||||
fileId: string;
|
||||
};
|
||||
unsetUserMutualBanner: {
|
||||
unsetUserMutualLink: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userBannerDescription: string | null;
|
||||
userBannerUrl: string | null;
|
||||
fileId: string;
|
||||
userMutualLinkSections: { name: string | null; mutualLinks: { fileId: string; description: string | null; imgSrc: string; }[]; }[] | []
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -8,7 +8,7 @@ process.env.NODE_ENV = 'test';
|
||||
import * as assert from 'assert';
|
||||
import { inspect } from 'node:util';
|
||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||
import { api, post, role, signup, successfulApiCall, uploadFile, failedApiCall } from '../utils.js';
|
||||
import { api, failedApiCall, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('ユーザー', () => {
|
||||
@ -74,8 +74,7 @@ describe('ユーザー', () => {
|
||||
lang: user.lang,
|
||||
fields: user.fields,
|
||||
verifiedLinks: user.verifiedLinks,
|
||||
myMutualBanner: user.myMutualBanner,
|
||||
mutualBanners: user.mutualBanners,
|
||||
mutualLinkSections: user.mutualLinkSections,
|
||||
followersCount: user.followersCount,
|
||||
followingCount: user.followingCount,
|
||||
notesCount: user.notesCount,
|
||||
|
@ -108,6 +108,8 @@ export const ROLE_POLICIES = [
|
||||
'userEachUserListsLimit',
|
||||
'rateLimitFactor',
|
||||
'avatarDecorationLimit',
|
||||
'mutualLinkSectionLimit',
|
||||
'mutualLinkLimit',
|
||||
] as const;
|
||||
|
||||
// なんか動かない
|
||||
|
@ -66,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkButton v-if="user.host == null" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
|
||||
<MkButton inline danger @click="unsetUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
|
||||
<MkButton inline danger @click="unsetUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
|
||||
<MkButton inline danger @click="unsetUserMutualBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserMutualBanner }}</MkButton>
|
||||
<MkButton inline danger @click="unsetUserMutualLink"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserMutualLink }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
@ -365,10 +365,10 @@ async function unsetUserBanner() {
|
||||
}).then(refreshUser);
|
||||
}
|
||||
|
||||
async function unsetUserMutualBanner() {
|
||||
async function unsetUserMutualLink() {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.unsetUserMutualBannerConfirm,
|
||||
text: i18n.ts.unsetUserMutualLinkConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
|
||||
|
@ -755,6 +755,44 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.mutualLinkLimit, 'mutualLinkLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.mutualLinkLimit }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.mutualLinkLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.mutualLinkLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.mutualLinkLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.mutualLinkLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.mutualLinkLimit.value" :disabled="role.policies.mutualLinkLimit.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.mutualLinkLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.mutualLinkSectionLimit, 'mutualLinkSectionLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.mutualLinkSectionLimit }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.mutualLinkSectionLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.mutualLinkSectionLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.mutualLinkSectionLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.mutualLinkSectionLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.mutualLinkSectionLimit.value" :disabled="role.policies.mutualLinkSectionLimit.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.mutualLinkSectionLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canHideAds, 'canHideAds'])">
|
||||
<template #label>{{ i18n.ts._role._options.canHideAds }}</template>
|
||||
<template #suffix>
|
||||
|
@ -270,6 +270,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.mutualLinkSectionLimit, 'mutualLinkSectionLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.mutualLinkSectionLimit }}</template>
|
||||
<template #suffix>{{ policies.mutualLinkSectionLimit }}</template>
|
||||
<MkInput v-model="policies.mutualLinkSectionLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.mutualLinkLimit, 'mutualLinkLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.mutualLinkLimit }}</template>
|
||||
<template #suffix>{{ policies.mutualLinkLimit }}</template>
|
||||
<MkInput v-model="policies.mutualLinkLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canHideAds, 'canHideAds'])">
|
||||
<template #label>{{ i18n.ts._role._options.canHideAds }}</template>
|
||||
<template #suffix>{{ policies.canHideAds ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
|
@ -88,31 +88,77 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
||||
</FormSlot>
|
||||
<FormSlot>
|
||||
<MkFolder class="_margin">
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts._profile.myMutualBanner }}</template>
|
||||
<template #label>{{ i18n.ts._profile.mutualLinksEdit }}</template>
|
||||
|
||||
<MkInput v-model="myMutualBanner.description" class="_margin" small>
|
||||
<template #label>{{ i18n.ts._profile.mutualBannerDescriptionEdit }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="myMutualBanner.url" class="_margin" type="url" small>
|
||||
<template #label>URL</template>
|
||||
</MkInput>
|
||||
<div>
|
||||
<p>{{ i18n.ts._profile.mutualBanner }}</p>
|
||||
<img class="_margin" :class="$style.mutualBannerImg" :src="myMutualBanner.imgUrl">
|
||||
<MkButton class="_button _margin" @click="ev => changeMutualBannerFile(ev)">{{ i18n.ts.selectFile }}</MkButton>
|
||||
<div :class="$style.metadataRoot">
|
||||
<div :class="$style.metadataMargin">
|
||||
<MkButton inline style="margin-right: 8px;" :disabled="mutualLinkSections.length >= $i.policies.mutualLinkSectionLimit" @click="addMutualLinkSections"><i class="ti ti-plus"></i> {{ i18n.ts._profile.addMutualLinkSection }}</MkButton>
|
||||
<MkButton v-if="!mutualLinkSectionEditMode" inline danger style="margin-right: 8px;" @click="mutualLinkSectionEditMode = !mutualLinkSectionEditMode"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
<MkButton v-else inline style="margin-right: 8px;" @click="mutualLinkSectionEditMode = !mutualLinkSectionEditMode"><i class="ti ti-arrows-sort"></i> {{ i18n.ts.rearrange }}</MkButton>
|
||||
<MkButton inline primary @click="saveMutualLinks"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
|
||||
<MkButton class="_button" primary @click="saveMyMutualBanner">{{ i18n.ts.save }}</MkButton>
|
||||
<MkButton class="_button" primary @click="deleteMyMutualBanner">{{ i18n.ts.delete }}</MkButton>
|
||||
</MkFolder>
|
||||
<template #caption>
|
||||
<span>{{ i18n.ts._profile.myMutualBannerDescription }}<br>
|
||||
{{ i18n.ts.recommended }} 200x40<br>
|
||||
{{ i18n.ts.maximum }} 300x60
|
||||
</span>
|
||||
<Sortable
|
||||
v-model="mutualLinkSections"
|
||||
class="_gaps_s"
|
||||
itemKey="id"
|
||||
:animation="150"
|
||||
:handle="'.' + $style.dragItemHandle"
|
||||
@start="e => e.item.classList.add('active')"
|
||||
@end="e => e.item.classList.remove('active')"
|
||||
>
|
||||
<template #item="{element: sectionElement,index: sectionIndex}">
|
||||
<div :class="$style.mutualLinkSectionRoot">
|
||||
<button v-if="!mutualLinkSectionEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button>
|
||||
<button v-if="mutualLinkSectionEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteMutualLinkSection(sectionIndex)"><i class="ti ti-x"></i></button>
|
||||
<FormSlot :style="{flexGrow: 1}">
|
||||
<MkFolder>
|
||||
<template #label>{{ sectionElement.name || i18n.ts._profile.sectionNameNone }}</template>
|
||||
|
||||
<div :class="$style.metadataMargin">
|
||||
<MkInput v-model="sectionElement.name" :disabled="sectionElement.none" :placeholder="i18n.ts._profile.sectionName" :max="32"></MkInput>
|
||||
<MkSwitch v-model="sectionElement.none" @update:modelValue="()=>{sectionElement.name = null}">{{ i18n.ts._profile.sectionNameNoneDescription }}</MkSwitch>
|
||||
<MkButton inline style="margin-right: 8px;" :disabled="sectionElement.mutualLinks.length >= $i.policies.mutualLinkLimit" @click="addMutualLinks(sectionIndex)"><i class="ti ti-plus"></i> {{ i18n.ts._profile.addMutualLink }}</MkButton>
|
||||
</div>
|
||||
<Sortable
|
||||
v-model="sectionElement.mutualLinks"
|
||||
class="_gaps_s"
|
||||
itemKey="id"
|
||||
:animation="150"
|
||||
:handle="'.' + $style.dragItemHandle"
|
||||
@start="e => e.item.classList.add('active')"
|
||||
@end="e => e.item.classList.remove('active')"
|
||||
>
|
||||
<template #item="{element: linkElement,index: linkIndex}">
|
||||
<div :class="$style.mutualLinkRoot">
|
||||
<button v-if="!mutualLinkSectionEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button>
|
||||
<button v-if="mutualLinkSectionEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteMutualLink(sectionIndex,linkIndex)"><i class="ti ti-x"></i></button>
|
||||
|
||||
<div class="_gaps_s" :style="{flex: 1}">
|
||||
<MkInput v-model="linkElement.url" small>
|
||||
<template #label>{{ i18n.ts._profile.mutualLinksUrl }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="linkElement.description" small>
|
||||
<template #label>{{ i18n.ts._profile.mutualLinksDescriptionEdit }}</template>
|
||||
</MkInput>
|
||||
<span>{{ i18n.ts._profile.mutualLinksBanner }}</span>
|
||||
<img :class="$style.mutualLinkImg" :src="linkElement.imgSrc">
|
||||
<MkButton class="_button" @click="ev => changeMutualLinkFile(ev, sectionIndex, linkIndex)">{{ i18n.ts.selectFile }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
</MkFolder>
|
||||
</FormSlot>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<template #caption>{{ i18n.ts._profile.mutualLinksDescription }}</template>
|
||||
</FormSlot>
|
||||
|
||||
<MkFolder>
|
||||
@ -136,7 +182,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, watch, defineAsyncComponent } from 'vue';
|
||||
import { computed, reactive, ref, watch, defineAsyncComponent, Ref } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
@ -155,11 +201,11 @@ import { defaultStore } from '@/store.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import * as Misskey from "misskey-js";
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance'));
|
||||
|
||||
const profile = reactive({
|
||||
@ -178,14 +224,10 @@ watch(() => profile, () => {
|
||||
deep: true,
|
||||
});
|
||||
|
||||
const mutualLinkSections = ref($i.mutualLinkSections ?? []) as Ref<Misskey.entities.UserDetailed['mutualLinkSections']>;
|
||||
const fields = ref($i.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []);
|
||||
const myMutualBanner = ref<{ fileId: string; description?: string; url?: string | null; imgUrl?: string; }>({
|
||||
fileId: $i.myMutualBanner?.fileId ?? '',
|
||||
description: $i.myMutualBanner?.description ?? '',
|
||||
url: $i.myMutualBanner?.url ?? '',
|
||||
imgUrl: $i.myMutualBanner?.imgUrl ?? '',
|
||||
});
|
||||
const fieldEditMode = ref(false);
|
||||
const mutualLinkSectionEditMode = ref(false);
|
||||
|
||||
function addField() {
|
||||
fields.value.push({
|
||||
@ -195,6 +237,22 @@ function addField() {
|
||||
});
|
||||
}
|
||||
|
||||
function addMutualLinks(index:number) {
|
||||
mutualLinkSections.value[index].mutualLinks.push({
|
||||
fileId: '',
|
||||
url: '',
|
||||
imgSrc: '',
|
||||
description: '',
|
||||
});
|
||||
}
|
||||
|
||||
function addMutualLinkSections() {
|
||||
mutualLinkSections.value.push({
|
||||
name: null,
|
||||
mutualLinks: [],
|
||||
});
|
||||
}
|
||||
|
||||
while (fields.value.length < 4) {
|
||||
addField();
|
||||
}
|
||||
@ -203,6 +261,14 @@ function deleteField(index: number) {
|
||||
fields.value.splice(index, 1);
|
||||
}
|
||||
|
||||
function deleteMutualLinkSection(index: number) {
|
||||
mutualLinkSections.value.splice(index, 1);
|
||||
}
|
||||
|
||||
function deleteMutualLink(sectionIndex:number, index: number) {
|
||||
mutualLinkSections.value[sectionIndex].mutualLinks.splice(index, 1);
|
||||
}
|
||||
|
||||
function saveFields() {
|
||||
os.apiWithDialog('i/update', {
|
||||
fields: fields.value.filter(field => field.name !== '' && field.value !== '').map(field => ({ name: field.name, value: field.value })),
|
||||
@ -210,40 +276,12 @@ function saveFields() {
|
||||
globalEvents.emit('requestClearPageCache');
|
||||
}
|
||||
|
||||
function isValidUrl(url: string): boolean {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function saveMyMutualBanner() {
|
||||
if ( myMutualBanner.value.fileId === '' || myMutualBanner.value.url && !isValidUrl(myMutualBanner.value.url)) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.invalidParamError,
|
||||
text: i18n.ts.invalidParamErrorDescription,
|
||||
});
|
||||
return;
|
||||
}
|
||||
function saveMutualLinks() {
|
||||
os.apiWithDialog('i/update', {
|
||||
myMutualBanner: {
|
||||
fileId: myMutualBanner.value.fileId,
|
||||
description: myMutualBanner.value.description,
|
||||
url: myMutualBanner.value.url === '' ? null : myMutualBanner.value.url,
|
||||
},
|
||||
mutualLinkSections: mutualLinkSections.value,
|
||||
});
|
||||
}
|
||||
|
||||
function deleteMyMutualBanner() {
|
||||
os.apiWithDialog('i/update', {
|
||||
myMutualBanner: null,
|
||||
});
|
||||
myMutualBanner.value = { fileId: '', description: '', url: '' };
|
||||
}
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog('i/update', {
|
||||
// 空文字列をnullにしたいので??は使うな
|
||||
@ -270,10 +308,10 @@ function save() {
|
||||
}
|
||||
}
|
||||
|
||||
function changeMutualBannerFile(ev: MouseEvent) {
|
||||
selectFile(ev.currentTarget ?? ev.target, i18n.ts.mutualBanner).then(async (file) => {
|
||||
myMutualBanner.value.imgUrl = file.url;
|
||||
myMutualBanner.value.fileId = file.id;
|
||||
function changeMutualLinkFile(ev: MouseEvent, sectionIndex: number, linkIndex: number) {
|
||||
selectFile(ev.currentTarget ?? ev.target, i18n.ts.mutualLink).then(async (file) => {
|
||||
mutualLinkSections.value[sectionIndex].mutualLinks[linkIndex].imgSrc = file.url;
|
||||
mutualLinkSections.value[sectionIndex].mutualLinks[linkIndex].fileId = file.id;
|
||||
});
|
||||
}
|
||||
|
||||
@ -373,6 +411,36 @@ definePageMetadata(() => ({
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.mutualLinkRoot{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
padding-bottom: .75em;
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
flex: 1;
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
}
|
||||
.mutualLinkSectionRoot{
|
||||
display: flex;
|
||||
padding-bottom: .75em;
|
||||
align-items: center;
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
overflow: clip;
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
/* (drag button) 32px + (drag button margin) 8px + (input width) 200px * 2 + (input gap) 12px = 452px */
|
||||
@container (max-width: 452px) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.metadataMargin {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
@ -425,11 +493,9 @@ definePageMetadata(() => ({
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mutualBannerImg {
|
||||
max-width: 300px;
|
||||
min-width: 200px;
|
||||
max-height: 60px;
|
||||
min-height: 40px;
|
||||
.mutualLinkImg {
|
||||
max-width: 150px;
|
||||
max-height: 30px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
|
@ -80,6 +80,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p>
|
||||
</MkOmit>
|
||||
</div>
|
||||
<MkContainer v-if="user?.mutualLinkSections?.length > 0" :showHeader="false" :max-height="200" class="fields" :style="{borderRadius: 0}">
|
||||
<div v-for="(section, index) in user?.mutualLinkSections" :key="index" :class="$style.mutualLinkSections">
|
||||
<span v-if="section.name">{{ section.name }}</span>
|
||||
<div :class="$style.mutualLinks">
|
||||
<div v-for="(mutualLink, i) in section.mutualLinks" :key="i">
|
||||
<MkLink :hideIcon="true" :url="mutualLink.url">
|
||||
<img :class="$style.mutualLinkImg" :src="mutualLink.imgSrc" :alt="mutualLink.description"/>
|
||||
</MkLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkContainer>
|
||||
<div class="fields system">
|
||||
<dl v-if="user.location" class="field">
|
||||
<dt class="name"><i class="ti ti-map-pin ti-fw"></i> {{ i18n.ts.location }}</dt>
|
||||
@ -143,28 +155,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div v-if="user?.myMutualBanner" class="fields">
|
||||
{{ i18n.ts.mutualBannerThisUser }}
|
||||
|
||||
<div :class="$style.myMutualBanner">
|
||||
<MkLink :hideIcon="true" :url="user.myMutualBanner.url">
|
||||
<img :class="$style.mutualBannerImg" :src="user.myMutualBanner.imgUrl" :alt="user.myMutualBanner.description"/>
|
||||
</MkLink>
|
||||
<span>{{ (user.myMutualBanner?.description === '' || user.myMutualBanner?.description === null) ? i18n.ts.noDescription : user.myMutualBanner?.description }}</span>
|
||||
<MkButton v-if="$i && $i?.id !== user.id && !$i?.mutualBanners?.some(banner => banner.id === user.myMutualBanner?.id) " @click="mutualBannerFollow(user.myMutualBanner?.id)">{{ i18n.ts.follow }}</MkButton>
|
||||
<MkButton v-else-if="$i && $i?.id !== user.id" @click="mutualBannerUnFollow(user.myMutualBanner?.id)">{{ i18n.ts.unfollow }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="user?.mutualBanners && user?.mutualBanners.length > 0" class="fields">
|
||||
{{ i18n.ts.mutualBanner }}
|
||||
<div :class="$style.mutualBanner">
|
||||
<div v-for="(mutualBanner, i) in mutualBanners?.slice(0, 9)" :key="i">
|
||||
<MkLink :hideIcon="true" :url="mutualBanner.url">
|
||||
<img :class="$style.mutualBannerImg" :src="mutualBanner.imgUrl" :alt="mutualBanner.description"/>
|
||||
</MkLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status">
|
||||
<MkA :to="userPage(user)">
|
||||
<b>{{ number(user.notesCount) }}</b>
|
||||
@ -235,6 +226,7 @@ import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
|
||||
function calcAge(birthdate: string): number {
|
||||
const date = new Date(birthdate);
|
||||
@ -276,7 +268,6 @@ const memoDraft = ref(props.user.memo);
|
||||
const isEditingMemo = ref(false);
|
||||
const moderationNote = ref(props.user.moderationNote);
|
||||
const editModerationNote = ref(false);
|
||||
const mutualBanners = ref(props.user.mutualBanners);
|
||||
|
||||
watch(moderationNote, async () => {
|
||||
await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value });
|
||||
@ -323,23 +314,6 @@ function showMemoTextarea() {
|
||||
});
|
||||
}
|
||||
|
||||
function mutualBannerFollow(id: string) {
|
||||
os.apiWithDialog('i/update', {
|
||||
mutualBannerPining: [
|
||||
...($i?.mutualBanners?.map(banner => banner.id) ?? []),
|
||||
id,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function mutualBannerUnFollow(id:string) {
|
||||
os.apiWithDialog('i/update', {
|
||||
mutualBannerPining: [
|
||||
...($i?.mutualBanners?.map(banner => banner.id) ?? []).filter(bannerId => bannerId !== id),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function adjustMemoTextarea() {
|
||||
if (!memoTextareaEl.value) return;
|
||||
memoTextareaEl.value.style.height = '0px';
|
||||
@ -829,27 +803,36 @@ onUnmounted(() => {
|
||||
background-color: rgb(54, 54, 54);
|
||||
}
|
||||
|
||||
.myMutualBanner {
|
||||
.mutualLinkSections {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-flow: column wrap;
|
||||
padding: 16px;
|
||||
flex-direction: column;
|
||||
background: var(--panel);
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
}
|
||||
|
||||
.mutualBanner {
|
||||
.mutualLinks {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
padding-top: 8px;
|
||||
@media (max-width: 500px) {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.mutualBannerImg {
|
||||
max-width: 300px;
|
||||
min-width: 200px;
|
||||
max-height: 60px;
|
||||
min-height: 40px;
|
||||
object-fit: contain;
|
||||
.mutualLink {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mutualLinkImg {
|
||||
max-width: 150px;
|
||||
max-height: 30px;
|
||||
}
|
||||
</style>
|
||||
|
@ -11,24 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<option :value="null">{{ i18n.ts.notes }}</option>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="files">{{ i18n.ts.withFiles }}</option>
|
||||
<option value="mutualBanners">{{ i18n.ts.mutualBanner }}</option>
|
||||
</MkTab>
|
||||
</template>
|
||||
<MkNotes v-if="tab !== 'mutualBanners'" :noGap="true" :pagination="pagination" :class="$style.tl"/>
|
||||
<div v-else>
|
||||
<div v-if="mutualBanners && mutualBanners.length > 0" :class="$style.mutualBanners">
|
||||
<div v-for="(mutualBanner, i) in mutualBanners" :key="i" class="_margin">
|
||||
<MkLink :hideIcon="true" :url="mutualBanner.url">
|
||||
<img :class="$style.banner" :src="mutualBanner.imgUrl"/>
|
||||
</MkLink>
|
||||
<p>{{ (mutualBanner.description === '' || mutualBanner.description === null) ? i18n.ts.noDescription : mutualBanner.description }}</p>
|
||||
<MkButton v-if="$i?.id === user.id" @click="mutualBannerUnFollow(mutualBanner.id)">{{ i18n.ts.unfollow }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>{{ i18n.ts.nothing }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<MkNotes :noGap="true" :pagination="pagination" :class="$style.tl"/>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
@ -38,17 +23,11 @@ import * as Misskey from 'misskey-js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { $i } from '@/account.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const props = defineProps<{
|
||||
user: Misskey.entities.UserDetailed;
|
||||
}>();
|
||||
|
||||
const mutualBanners = ref(props.user.mutualBanners);
|
||||
|
||||
const tab = ref<string | null>('all');
|
||||
|
||||
const pagination = computed(() => tab.value === 'featured' ? {
|
||||
@ -68,17 +47,6 @@ const pagination = computed(() => tab.value === 'featured' ? {
|
||||
withChannelNotes: true,
|
||||
},
|
||||
});
|
||||
|
||||
function mutualBannerUnFollow(id:string) {
|
||||
os.apiWithDialog('i/update', {
|
||||
mutualBannerPining: [
|
||||
...($i?.mutualBanners?.map(banner => banner.id) ?? []).filter(bannerId => bannerId !== id),
|
||||
],
|
||||
});
|
||||
if (mutualBanners.value) {
|
||||
mutualBanners.value = mutualBanners.value.filter(banner => banner.id !== id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@ -92,20 +60,4 @@ function mutualBannerUnFollow(id:string) {
|
||||
border-radius: var(--radius);
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.banner {
|
||||
max-width: 300px;
|
||||
min-width: 200px;
|
||||
max-height: 60px;
|
||||
min-height: 40px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.mutualBanners{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
</style>
|
||||
|
@ -377,7 +377,7 @@ type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requ
|
||||
type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminUnsetUserMutualBannerRequest = operations['admin___unset-user-mutual-banner']['requestBody']['content']['application/json'];
|
||||
type AdminUnsetUserMutualLinkRequest = operations['admin___unset-user-mutual-link']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json'];
|
||||
@ -1235,7 +1235,7 @@ declare namespace entities {
|
||||
AdminAvatarDecorationsUpdateRequest,
|
||||
AdminUnsetUserAvatarRequest,
|
||||
AdminUnsetUserBannerRequest,
|
||||
AdminUnsetUserMutualBannerRequest,
|
||||
AdminUnsetUserMutualLinkRequest,
|
||||
AdminDriveDeleteAllFilesOfAUserRequest,
|
||||
AdminDriveFilesRequest,
|
||||
AdminDriveFilesResponse,
|
||||
@ -1787,7 +1787,6 @@ declare namespace entities {
|
||||
MeDetailed,
|
||||
UserDetailed,
|
||||
User,
|
||||
UserBanner,
|
||||
UserList,
|
||||
UserListMembership,
|
||||
Ad,
|
||||
@ -2759,7 +2758,7 @@ type PagesUpdateRequest = operations['pages___update']['requestBody']['content']
|
||||
function parse(acct: string): Acct;
|
||||
|
||||
// @public (undocumented)
|
||||
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unset-user-mutual-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
|
||||
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unset-user-mutual-link", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
|
||||
|
||||
// @public (undocumented)
|
||||
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
|
||||
@ -3058,9 +3057,6 @@ function toString_2(acct: Acct): string;
|
||||
// @public (undocumented)
|
||||
type User = components['schemas']['User'];
|
||||
|
||||
// @public (undocumented)
|
||||
type UserBanner = components['schemas']['UserBanner'];
|
||||
|
||||
// @public (undocumented)
|
||||
type UserDetailed = components['schemas']['UserDetailed'];
|
||||
|
||||
|
@ -281,9 +281,9 @@ declare module '../api.js' {
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-mutual-banner*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-mutual-link*
|
||||
*/
|
||||
request<E extends 'admin/unset-user-mutual-banner', P extends Endpoints[E]['req']>(
|
||||
request<E extends 'admin/unset-user-mutual-link', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
|
@ -37,7 +37,7 @@ import type {
|
||||
AdminAvatarDecorationsUpdateRequest,
|
||||
AdminUnsetUserAvatarRequest,
|
||||
AdminUnsetUserBannerRequest,
|
||||
AdminUnsetUserMutualBannerRequest,
|
||||
AdminUnsetUserMutualLinkRequest,
|
||||
AdminDriveDeleteAllFilesOfAUserRequest,
|
||||
AdminDriveFilesRequest,
|
||||
AdminDriveFilesResponse,
|
||||
@ -609,7 +609,7 @@ export type Endpoints = {
|
||||
'admin/avatar-decorations/update': { req: AdminAvatarDecorationsUpdateRequest; res: EmptyResponse };
|
||||
'admin/unset-user-avatar': { req: AdminUnsetUserAvatarRequest; res: EmptyResponse };
|
||||
'admin/unset-user-banner': { req: AdminUnsetUserBannerRequest; res: EmptyResponse };
|
||||
'admin/unset-user-mutual-banner': { req: AdminUnsetUserMutualBannerRequest; res: EmptyResponse };
|
||||
'admin/unset-user-mutual-link': { req: AdminUnsetUserMutualLinkRequest; res: EmptyResponse };
|
||||
'admin/drive/clean-remote-files': { req: EmptyRequest; res: EmptyResponse };
|
||||
'admin/drive/cleanup': { req: EmptyRequest; res: EmptyResponse };
|
||||
'admin/drive/delete-all-files-of-a-user': { req: AdminDriveDeleteAllFilesOfAUserRequest; res: EmptyResponse };
|
||||
|
@ -40,7 +40,7 @@ export type AdminAvatarDecorationsListResponse = operations['admin___avatar-deco
|
||||
export type AdminAvatarDecorationsUpdateRequest = operations['admin___avatar-decorations___update']['requestBody']['content']['application/json'];
|
||||
export type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requestBody']['content']['application/json'];
|
||||
export type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requestBody']['content']['application/json'];
|
||||
export type AdminUnsetUserMutualBannerRequest = operations['admin___unset-user-mutual-banner']['requestBody']['content']['application/json'];
|
||||
export type AdminUnsetUserMutualLinkRequest = operations['admin___unset-user-mutual-link']['requestBody']['content']['application/json'];
|
||||
export type AdminDriveDeleteAllFilesOfAUserRequest = operations['admin___drive___delete-all-files-of-a-user']['requestBody']['content']['application/json'];
|
||||
export type AdminDriveFilesRequest = operations['admin___drive___files']['requestBody']['content']['application/json'];
|
||||
export type AdminDriveFilesResponse = operations['admin___drive___files']['responses']['200']['content']['application/json'];
|
||||
|
@ -7,7 +7,6 @@ export type UserDetailedNotMe = components['schemas']['UserDetailedNotMe'];
|
||||
export type MeDetailed = components['schemas']['MeDetailed'];
|
||||
export type UserDetailed = components['schemas']['UserDetailed'];
|
||||
export type User = components['schemas']['User'];
|
||||
export type UserBanner = components['schemas']['UserBanner'];
|
||||
export type UserList = components['schemas']['UserList'];
|
||||
export type UserListMembership = components['schemas']['UserListMembership'];
|
||||
export type Ad = components['schemas']['Ad'];
|
||||
|
@ -237,14 +237,14 @@ export type paths = {
|
||||
*/
|
||||
post: operations['admin___unset-user-banner'];
|
||||
};
|
||||
'/admin/unset-user-mutual-banner': {
|
||||
'/admin/unset-user-mutual-link': {
|
||||
/**
|
||||
* admin/unset-user-mutual-banner
|
||||
* admin/unset-user-mutual-link
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-mutual-banner*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-mutual-link*
|
||||
*/
|
||||
post: operations['admin___unset-user-mutual-banner'];
|
||||
post: operations['admin___unset-user-mutual-link'];
|
||||
};
|
||||
'/admin/drive/clean-remote-files': {
|
||||
/**
|
||||
@ -3842,6 +3842,16 @@ export type components = {
|
||||
roles: components['schemas']['RoleLite'][];
|
||||
memo: string | null;
|
||||
moderationNote?: string;
|
||||
mutualLinkSections: ({
|
||||
name: string | null;
|
||||
mutualLinks: ({
|
||||
url: string;
|
||||
/** Format: misskey:id */
|
||||
fileId: string;
|
||||
description: string | null;
|
||||
imgSrc: string;
|
||||
})[];
|
||||
})[];
|
||||
isFollowing?: boolean;
|
||||
isFollowed?: boolean;
|
||||
hasPendingFollowRequestFromYou?: boolean;
|
||||
@ -3853,30 +3863,6 @@ export type components = {
|
||||
/** @enum {string} */
|
||||
notify?: 'normal' | 'none';
|
||||
withReplies?: boolean;
|
||||
mutualBanners: (({
|
||||
/** Format: id */
|
||||
id: string;
|
||||
user: components['schemas']['UserLite'];
|
||||
description: string | null;
|
||||
/** Format: url */
|
||||
imgUrl: string;
|
||||
/** Format: url */
|
||||
url: string;
|
||||
/** Format: id */
|
||||
fileId: string;
|
||||
})[]) | null;
|
||||
myMutualBanner: ({
|
||||
/** Format: id */
|
||||
id: string;
|
||||
user: components['schemas']['UserLite'];
|
||||
description: string | null;
|
||||
/** Format: url */
|
||||
imgUrl: string;
|
||||
/** Format: url */
|
||||
url: string;
|
||||
/** Format: id */
|
||||
fileId: string;
|
||||
}) | null;
|
||||
};
|
||||
MeDetailedOnly: {
|
||||
/** Format: id */
|
||||
@ -4061,17 +4047,6 @@ export type components = {
|
||||
MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly'];
|
||||
UserDetailed: components['schemas']['UserDetailedNotMe'] | components['schemas']['MeDetailed'];
|
||||
User: components['schemas']['UserLite'] | components['schemas']['UserDetailed'];
|
||||
UserBanner: {
|
||||
/** Format: id */
|
||||
id: string;
|
||||
user: components['schemas']['UserLite'];
|
||||
description: string | null;
|
||||
/** Format: url */
|
||||
imgUrl: string;
|
||||
url: string | null;
|
||||
/** Format: id */
|
||||
fileId: string;
|
||||
};
|
||||
UserList: {
|
||||
/**
|
||||
* Format: id
|
||||
@ -4970,6 +4945,8 @@ export type components = {
|
||||
userEachUserListsLimit: number;
|
||||
rateLimitFactor: number;
|
||||
avatarDecorationLimit: number;
|
||||
mutualLinkSectionLimit: number;
|
||||
mutualLinkLimit: number;
|
||||
};
|
||||
ReversiGameLite: {
|
||||
/** Format: id */
|
||||
@ -6864,12 +6841,12 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
/**
|
||||
* admin/unset-user-mutual-banner
|
||||
* admin/unset-user-mutual-link
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-mutual-banner*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-mutual-link*
|
||||
*/
|
||||
'admin___unset-user-mutual-banner': {
|
||||
'admin___unset-user-mutual-link': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
@ -20308,14 +20285,16 @@ export type operations = {
|
||||
};
|
||||
emailNotificationTypes?: string[];
|
||||
alsoKnownAs?: string[];
|
||||
mutualBannerPining?: string[] | null;
|
||||
myMutualBanner?: ({
|
||||
mutualLinkSections?: ({
|
||||
name?: string | null;
|
||||
mutualLinks: ({
|
||||
/** Format: url */
|
||||
url: string;
|
||||
/** Format: misskey:id */
|
||||
fileId: string;
|
||||
description?: string;
|
||||
/** Format: url */
|
||||
url?: string | null;
|
||||
}) | null;
|
||||
description?: string | null;
|
||||
})[];
|
||||
})[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -62,7 +62,7 @@ export const permissions = [
|
||||
'write:admin:suspend-user',
|
||||
'write:admin:unset-user-avatar',
|
||||
'write:admin:unset-user-banner',
|
||||
'write:admin:unset-user-mutual-banner',
|
||||
'write:admin:unset-user-mutual-link',
|
||||
'write:admin:unsuspend-user',
|
||||
'write:admin:meta',
|
||||
'write:admin:user-note',
|
||||
@ -364,11 +364,9 @@ export type ModerationLogPayloads = {
|
||||
userHost: string | null;
|
||||
fileId: string;
|
||||
};
|
||||
unsetUserMutualBanner: {
|
||||
unsetUserMutualLink: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userBannerDescription: string | null;
|
||||
userBannerUrl: string;
|
||||
fileId: string;
|
||||
mutualLinkSections: string;
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user