spec(profile): 相互リンク機能のロールによる制御の仕様変更 (MisskeyIO#701)

This commit is contained in:
まっちゃとーにゅ 2024-08-14 20:04:06 +09:00 committed by GitHub
parent 80389a9140
commit 09c419e11b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 61 additions and 43 deletions

View File

@ -2273,6 +2273,7 @@ _profile:
sectionName: "Section name" sectionName: "Section name"
sectionNameNoneDescription: "Do not display the section name" sectionNameNoneDescription: "Do not display the section name"
sectionNameNone: "Section without 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: _exportOrImport:
allNotes: "All notes" allNotes: "All notes"
favoritedNotes: "Favorite notes" favoritedNotes: "Favorite notes"

4
locales/index.d.ts vendored
View File

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

View File

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

View File

@ -2255,8 +2255,9 @@ _profile:
addMutualLink: "서로링크 추가" addMutualLink: "서로링크 추가"
addMutualLinkSection: "섹션 추가" addMutualLinkSection: "섹션 추가"
sectionName: "섹션 이름" sectionName: "섹션 이름"
sectionNameNoneDescription: "섹션 이름이 표시되지 않도록 합니다." sectionNameNoneDescription: "섹션 이름이 표시되지 않도록 합니다"
sectionNameNone: "이름이 표시되지 않는 섹션" sectionNameNone: "이름이 표시되지 않는 섹션"
policyDisplayLimitExceeded: "현재 지원 플랜의 표시 제한({max}개)을 초과하였기 때문에 이 항목은 표시되지 않습니다. [여기](https://go.misskey.io/donate)에서 플랜을 업그레이드할 수 있습니다."
_exportOrImport: _exportOrImport:
allNotes: "모든 노트" allNotes: "모든 노트"
favoritedNotes: "즐겨찾기한 노트" favoritedNotes: "즐겨찾기한 노트"

View File

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

View File

@ -533,7 +533,7 @@ export class UserEntityService implements OnModuleInit {
lang: profile!.lang, lang: profile!.lang,
fields: profile!.fields, fields: profile!.fields,
verifiedLinks: profile!.verifiedLinks, 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, followersCount: followersCount ?? 0,
followingCount: followingCount ?? 0, followingCount: followingCount ?? 0,
notesCount: user.notesCount, notesCount: user.notesCount,

View File

@ -117,6 +117,12 @@ export const meta = {
id: 'bf326f31-d430-4f97-9933-5d61e4d48a23', id: 'bf326f31-d430-4f97-9933-5d61e4d48a23',
}, },
invalidUrl: {
message: 'Invalid URL',
code: 'INVALID_URL',
id: 'b2452e00-2bd0-4da8-a2d0-972859da7358',
},
forbiddenToSetYourself: { forbiddenToSetYourself: {
message: 'You can\'t set yourself as your own alias.', message: 'You can\'t set yourself as your own alias.',
code: 'FORBIDDEN_TO_SET_YOURSELF', code: 'FORBIDDEN_TO_SET_YOURSELF',
@ -228,12 +234,14 @@ export const paramDef = {
}, },
mutualLinkSections: { mutualLinkSections: {
type: 'array', type: 'array',
maxItems: 10,
items: { items: {
type: 'object', type: 'object',
properties: { properties: {
name: { type: 'string', nullable: true }, name: { type: 'string', nullable: true },
mutualLinks: { mutualLinks: {
type: 'array', type: 'array',
maxItems: 30,
items: { items: {
type: 'object', type: 'object',
properties: { properties: {
@ -359,24 +367,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
if (ps.mutualLinkSections) { if (ps.mutualLinkSections) {
if (ps.mutualLinkSections.length > policy.mutualLinkSectionLimit) {
throw new ApiError(meta.errors.restrictedByRole);
}
const mutualLinkSections = ps.mutualLinkSections.map(async (section) => { 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 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) { const file = await this.driveFilesRepository.findOneBy({ id: mutualLink.fileId });
throw new ApiError(meta.errors.noSuchFile); if (!file) throw new ApiError(meta.errors.noSuchFile);
} if (!file.type.startsWith("image/")) throw new ApiError(meta.errors.fileNotAnImage);
if (!file.type.startsWith('image/')) {
throw new ApiError(meta.errors.fileNotAnImage);
}
return { return {
id: this.idService.gen(), id: this.idService.gen(),

View File

@ -99,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-message"></i></template> <template #icon><i class="ti ti-message"></i></template>
{{ i18n.ts.support }} {{ i18n.ts.support }}
</FormLink> </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> <template #icon><i class="ti ti-pig-money"></i></template>
{{ i18n.tsx.supportThisInstance({ name: instance.name ?? host }) }} {{ i18n.tsx.supportThisInstance({ name: instance.name ?? host }) }}
<template #suffix>pixivFANBOX</template> <template #suffix>pixivFANBOX</template>

View File

@ -755,25 +755,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkFolder> </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'])"> <MkFolder v-if="matchQuery([i18n.ts._role._options.mutualLinkSectionLimit, 'mutualLinkSectionLimit'])">
<template #label>{{ i18n.ts._role._options.mutualLinkSectionLimit }}</template> <template #label>{{ i18n.ts._role._options.mutualLinkSectionLimit }}</template>
<template #suffix> <template #suffix>
@ -793,6 +774,25 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkFolder> </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'])"> <MkFolder v-if="matchQuery([i18n.ts._role._options.canHideAds, 'canHideAds'])">
<template #label>{{ i18n.ts._role._options.canHideAds }}</template> <template #label>{{ i18n.ts._role._options.canHideAds }}</template>
<template #suffix> <template #suffix>

View File

@ -94,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.metadataRoot"> <div :class="$style.metadataRoot">
<div :class="$style.metadataMargin"> <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-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 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> <MkButton inline primary @click="saveMutualLinks"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
@ -109,7 +109,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@start="e => e.item.classList.add('active')" @start="e => e.item.classList.add('active')"
@end="e => e.item.classList.remove('active')" @end="e => e.item.classList.remove('active')"
> >
<template #item="{element: sectionElement,index: sectionIndex}"> <template #item="{ element: sectionElement, index: sectionIndex }">
<div :class="$style.mutualLinkSectionRoot"> <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" 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> <button v-if="mutualLinkSectionEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteMutualLinkSection(sectionIndex)"><i class="ti ti-x"></i></button>
@ -118,9 +118,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ sectionElement.name || i18n.ts._profile.sectionNameNone }}</template> <template #label>{{ sectionElement.name || i18n.ts._profile.sectionNameNone }}</template>
<div class="_gaps_s" :class="$style.metadataMargin"> <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> <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> <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;" :disabled="sectionElement.mutualLinks.length >= $i.policies.mutualLinkLimit" @click="addMutualLinks(sectionIndex)"><i class="ti ti-plus"></i> {{ i18n.ts._profile.addMutualLink }}</MkButton> <MkButton inline style="margin-right: 8px;" @click="addMutualLinks(sectionIndex)"><i class="ti ti-plus"></i> {{ i18n.ts._profile.addMutualLink }}</MkButton>
</div> </div>
<Sortable <Sortable
@ -132,12 +133,13 @@ SPDX-License-Identifier: AGPL-3.0-only
@start="e => e.item.classList.add('active')" @start="e => e.item.classList.add('active')"
@end="e => e.item.classList.remove('active')" @end="e => e.item.classList.remove('active')"
> >
<template #item="{element: linkElement,index: linkIndex}"> <template #item="{ element: linkElement, index: linkIndex }">
<div :class="$style.mutualLinkRoot"> <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" 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> <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}"> <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> <MkInput v-model="linkElement.url" small>
<template #label>{{ i18n.ts._profile.mutualLinksUrl }}</template> <template #label>{{ i18n.ts._profile.mutualLinksUrl }}</template>
</MkInput> </MkInput>

View File

@ -80,7 +80,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p> <p v-else class="empty">{{ i18n.ts.noAccountDescription }}</p>
</MkOmit> </MkOmit>
</div> </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"> <div v-for="(section, index) in user?.mutualLinkSections" :key="index" :class="$style.mutualLinkSections">
<span v-if="section.name">{{ section.name }}</span> <span v-if="section.name">{{ section.name }}</span>
<div :class="$style.mutualLinks"> <div :class="$style.mutualLinks">