Merge upstream

This commit is contained in:
オスカー、 2024-08-17 20:40:14 +09:00
commit ce48a79f6b
Signed by: SWREI
GPG Key ID: 139D6573F92DA9F7
24 changed files with 173 additions and 94 deletions

View File

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

@ -8999,9 +8999,13 @@ export interface Locale extends ILocale {
*/
"sectionNameNoneDescription": string;
/**
*
*
*/
"sectionNameNone": string;
/**
* ({max})[](https://go.misskey.io/donate)からプランをアップグレードできます。
*/
"policyDisplayLimitExceeded": ParameterizedString<"max">;
};
"_exportOrImport": {
/**

View File

@ -2364,7 +2364,8 @@ _profile:
addMutualLinkSection: "セクションを追加"
sectionName: "セクション名"
sectionNameNoneDescription: "セクション名を表示しないようにする"
sectionNameNone: "セクション名を表示しない"
sectionNameNone: "名前が表示されないセクション"
policyDisplayLimitExceeded: "現在の支援プランの表示上限({max}個)を超えているため、この項目は表示されません。[ここ](https://go.misskey.io/donate)からプランをアップグレードできます。"
_exportOrImport:
allNotes: "全てのノート"

View File

@ -1275,7 +1275,7 @@ _initialAccountSetting:
pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をあんたのデバイスで受け取れるで。"
initialAccountSettingCompleted: "初期設定終わりや!"
haveFun: "{name}、楽しんでな~"
youCanContinueTutorial: "こんまま{name}(Misskey)の使い方のチュートリアルにも行けるけど、ここでやめてすぐに使い始めてもええで。"
continueTutorial: "こんまま{name}(Misskey)の使い方のチュートリアルが始まるで。"
startTutorial: "チュートリアルはじめる"
skipAreYouSure: "初期設定飛ばすか?"
laterAreYouSure: "初期設定あとでやり直すん?"

View File

@ -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: "즐겨찾기한 노트"

View File

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

View File

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

View File

@ -113,7 +113,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
avatarDecorationLimit: 1,
canUseAccountRemoval: true,
mutualLinkSectionLimit: 1,
mutualLinkLimit: 15,
mutualLinkLimit: 3,
};
@Injectable()

View File

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

View File

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

View File

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

View File

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

View File

@ -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'],
},
},
},

View File

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

View File

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

View 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; }[]; }[] | []
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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']>(

View File

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