Merge upstream
This commit is contained in:
commit
ce48a79f6b
@ -1340,7 +1340,7 @@ _initialAccountSetting:
|
||||
pushNotificationDescription: "Enabling push notifications will allow you to receive notifications from {name} directly on your device."
|
||||
initialAccountSettingCompleted: "Profile setup complete!"
|
||||
haveFun: "Enjoy {name}!"
|
||||
youCanContinueTutorial: "You can proceed to a tutorial on how to use {name} (Misskey) or you can exit the setup here and start using it immediately."
|
||||
continueTutorial: "Now, let's proceed with a tutorial on how to use {name} (Misskey)."
|
||||
startTutorial: "Start Tutorial"
|
||||
skipAreYouSure: "Really skip profile setup?"
|
||||
laterAreYouSure: "Really do profile setup later?"
|
||||
@ -2284,7 +2284,8 @@ _profile:
|
||||
addMutualLinkSection: "Add section"
|
||||
sectionName: "Section name"
|
||||
sectionNameNoneDescription: "Do not display the section name"
|
||||
sectionNameNone: "Hide section name"
|
||||
sectionNameNone: "Section without name"
|
||||
policyDisplayLimitExceeded: "The number of items displayed exceeds the current support plan's limit ({max}). This item will not be displayed. You can upgrade your plan [here](https://go.misskey.io/donate)."
|
||||
_exportOrImport:
|
||||
allNotes: "All notes"
|
||||
favoritedNotes: "Favorite notes"
|
||||
|
6
locales/index.d.ts
vendored
6
locales/index.d.ts
vendored
@ -8999,9 +8999,13 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"sectionNameNoneDescription": string;
|
||||
/**
|
||||
* セクション名を表示しない
|
||||
* 名前が表示されないセクション
|
||||
*/
|
||||
"sectionNameNone": string;
|
||||
/**
|
||||
* 現在の支援プランの表示上限({max}個)を超えているため、この項目は表示されません。[ここ](https://go.misskey.io/donate)からプランをアップグレードできます。
|
||||
*/
|
||||
"policyDisplayLimitExceeded": ParameterizedString<"max">;
|
||||
};
|
||||
"_exportOrImport": {
|
||||
/**
|
||||
|
@ -2364,7 +2364,8 @@ _profile:
|
||||
addMutualLinkSection: "セクションを追加"
|
||||
sectionName: "セクション名"
|
||||
sectionNameNoneDescription: "セクション名を表示しないようにする"
|
||||
sectionNameNone: "セクション名を表示しない"
|
||||
sectionNameNone: "名前が表示されないセクション"
|
||||
policyDisplayLimitExceeded: "現在の支援プランの表示上限({max}個)を超えているため、この項目は表示されません。[ここ](https://go.misskey.io/donate)からプランをアップグレードできます。"
|
||||
|
||||
_exportOrImport:
|
||||
allNotes: "全てのノート"
|
||||
|
@ -1275,7 +1275,7 @@ _initialAccountSetting:
|
||||
pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をあんたのデバイスで受け取れるで。"
|
||||
initialAccountSettingCompleted: "初期設定終わりや!"
|
||||
haveFun: "{name}、楽しんでな~"
|
||||
youCanContinueTutorial: "こんまま{name}(Misskey)の使い方のチュートリアルにも行けるけど、ここでやめてすぐに使い始めてもええで。"
|
||||
continueTutorial: "こんまま{name}(Misskey)の使い方のチュートリアルが始まるで。"
|
||||
startTutorial: "チュートリアルはじめる"
|
||||
skipAreYouSure: "初期設定飛ばすか?"
|
||||
laterAreYouSure: "初期設定あとでやり直すん?"
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
_lang_: "한국어"
|
||||
headlineMisskey: "노트로 연결되는 네트워크"
|
||||
introMisskey: "환영합니다! Misskey는 오픈 소스 분산형 마이크로 블로그 서비스입니다.\n'노트'를 작성해서 지금 일어나고 있는 일을 공유하거나, 당신만의 이야기를 모두에게 발신하세요📡\n'리액션' 기능으로 친구의 노트에 총알같이 반응할 수도 있습니다👍\n새로운 세계를 탐험해 보세요🚀"
|
||||
introMisskey: "환영합니다! Misskey는 오픈 소스 분산형 마이크로 블로그 서비스입니다.\n'노트'를 작성해서 지금 일어나고 있는 일을 공유하거나, 당신만의 이야기를 모두에게 발신하세요📡\n'리액션' 기능으로 친구의 노트에 총알같이 반응을 추가할 수도 있습니다👍\n새로운 세계를 탐험해 보세요🚀"
|
||||
poweredByMisskeyDescription: "{name} 서버는 오픈소스 플랫폼 <b>Misskey</b>의 서버 가운데 하나입니다."
|
||||
monthAndDay: "{month}월 {day}일"
|
||||
search: "검색"
|
||||
@ -587,7 +587,7 @@ scratchpadDescription: "스크래치 패드는 AiScript 의 테스트 환경을
|
||||
output: "출력"
|
||||
script: "스크립트"
|
||||
disablePagesScript: "Pages 에서 AiScript 를 사용하지 않음"
|
||||
updateRemoteUser: "원격 유저 정보 갱신"
|
||||
updateRemoteUser: "리모트 유저 정보 갱신"
|
||||
unsetUserAvatar: "아바타 제거"
|
||||
unsetUserAvatarConfirm: "아바타를 제거할까요?"
|
||||
unsetUserBanner: "배너 제거"
|
||||
@ -2297,8 +2297,9 @@ _profile:
|
||||
addMutualLink: "서로링크 추가"
|
||||
addMutualLinkSection: "섹션 추가"
|
||||
sectionName: "섹션 이름"
|
||||
sectionNameNoneDescription: "섹션 이름이 표시되지 않도록 합니다."
|
||||
sectionNameNone: "섹션 이름을 숨기기"
|
||||
sectionNameNoneDescription: "섹션 이름이 표시되지 않도록 합니다"
|
||||
sectionNameNone: "이름이 표시되지 않는 섹션"
|
||||
policyDisplayLimitExceeded: "현재 지원 플랜의 표시 제한({max}개)을 초과하였기 때문에 이 항목은 표시되지 않습니다. [여기](https://go.misskey.io/donate)에서 플랜을 업그레이드할 수 있습니다."
|
||||
_exportOrImport:
|
||||
allNotes: "모든 노트"
|
||||
favoritedNotes: "즐겨찾기한 노트"
|
||||
|
@ -253,6 +253,11 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
const meta = await this.metaService.fetch();
|
||||
const policies = await this.roleService.getUserPolicies(user.id);
|
||||
|
||||
if (!policies.canCreateContent) {
|
||||
this.logger.error('Request rejected because user has no permission to create content', { user: user.id, note: data });
|
||||
throw new IdentifiableError('5b1c2b67-50a6-4a8a-a59c-0ede40890de3', 'User has no permission to create content.');
|
||||
}
|
||||
|
||||
if (data.visibility === 'public' && data.channel == null) {
|
||||
const sensitiveWords = meta.sensitiveWords;
|
||||
if (this.utilityService.isKeyWordIncluded(data.cw ?? this.utilityService.concatNoteContentsForKeyWordCheck({ text: data.text, pollChoices: data.poll?.choices }), sensitiveWords)) {
|
||||
|
@ -116,8 +116,13 @@ export class ReactionService {
|
||||
if (!await this.noteEntityService.isVisibleForMe(note, user.id)) {
|
||||
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
||||
}
|
||||
|
||||
const policies = await this.roleService.getUserPolicies(user.id);
|
||||
|
||||
if (!policies.canUpdateContent) {
|
||||
throw new IdentifiableError('cf63c2de-0df1-4db5-9fff-b2110b6e5450', 'User has no permission to update content.');
|
||||
}
|
||||
|
||||
let reaction = _reaction ?? FALLBACK;
|
||||
if (note.reactionAcceptance === 'likeOnly' || !policies.canUseReaction || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
|
||||
reaction = '\u2764';
|
||||
|
@ -113,7 +113,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
avatarDecorationLimit: 1,
|
||||
canUseAccountRemoval: true,
|
||||
mutualLinkSectionLimit: 1,
|
||||
mutualLinkLimit: 15,
|
||||
mutualLinkLimit: 3,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
|
@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { FollowingsRepository } from '@/models/_.js';
|
||||
import type { FollowingsRepository, FollowRequestsRepository } from '@/models/_.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
@ -27,6 +27,9 @@ export class UserSuspendService {
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.followRequestsRepository)
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private apRendererService: ApRendererService,
|
||||
@ -42,6 +45,11 @@ export class UserSuspendService {
|
||||
|
||||
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
|
||||
|
||||
await Promise.all([
|
||||
this.followRequestsRepository.delete({ followeeId: user.id }),
|
||||
this.followRequestsRepository.delete({ followerId: user.id }),
|
||||
]).catch(() => null);
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 知り得る全SharedInboxにDelete配信
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
|
||||
|
@ -466,7 +466,7 @@ export class ApRendererService {
|
||||
this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
|
||||
]);
|
||||
|
||||
const attachment = profile.fields.map(field => ({
|
||||
const profileFields = profile.fields.map(field => ({
|
||||
type: 'PropertyValue',
|
||||
name: field.name,
|
||||
value: (field.value.startsWith('http://') || field.value.startsWith('https://'))
|
||||
@ -474,6 +474,16 @@ export class ApRendererService {
|
||||
: field.value,
|
||||
}));
|
||||
|
||||
const mutualLinks = profile.mutualLinkSections.flatMap(section =>
|
||||
section.mutualLinks.map(link => ({
|
||||
type: 'PropertyValue',
|
||||
name: section.name ?? link.description ?? 'Link',
|
||||
value: `<a href="${link.url}" target="_blank">${link.description ?? link.url}</a>`,
|
||||
})),
|
||||
);
|
||||
|
||||
const attachment = mutualLinks.concat(profileFields);
|
||||
|
||||
const emojis = await this.getEmojis(user.emojis);
|
||||
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
||||
|
||||
|
@ -534,7 +534,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
lang: profile?.lang,
|
||||
fields: profile?.fields,
|
||||
verifiedLinks: profile?.verifiedLinks,
|
||||
mutualLinkSections: profile?.mutualLinkSections,
|
||||
mutualLinkSections: isMe ? profile?.mutualLinkSections : profile?.mutualLinkSections.slice(0, policies?.mutualLinkSectionLimit).map(section => ({ ...section, mutualLinks: section.mutualLinks.slice(0, policies?.mutualLinkLimit) })),
|
||||
followersCount: followersCount ?? 0,
|
||||
followingCount: followingCount ?? 0,
|
||||
notesCount: user.notesCount,
|
||||
|
@ -49,6 +49,8 @@ export class MiUserProfile {
|
||||
public mutualLinkSections: {
|
||||
name: string | null;
|
||||
mutualLinks: {
|
||||
id: string;
|
||||
url: string;
|
||||
fileId: MiDriveFile['id'];
|
||||
description: string | null;
|
||||
imgSrc: string;
|
||||
|
@ -397,12 +397,13 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string' },
|
||||
id: { type: 'string', format: 'misskey:id' },
|
||||
url: { type: 'string', format: 'url' },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
description: { type: 'string', nullable: true },
|
||||
imgSrc: { type: 'string' },
|
||||
},
|
||||
required: ['url', 'fileId'],
|
||||
required: ['id', 'url', 'fileId'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin', 'role'],
|
||||
@ -33,12 +34,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
constructor(
|
||||
private metaService: MetaService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const before = await this.metaService.fetch(true);
|
||||
|
||||
await this.metaService.update({
|
||||
policies: ps.policies,
|
||||
});
|
||||
this.globalEventService.publishInternalEvent('policiesUpdated', ps.policies);
|
||||
|
||||
const after = await this.metaService.fetch(true);
|
||||
|
||||
this.globalEventService.publishInternalEvent('policiesUpdated', after.policies);
|
||||
this.moderationLogService.log(me, 'updateServerSettings', {
|
||||
before: before.policies,
|
||||
after: after.policies,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
|
||||
import { ApiLoggerService } from '../../ApiLoggerService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { IdService } from "@/core/IdService.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
@ -116,6 +117,12 @@ export const meta = {
|
||||
id: 'bf326f31-d430-4f97-9933-5d61e4d48a23',
|
||||
},
|
||||
|
||||
invalidUrl: {
|
||||
message: 'Invalid URL',
|
||||
code: 'INVALID_URL',
|
||||
id: 'b2452e00-2bd0-4da8-a2d0-972859da7358',
|
||||
},
|
||||
|
||||
forbiddenToSetYourself: {
|
||||
message: 'You can\'t set yourself as your own alias.',
|
||||
code: 'FORBIDDEN_TO_SET_YOURSELF',
|
||||
@ -227,12 +234,14 @@ export const paramDef = {
|
||||
},
|
||||
mutualLinkSections: {
|
||||
type: 'array',
|
||||
maxItems: 10,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', nullable: true },
|
||||
mutualLinks: {
|
||||
type: 'array',
|
||||
maxItems: 30,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@ -268,6 +277,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
@Inject(DI.pagesRepository)
|
||||
private pagesRepository: PagesRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
@ -357,26 +367,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}
|
||||
|
||||
if (ps.mutualLinkSections) {
|
||||
if (ps.mutualLinkSections.length > policy.mutualLinkSectionLimit) {
|
||||
throw new ApiError(meta.errors.restrictedByRole);
|
||||
}
|
||||
|
||||
const mutualLinkSections = ps.mutualLinkSections.map(async (section) => {
|
||||
if (section.mutualLinks.length > policy.mutualLinkLimit) {
|
||||
throw new ApiError(meta.errors.restrictedByRole);
|
||||
}
|
||||
|
||||
const mutualLinks = await Promise.all(section.mutualLinks.map(async (mutualLink) => {
|
||||
const file = await this.driveFilesRepository.findOneBy({ id: mutualLink.fileId });
|
||||
if (!RegExp(/^https?:\/\//).test(mutualLink.url)) throw new ApiError(meta.errors.invalidUrl);
|
||||
|
||||
if (!file) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
if (!file.type.startsWith('image/')) {
|
||||
throw new ApiError(meta.errors.fileNotAnImage);
|
||||
}
|
||||
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 {
|
||||
id: this.idService.gen(),
|
||||
url: mutualLink.url,
|
||||
fileId: file.id,
|
||||
imgSrc: this.driveFileEntityService.getPublicUrl(file),
|
||||
|
@ -318,7 +318,7 @@ export type ModerationLogPayloads = {
|
||||
unsetUserMutualLink: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userMutualLinkSections: { name: string | null; mutualLinks: { fileId: string; description: string | null; imgSrc: string; }[]; }[] | []
|
||||
userMutualLinkSections: { name: string | null; mutualLinks: { id: string; url: string; fileId: string; description: string | null; imgSrc: string; }[]; }[] | []
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span :class="$style.title">
|
||||
<slot name="header"></slot>
|
||||
</span>
|
||||
<button v-if="!withOkButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button>
|
||||
<button v-if="!withOkButton && withCloseButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button>
|
||||
<button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button>
|
||||
</div>
|
||||
<div :class="$style.body">
|
||||
@ -27,12 +27,16 @@ import MkModal from './MkModal.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
withOkButton: boolean;
|
||||
withCloseButton: boolean;
|
||||
okButtonDisabled: boolean;
|
||||
escKeyDisabled: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
}>(), {
|
||||
withOkButton: false,
|
||||
withCloseButton: true,
|
||||
okButtonDisabled: false,
|
||||
escKeyDisabled: false,
|
||||
width: 400,
|
||||
height: 500,
|
||||
});
|
||||
@ -60,6 +64,7 @@ const onBgClick = () => {
|
||||
|
||||
const onKeydown = (evt) => {
|
||||
if (evt.which === 27) { // Esc
|
||||
if (props.escKeyDisabled) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
close();
|
||||
|
@ -102,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
{{ i18n.ts.support }}
|
||||
</FormLink>
|
||||
<!--
|
||||
<FormLink to="https://misskeyhq.fanbox.cc" external>
|
||||
<FormLink to="https://go.misskey.io/donate" external>
|
||||
<template #icon><i class="ti ti-pig-money"></i></template>
|
||||
{{ i18n.tsx.supportThisInstance({ name: instance.name ?? host }) }}
|
||||
<template #suffix>pixivFANBOX</template>
|
||||
|
@ -483,6 +483,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseReaction, 'canUseReaction'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUseReaction }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.canUseReaction.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canUseReaction.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUseReaction)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.canUseReaction.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="role.policies.canUseReaction.value" :disabled="role.policies.canUseReaction.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="role.policies.canUseReaction.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.canUseTranslator, 'canUseTranslator'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUseTranslator }}</template>
|
||||
<template #suffix>
|
||||
@ -523,26 +543,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseReaction, 'canUseReaction'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUseReaction }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.canUseReaction.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canUseReaction.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUseReaction)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.canUseReaction.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="role.policies.canUseReaction.value" :disabled="role.policies.canUseReaction.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="role.policies.canUseReaction.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.driveCapacity, 'driveCapacityMb'])">
|
||||
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
|
||||
<template #suffix>
|
||||
@ -775,25 +775,6 @@ 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>
|
||||
@ -813,6 +794,25 @@ 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.canHideAds, 'canHideAds'])">
|
||||
<template #label>{{ i18n.ts._role._options.canHideAds }}</template>
|
||||
<template #suffix>
|
||||
|
@ -174,6 +174,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseReaction, 'canUseReaction'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUseReaction }}</template>
|
||||
<template #suffix>{{ policies.canUseReaction ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canUseReaction">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseTranslator, 'canUseTranslator'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUseTranslator }}</template>
|
||||
<template #suffix>{{ policies.canUseTranslator ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
|
@ -94,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<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 inline style="margin-right: 8px;" @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>
|
||||
@ -117,11 +117,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<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 class="_gaps_s" :class="$style.metadataMargin">
|
||||
<MkInfo v-if="sectionIndex >= $i.policies.mutualLinkSectionLimit" warn><Mfm :text="i18n.tsx._profile.policyDisplayLimitExceeded({ max: $i.policies.mutualLinkSectionLimit })"/></MkInfo>
|
||||
<MkInput v-if="sectionElement.name !== null" v-model="sectionElement.name" :placeholder="i18n.ts._profile.sectionName" :max="32"></MkInput>
|
||||
<MkSwitch v-model="sectionElement.none" @update:modelValue="()=>{ sectionElement.none ? sectionElement.name = null : sectionElement.name = 'New Section' }">{{ i18n.ts._profile.sectionNameNoneDescription }}</MkSwitch>
|
||||
<MkButton inline style="margin-right: 8px;" @click="addMutualLinks(sectionIndex)"><i class="ti ti-plus"></i> {{ i18n.ts._profile.addMutualLink }}</MkButton>
|
||||
</div>
|
||||
|
||||
<Sortable
|
||||
v-model="sectionElement.mutualLinks"
|
||||
class="_gaps_s"
|
||||
@ -137,6 +139,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<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}">
|
||||
<MkInfo v-if="linkIndex >= $i.policies.mutualLinkLimit" warn><Mfm :text="i18n.tsx._profile.policyDisplayLimitExceeded({ max: $i.policies.mutualLinkLimit })"/></MkInfo>
|
||||
<MkInput v-model="linkElement.url" small>
|
||||
<template #label>{{ i18n.ts._profile.mutualLinksUrl }}</template>
|
||||
</MkInput>
|
||||
@ -182,7 +185,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, watch, defineAsyncComponent, Ref } from 'vue';
|
||||
import { computed, reactive, ref, watch, defineAsyncComponent } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
@ -201,7 +204,6 @@ 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();
|
||||
|
||||
@ -224,7 +226,7 @@ watch(() => profile, () => {
|
||||
deep: true,
|
||||
});
|
||||
|
||||
const mutualLinkSections = ref($i.mutualLinkSections ?? []) as Ref<Misskey.entities.UserDetailed['mutualLinkSections']>;
|
||||
const mutualLinkSections = ref($i.mutualLinkSections.map(section => ({ ...section, id: Math.random().toString(), none: !section.name })) ?? []);
|
||||
const fields = ref($i.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []);
|
||||
const fieldEditMode = ref(false);
|
||||
const mutualLinkSectionEditMode = ref(false);
|
||||
@ -239,6 +241,7 @@ function addField() {
|
||||
|
||||
function addMutualLinks(index:number) {
|
||||
mutualLinkSections.value[index].mutualLinks.push({
|
||||
id: Math.random().toString(),
|
||||
fileId: '',
|
||||
url: '',
|
||||
imgSrc: '',
|
||||
@ -248,7 +251,9 @@ function addMutualLinks(index:number) {
|
||||
|
||||
function addMutualLinkSections() {
|
||||
mutualLinkSections.value.push({
|
||||
name: null,
|
||||
id: Math.random().toString(),
|
||||
name: 'New Section',
|
||||
none: false,
|
||||
mutualLinks: [],
|
||||
});
|
||||
}
|
||||
@ -494,8 +499,8 @@ definePageMetadata(() => ({
|
||||
}
|
||||
|
||||
.mutualLinkImg {
|
||||
max-width: 150px;
|
||||
max-height: 30px;
|
||||
max-width: 200px;
|
||||
max-height: 40px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
|
@ -80,11 +80,23 @@ 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}">
|
||||
<MkContainer v-if="$i && $i.id == user.id && user?.mutualLinkSections?.slice(0, $i.policies.mutualLinkSectionLimit).length > 0" :showHeader="false" :max-height="200" class="fields" :style="{borderRadius: 0}">
|
||||
<div v-for="(section, index) in user?.mutualLinkSections.slice(0, $i.policies.mutualLinkSectionLimit)" :key="index" :class="$style.mutualLinkSections">
|
||||
<span v-if="section.name">{{ section.name }}</span>
|
||||
<div :class="$style.mutualLinks">
|
||||
<div v-for="mutualLink in section.mutualLinks.slice(0, $i.policies.mutualLinkLimit)" :key="mutualLink.id">
|
||||
<MkLink :hideIcon="true" :url="mutualLink.url">
|
||||
<img :class="$style.mutualLinkImg" :src="mutualLink.imgSrc" :alt="mutualLink.description"/>
|
||||
</MkLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkContainer>
|
||||
<MkContainer v-else-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">
|
||||
<div v-for="mutualLink in section.mutualLinks" :key="mutualLink.id">
|
||||
<MkLink :hideIcon="true" :url="mutualLink.url">
|
||||
<img :class="$style.mutualLinkImg" :src="mutualLink.imgSrc" :alt="mutualLink.description"/>
|
||||
</MkLink>
|
||||
@ -837,7 +849,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.mutualLinkImg {
|
||||
max-width: 150px;
|
||||
max-height: 30px;
|
||||
max-width: 200px;
|
||||
max-height: 40px;
|
||||
}
|
||||
</style>
|
||||
|
@ -526,7 +526,6 @@ declare module '../api.js' {
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:federation*
|
||||
*/
|
||||
request<E extends 'admin/federation/remove-all-following', P extends Endpoints[E]['req']>(
|
||||
|
@ -443,7 +443,6 @@ export type paths = {
|
||||
* admin/federation/remove-all-following
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:federation*
|
||||
*/
|
||||
post: operations['admin___federation___remove-all-following'];
|
||||
@ -3857,6 +3856,9 @@ export type components = {
|
||||
mutualLinkSections: ({
|
||||
name: string | null;
|
||||
mutualLinks: ({
|
||||
/** Format: misskey:id */
|
||||
id: string;
|
||||
/** Format: url */
|
||||
url: string;
|
||||
/** Format: misskey:id */
|
||||
fileId: string;
|
||||
@ -8128,7 +8130,6 @@ export type operations = {
|
||||
* admin/federation/remove-all-following
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:federation*
|
||||
*/
|
||||
'admin___federation___remove-all-following': {
|
||||
|
Loading…
Reference in New Issue
Block a user